Redis-BitMap实现签到功能

发布于:2025-02-10 ⋅ 阅读:(39) ⋅ 点赞:(0)

为什么需要 bitmap

痛点:在 项目开发中经常会用到签到的功能,如果用户有量很少,签到信息直接存储在数据库中还是很合理的,但是随着用户量的增加,100 万用户,就算一个用户一年签到 20 次就已经 2000 万数据了,增长的非常快,并且查询的时候效率也低。

为了解决这个问题可以用 Bitmap 的数据结构。

用 1 来表示签到,用 0 来表示没有签到。

对于一个月来说,从第一个开始签到。一个月最多 31 天,用 31bit 来表示用户签到。一个月只需要两个字节。

一个用户一个月签到的信息也就只有一条,这样大大减少了数据库的压力。

布隆过滤器底层也是 bitmap。 在 redis 中使用 string 来实现 bitmap。最大存储上线 512,最大时 2 的 32 次方比特位。

BitMap 的操作

功能分析:

对于用户签到数据,如果直接采用数据库存储,当出现高并发访问时,对数据库压力会很大,例如双十一签到活动。这时候应该采用缓存,以减轻数据库的压力,Redis是高性能的内存数据库,适用于这样的场景。

另外如果系统的用户量很多,每次签到都插入一条记录,那么数据库表增长就很快。如果系统两百万用户,一个月平均签到十次,那么就是两千万数据量,mysql 一张表大概也就是两千万。

签到出来的数据

setbit  bm1  0 1  在第0个位置赋值为1

这个图中可以看出来是第 1、2、7 天签到的。

判断第二天是否签到,getbit 如果等于 1 说明用户在这一天完成了签到功能;

get bm1 1  //判断是否等于1

bitpos 判断开始签到的位置

实现签到的代码 存储在 redis 中的格式

sign:userID:202401,这样每一个用户都是一个 key.

@Override
public Result sign() {
    // 1.获取当前登录用户
    Long userId = UserHolder.getUser().getId();
    // 2.获取日期
    LocalDateTime now = LocalDateTime.now(); //获取当前的时间
    // 3.拼接key
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); //当前月
    String key = USER_SIGN_KEY + userId + keySuffix;
    // 4.获取今天是本月的第几天
    int dayOfMonth = now.getDayOfMonth(); //当前天数
    // 5.写入Redis SETBIT key offset 1
    stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
    return Result.ok();
}

连续签到次数


* 1. 用户签到 * 2. 检查用户是否签到 * 3. 获取当月签到次数 * 4. 获取当月连续签到次数

创建用户的签到数据库表:

功能实现

1. 用户签到实现

@Override
public void signIn() {
    // 获取当前登录的用户Id
    Long userId = SecurityUtils.getUserId();
    //获取日期
    LocalDateTime now = LocalDateTime.now();
    // 获取当前的月份
    String keySuffix=now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    //拼接
    String   key= SignRedisConstant.SIGN_KEY + userId+keySuffix;
    int dayOfMonth = now.getDayOfMonth();
    redisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
}

还需要做异步任务,将数据同步到数据库中去。

待做:用定时任务或者异步操作更新数据库中的签到信息。

2. 检查用户是否签到

将获取 key 的方法封装起来

public class RedisUtil {

    public static String getSign() {
        Long userId = SecurityUtils.getUserId();
        LocalDateTime now = LocalDateTime.now();
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        return SignRedisConstant.SIGN_KEY + userId + keySuffix;
    }
    public static LocalDateTime getNow() {
        return  LocalDateTime.now();
    }
}
@Override
public boolean isSignIn() {
    String keySuffix = RedisUtil.getSign();
    int dayOfMonth = RedisUtil.getNow().getDayOfMonth();
    return redisTemplate.opsForValue().getBit(keySuffix, dayOfMonth - 1);
}

3. 获取当月签到次数

@Override
public int getCurrentMonth() {
    String keySuffix = RedisUtil.getSign();
    String str = (String) redisTemplate.opsForValue().get(keySuffix);
    int count = 0;
    for (int i = 0; i < str.length(); i++) {
        if (str.charAt(i) == '1') {
            count++;
        }
    }
    return count;
}

4. 获取当月连续签到次数

从最后一个开始一直找到第一个为 0 的地方,和 1 进行与操作就是拿到最后一个数字的。

@Override
public int getContinuousSignInCount() {
    String keySuffix = RedisUtil.getSign();
    int dayOfMonth = RedisUtil.getNow().getDayOfMonth();
    List<Long> result = redisTemplate.opsForValue().bitField(keySuffix, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
    if (result == null || result.size() == 0) {
        return 0;
    }
    Long num = result.get(0);
    if (num == null || num == 0) {
        return 0;
    }
    int count = 0;
    //主要是这一段的逻辑
    while (true) {
        if ((num & 1) == 0) {
            break;
        } else {
            count++;
        }
        num = num >> 1;
    }
    return count;
}


网站公告

今日签到

点亮在社区的每一天
去签到