Reids的使用 | 社区项目

261 阅读6分钟

简介

一个SpringBoot的社区项目。

redis使用场景:网站数据统计、缓存用户信息、验证码、登录凭证、点赞、关注。

redis配置类

@Configuration
public class RedisConfig {

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

        // 设置key的序列化方式
        template.setKeySerializer(RedisSerializer.string());
        // 设置value的序列化方式
        template.setValueSerializer(RedisSerializer.json());
        // 设置hash的key的序列化方式
        template.setHashKeySerializer(RedisSerializer.string());
        // 设置hash的value的序列化方式
        template.setHashValueSerializer(RedisSerializer.json());

        template.afterPropertiesSet();//使上面这些template的设置生效
        return template;
    }
}
  1. 自定义 RedisTemplate 进行序列化

  2. 使用 StringRedisTemplate 进行序列化

RedisTemplate 的两种序列化方式_华仔仔coding的博客-CSDN博客_redistemplate 序列化

功能描述:初始化RedisTemplate的一些参数设置

使用场景:主要是在RedisTemplate初始化时进行调用,如果不执行此方法,可能会报一些莫名其妙的错误,那应该就是部分参数没有初始化造成的。

细说一下RedisTemplate的使用方法(一) - 掘金 (juejin.cn)

网站数据统计

UV(Unique Visitor)

网站的独立访客,将访问网站的IP地址去重后统计数量,使用redis中的HyperLogLog记录。

HyperLogLog是用来做基数统计的算法,它提供不精确的去重计数方案(这个不精确并不是非常不精确),标准误差是0.81%

每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同的基数

HyperLogLog只能统计基数的大小(也就是数据集的大小,集合的个数),他不能存储元素的本身,不能向set集合那样存储元素本身,也就是说无法返回元素。

HyperLogLog 使用及其算法原理详细讲解_李子捌的博客-CSDN博客_hyperloglog

1. 写拦截器

DataInterceptor实现HandlerInterceptor接口,重写preHandle方法。

preHandle:预处理,在业务处理器处理请求之前被调用,可以进行登录拦截,编码处理、安全控制、权限校验等处理;

SpringBoot之HandlerInterceptor拦截器的使用详解_java_脚本之家 (jb51.net)

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 记录UV
        String ip = request.getRemoteHost();
        dataService.recordUV(ip);

        return true;
    }

以当前日期为key,将访问网站的IP地址添加到HyperLogLog中。

private SimpleDateFormat df=new SimpleDateFormat("yyyyMMdd");

// 将指定的IP计入UV
public void recordUV(String ip) {
    String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
    redisTemplate.opsForHyperLogLog().add(redisKey, ip);
}

Redis Pfadd 命令_添加指定元素到 HyperLogLog 中。


单日的UV的key为uv:date

// 单日UV
public static String getUVKey(String date) {
    return PREFIX_UV + SPLIT + date;
}

2. 配置拦截器

实现WebMvcConfigurer接口。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private DataInterceptor dataInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(dataInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }

}

静态文件不需要被拦截,使用excludePathPatterns()排除。

3. 统计指定日期范围内的UV

public long calculateUV(Date start, Date end) {
    if (start == null || end == null) {
        throw new IllegalArgumentException("参数不能为空!");
    }

    // 整理该日期范围内的key
    List<String> keyList = new ArrayList<>();
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(start);
    while (!calendar.getTime().after(end)) {
        String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));
        keyList.add(key);
        calendar.add(Calendar.DATE, 1);
    }

    // 合并这些数据
    String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));
    redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());

    // 返回统计的结果
    return redisTemplate.opsForHyperLogLog().size(redisKey);
}

Redis Pgmerge 命令_将多个 HyperLogLog 合并为一个 HyperLogLog

Redis Pfcount 命令_返回给定 HyperLogLog 的基数估算值。


区间的UV的key为uv:startDate:endDate

// 区间UV
public static String getUVKey(String startDate, String endDate) {
    return PREFIX_UV + SPLIT + startDate + SPLIT + endDate;
}

DAU(Daily Active User)

网站日活跃用户,用户只要在一天内访问过网站,就是活跃用户,使用redis中的Bitmap记录。

Bitmap实际上就是String类型,通过最小的单位bit来进行0或者1的设置,表示某个元素对应的值或者状态。 一个bit的值,或者是0,或者是1

redis 字符串最大值为512M,所以bigmap最大值为:4294967295

redis bitmap简介_可能是喝了假酒的博客-CSDN博客_bitmap redis

1. 写拦截器

统计UV的时候我们已经写过拦截器DataInterceptor了,而且配置了拦截器,现在我们只需要在preHandle方法中加入记录DAU的代码。

只有登录过的用户才需要被DAU统计。

// 记录DAU
User user = hostHolder.getUser();
if (user != null) {
    dataService.recordDAU(user.getId());
}

以当前日期为key,将登录用户的id设置为true。

// 将指定用户计入DAU
public void recordDAU(int userId) {
    String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));
    redisTemplate.opsForValue().setBit(redisKey, userId, true);
}

