Spring之借助Redis设计一个简单访问计数器的示例

为什么要做一个访问计数?之前的个人博客用得是卜算子做站点访问计数,用起来挺好,但出现较多次的响应很慢,再其次就是个人博客实在是访问太少,数据不好看😢…

前面一篇博文简单介绍了Spring中的RedisTemplate的配置与使用,那么这篇算是一个简单的应用case了,主要基于Redis的计数器来实现统计

I. 设计

一个简单的访问计数器,主要利用redis的hash结构,对应的存储结构如下:

存储结构比较简单,为了扩展,每个应用(or站点)对应一个APP,然后根据path路径进行分页统计,最后有一个特殊的用于统计全站的访问计数

II. 实现

主要就是利用Redis的hash结构,然后实现数据统计,并没有太多的难度,Spring环境下搭建redis环境可以参考:

Spring之RedisTemplate配置与使用

1. Redis封装类

针对几个常用的做了简单的封装,直接使用RedisTemplate的excute方法进行的操作,当然也是可以使用 template.opsForValue() 等便捷方式,这里采用JSON方式进行对象的序列化和反序列化

public class QuickRedisClient {
  private static final Charset CODE = Charset.forName("UTF-8");
  private static RedisTemplate<String, String> template;

  public static void register(RedisTemplate<String, String> template) {
    QuickRedisClient.template = template;
  }

  public static void nullCheck(Object... args) {
    for (Object obj : args) {
      if (obj == null) {
        throw new IllegalArgumentException("redis argument can not be null!");
      }
    }
  }

  public static byte[] toBytes(String key) {
    nullCheck(key);
    return key.getBytes(CODE);
  }

  public static byte[][] toBytes(List<String> keys) {
    byte[][] bytes = new byte[keys.size()][];
    int index = 0;
    for (String key : keys) {
      bytes[index++] = toBytes(key);
    }
    return bytes;
  }

  public static String getStr(String key) {
    return template.execute((RedisCallback<String>) con -> {
      byte[] val = con.get(toBytes(key));
      return val == null ? null : new String(val);
    });
  }

  public static void putStr(String key, String value) {
    template.execute((RedisCallback<Void>) con -> {
      con.set(toBytes(key), toBytes(value));
      return null;
    });
  }

  public static Long incr(String key, long add) {
    return template.execute((RedisCallback<Long>) con -> {
      Long record = con.incrBy(toBytes(key), add);
      return record == null ? 0L : record;
    });
  }

  public static Long hIncr(String key, String field, long add) {
    return template.execute((RedisCallback<Long>) con -> {
      Long record = con.hIncrBy(toBytes(key), toBytes(field), add);
      return record == null ? 0L : record;
    });
  }

  public static <T> T hGet(String key, String field, Class<T> clz) {
    return template.execute((RedisCallback<T>) con -> {
      byte[] records = con.hGet(toBytes(key), toBytes(field));
      if (records == null) {
        return null;
      }

      return JSON.parseObject(records, clz);
    });
  }

  public static <T> Map<String, T> hMGet(String key, List<String> fields, Class<T> clz) {
    List<byte[]> list =
        template.execute((RedisCallback<List<byte[]>>) con -> con.hMGet(toBytes(key), toBytes(fields)));
    if (CollectionUtils.isEmpty(list)) {
      return Collections.emptyMap();
    }

    Map<String, T> result = new HashMap<>();
    for (int i = 0; i < fields.size(); i++) {
      if (list.get(i) == null) {
        continue;
      }

      result.put(fields.get(i), JSON.parseObject(list.get(i), clz));
    }
    return result;
  }
}

对应的配置类

package com.git.hui.story.cache.redis;

import com.git.hui.story.cache.redis.serializer.DefaultStrSerializer;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * Created by yihui in 18:45 18/6/11.
 */
@Configuration
@PropertySource(value = "classpath:application.yml")
public class RedisConf {

  private final Environment environment;

  public RedisConf(Environment environment) {
    this.environment = environment;
  }

  @Bean
  public CacheManager cacheManager() {
    return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory()).build();
  }

  @Bean
  public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);

    DefaultStrSerializer serializer = new DefaultStrSerializer();
    redisTemplate.setValueSerializer(serializer);
    redisTemplate.setHashValueSerializer(serializer);
    redisTemplate.setKeySerializer(serializer);
    redisTemplate.setHashKeySerializer(serializer);

    redisTemplate.afterPropertiesSet();

    QuickRedisClient.register(redisTemplate);
    return redisTemplate;
  }

  @Bean
  public RedisConnectionFactory redisConnectionFactory() {
    LettuceConnectionFactory fac = new LettuceConnectionFactory();
    fac.getStandaloneConfiguration().setHostName(environment.getProperty("spring.redis.host"));
    fac.getStandaloneConfiguration().setPort(Integer.parseInt(environment.getProperty("spring.redis.port")));
    fac.getStandaloneConfiguration()
        .setPassword(RedisPassword.of(environment.getProperty("spring.redis.password")));
    fac.afterPropertiesSet();
    return fac;
  }
}