Redis Setbit 命令_对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。


单日DAU的key为dau:date

// 单日活跃用户
public static String getDAUKey(String date) {
    return PREFIX_DAU + SPLIT + date;
}

2. 统计指定日期范围内的DAU

只要用户在给定的日期范围内有一天达到了活跃用户的标准,就算是活跃用户。

public long calculateDAU(Date start, Date end) {
    if (start == null || end == null) {
        throw new IllegalArgumentException("参数不能为空!");
    }

    // 整理该日期范围内的key
    List<byte[]> keyList = new ArrayList<>();
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(start);
    while (!calendar.getTime().after(end)) {
        String key = RedisKeyUtil.getDAUKey(df.format(calendar.getTime()));
        keyList.add(key.getBytes());
        calendar.add(Calendar.DATE, 1);
    }

    // 进行OR运算
    return (long) redisTemplate.execute(new RedisCallback() {
        @Override
        public Object doInRedis(RedisConnection connection) throws DataAccessException {
            String redisKey = RedisKeyUtil.getDAUKey(df.format(start), df.format(end));
            connection.bitOp(RedisStringCommands.BitOperation.OR, redisKey.getBytes(), keyList.toArray(new byte[0][0]));
            return connection.bitCount(redisKey.getBytes());
        }
    });
}

execute() 需要 RedisConnection 对象,通过 RedisConnection 操作 Redis

使用 RedisTemplate excute()与opsFor_bingguang1993的博客-CSDN博客_redistemplate.execute()的用法

BITOP — Redis 命令参考 (redisfans.com)

BITCOUNT — Redis 命令参考 (redisfans.com)


区间DAU的key为dau:startDate:endDate

// 区间活跃用户
public static String getDAUKey(String startDate, String endDate) {
    return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate;
}

缓存用户信息

使用redis临时存储用户的信息。

重构UserService中的getUser(int id)方法,不直接从DB中查询,而是从redis中查询。

如果从redis中查询不到用户信息,就从DB中查询数据,并且存储到redis中,这样下次就能直接从redis中查询到用户数据了。

    public User getUser(int id) {
//        return userDao.getUser(id);
        User user = getCache(id);
        if(user==null){
            user = initCache(id);
        }
        return user;
    }

getCache

优先从缓存中取数据。

private User getCache(int userId){
    String key = RedisKeyUtil.getUserKey(userId);
    return (User) redisTemplate.opsForValue().get(key);
}

用户信息在redis中的key为user:userId

public static String getUserKey(int userId) {
    return PREFIX_USER + SPLIT + userId;
}

initCache

缓存取不到时,从DB中获取数据,并初始化缓存数据。注意redis中序列化的问题。

private User initCache(int userId) {
    User user = userDao.getUser(userId);
    String redisKey = RedisKeyUtil.getUserKey(userId);
    redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);//设置过期时间为1小时
    return user;
}

缓存在redis中的用户信息不需要一直保存,设置1小时过期时间,过期后缓存数据从redis中自动清除。

clearCache

数据变更时清除缓存数据,eg:用户更换头像时,用户激活成功时。

private void clearCache(int userId) {
    String redisKey = RedisKeyUtil.getUserKey(userId);
    redisTemplate.delete(redisKey);
}

验证码

以前的验证码存储在session中,在分布式系统中存在session共享问题。

验证码不需要永久存储,在redis中存储可以方便的对key的过期时间进行设置。

1. 生成验证码

我们使用kaptcha生成验证码,使用springboot整合kaptcha。

String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);

Kaptcha图片验证码工具_BUG弄潮儿的技术博客_51CTO博客

2. 存储验证码的归属

通过UUID生存随机字符串kaptchaOwner,用于将用户与验证码联系起来。

kaptchaOwner存入cookie中。

String kaptchaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60);//设置cookie过期时间为60s
cookie.setPath(contextPath);
response.addCookie(cookie);

3. 验证码存入redis

注意下kaptchaOwner

String key = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(key, text, 60, TimeUnit.SECONDS);//失效时间60s

redis中key为kaptcha:kaptchaOwner,value为验证码。

public static String getKaptchaKey(String owner) {
    return PREFIX_KAPTCHA + SPLIT + owner;
}

4. 返回验证码图片给浏览器

5. 校验验证码

  1. 从cookie中拿到kaptchaOwner,根据kaptchaOwner从reids中拿到正确的验证码。

  2. 用正确的验证码与用户输入的验证码进行对比。

登录凭证

处理每次请求时,都要查询用户的登录凭证,访问的频率非常高。

  • 登陆时将登录凭证存入redis中,ticket通过cookie存在浏览器中

  • 退出时将redis中登录凭证的状态改为删除状态

    • 取出登录凭证
    • 修改状态
    • 存入reids
  • 通过cookie获取ticket,通过ticket从reids中获取登录凭证,登录凭证中有用户信息。

点赞&关注

  • 点赞使用set
  • 关注使用sorted set