2. Controller 支持

首先是定义请求参数:

@Data
public class WebCountReqDO implements Serializable {
  private String appKey;
  private String referer;
}

其次是实现Controller接口,稍稍注意下,根据path进行计数的逻辑:

  1. 如果请求参数显示指定了referer参数,则用传入的参数进行统计
  2. 如果没有显示指定referer,则根据header获取referer
  3. 解析referer,分别对path和host进行统计+1,这样站点的统计计数就是根据host来的,而页面的统计计数则是根据path路径来的
@Slf4j
@RestController
@RequestMapping(path = "/count")
public class WebCountController {

  @RequestMapping(path = "cc", method = {RequestMethod.GET})
  public ResponseWrapper<CountDTO> addCount(WebCountReqDO webCountReqDO) {
    String appKey = webCountReqDO.getAppKey();
    if (StringUtils.isBlank(appKey)) {
      return ResponseWrapper.errorReturnMix(Status.StatusEnum.ILLEGAL_PARAMS_MIX, "请指定APPKEY!");
    }

    String referer = ReqInfoContext.getReqInfo().getReferer();
    if (StringUtils.isBlank(referer)) {
      referer = webCountReqDO.getReferer();
    }

    if (StringUtils.isBlank(referer)) {
      return ResponseWrapper.errorReturnMix(Status.StatusEnum.FAIL_MIX, "无法获取请求referer!");
    }

    return ResponseWrapper.successReturn(doUpdateCnt(appKey, referer));
  }

  private CountDTO doUpdateCnt(String appKey, String referer) {
    try {
      if (!referer.startsWith("http")) {
        referer = "https://" + referer;
      }

      URI uri = new URI(referer);
      String host = uri.getHost();
      String path = uri.getPath();
      long count = QuickRedisClient.hIncr(appKey, path, 1);
      long total = QuickRedisClient.hIncr(appKey, host, 1);
      return new CountDTO(count, total);
    } catch (Exception e) {
      log.error("get referer path error! referer: {}, e: {}", referer, e);
      return new CountDTO(1L, 1L);
    }
  }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Redis实现高并发计数器

    业务需求中经常有需要用到计数器的场景:譬如一个手机号一天限制发送5条短信.一个接口一分钟限制多少请求.一个接口一天限制调用多少次等等.使用Redis的Incr自增命令可以轻松实现以上需求.以一个接口一天限制调用次数为例: /** * 是否拒绝服务 * @return */ private boolean denialOfService(String userId){ long count=JedisUtil.setIncr(DateUtil.getDate()+"&"+user

  • redis实现计数器-防止刷单方法介绍

    最近由于双11要来临,公司需要在接口请求上,做一下并发限制的处理,或者做一个防止刷单的安全拦截: 比如:一个接口请求,限制每秒请求总数为200次,超过200次就等待,等下一秒,再次请求,这里用到一个redis作为一个计数器的模式来实现. 调用redis的方法: INCR key 将 key 中储存的数字值增一. 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作. 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误. 这是一个针对字符串的

  • Redis的使用模式之计数器模式实例

    Redis 是目前 NoSQL 领域的当红炸子鸡,它象一把瑞士军刀,小巧.锋利.实用,特别适合解决一些使用传统关系数据库难以解决的问题.打算写一系列 Redis 使用模式的文章,深入总结介绍 Redis 常见的使用模式,以供大家参考. 常见汇总计数器 汇总计数是系统常见功能,比如网站通常需要统计注册用户数,网站总浏览次数等等. 使用 Redis 提供的基本数据类型就能实现汇总计数器,通过 incr 命令实现增加操作. 比如注册用户数,基本操作命令如下: 复制代码 代码如下: # 获取注册用户数

  • PHP基于redis计数器类定义与用法示例

    本文实例讲述了PHP基于redis计数器类定义与用法.分享给大家供大家参考,具体如下: Redis是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API. 这里使用其incr(自增),get(获取),delete(清除)方法来实现计数器类. 1.Redis计数器类代码及演示实例 RedisCounter.class.php <?php /** * PHP基于Redis计数器类 * Date: 2017-10-28 * Aut

  • Spring之借助Redis设计一个简单访问计数器的示例

    为什么要做一个访问计数?之前的个人博客用得是卜算子做站点访问计数,用起来挺好,但出现较多次的响应很慢,再其次就是个人博客实在是访问太少,数据不好看

  • C++设计一个简单内存池的全过程

    什么是内存池??? 通常我们用new或malloc来分配内存的话,由于申请的大小不确定,所以当频繁的使用时会造成内存碎片和效率的降低.为了克服这种问题我们提出了内存池的概念.内存池是一种内存分配方式.内存池的优点就是可以有效的减少内存碎片化,分配内存更快速,减少内存泄漏等优点. 内存池是在真正使用内存之前,先申请分配一个大的内存块留作备用.当真正需要使用内存的时候,就从内存池中分配一块内存使用,当使这块用完了之后再还给内存池.若是内存块不够了就向内存再申请一块大的内存块. 可以看出这样做有两个好

  • js实现一个简单的MVVM框架示例

    以前都是默默地看园子里的文章,猥琐的点赞,今天也分享一下自己用js实现的一个简单mvvm框架. 最初只做了自动绑定事件,后面又参考学习了vue,knouckout以及argular实现方式,以及结合自己做WPF的一些经验,增加了属性绑定,今天又稍微整理了下,完善了部分功能,把代码提交到了码云:https://gitee.com/zlj_fy/Simple-MVVM 先简单介绍下用法: <form class="form-horizontal" role="form&qu

  • python实现一个简单RPC框架的示例

    本文需要一点Python socket基础. 回顾RPC 客户端(Client):服务调用方. 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端. 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理. 服务端(Server):服务的真正提供者. Network Service:底层传输,可以是 TCP 或 HTTP. 实现jsonrpc 在实现前,简单理一下整体思路

  • 用java的spring实现一个简单的IOC容器示例代码

    要想深入的理解IOC的技术原理,没有什么能比的上我们自己实现它.这次我们一起实现一个简单IOC容器.让大家更容易理解Spring IOC的基本原理. 这里会涉及到一些java反射的知识,如果有不了解的,可以自己去找些资料看看. 注意 在上一篇文章,我说,启动IOC容器时,Spring会将xml文件里面配置的bean扫描并实例化,其实这种说法不太准确,所以我在这里更正一下,xml文件里面配置的非单利模式的bean,会在第一次调用的时候被初始化,而不是启动容器的时候初始化.但是我们这次要做的例子是容

  • 利用jQuery设计一个简单的web音乐播放器的实例分享

    一.准备数据库 首先,我们设计MYSQL数据库如下: CREATE TABLE `songs` ( `song_id` int(11) NOT NULL AUTO_INCREMENT, `url` varchar(500) NOT NULL, `artist` varchar(250) NOT NULL, `title` varchar(250) NOT NULL, PRIMARY KEY (`song_id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AU

  • 使用JavaScript制作一个简单的计数器的方法

    设计思想 该方法的关键是Cookie技术和动态图像特性的综合运用.使用Cookie,可以在用户端的硬盘上记录用户的数据,下次访问此站点时,即可读取用户端硬盘的Cookie,直接得知来访者的身份和访问次数等有关信息.JavaScript中通过document.cookie属性访问Cookie,这个属性包括名字.失效日期.有效域名.有效URL路径等.用等号分开的名字和其值是Cookie的实际数据,本例中用来存储该访问者访问该页面的次数.通过把Web页中的图像映射到一个Images数组,一定条件下修改

  • 利用PHP实现一个简单的用户登记表示例

    一.展示单行的用户信息表 首先我们需要创建两个文件,一个为html文件,一个为php文件. 这里博主就创建了form.html和formHandle.php两个文件. 下面展示一下两个文件的内容: form.html的文件内容: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body>

  • javascript如何用递归写一个简单的树形结构示例

    现在有一个数据,需要你渲染出对应的列表出来: var data = [ {"id":1}, {"id":2}, {"id":3}, {"id":4}, ]; var str="<ul>"; data.forEach(function(v,i){ str+="<li><span>"+v.id+"</span></li>&

  • Angular6 写一个简单的Select组件示例

    Select组件目录结构 /src /app /select /select.ts /select.html /select.css /options.ts /index.ts //select.ts import { Component, ContentChildren, ViewChild, Input, Output, EventEmitter, QueryList, HostListener } from '@angular/core'; import { NzOptionDirecti

随机推荐