Skip to content

用户签到

1.1 BitMap的用法

1.1.1 背景

  1. 实现签到功能,如果我们存到数据库中,数据库表应如下

    image-20260422093235407

    假设有1000万用户,平均每人每年签到次数为10次,则这张表一年的数据量为1亿条

    一个人,每签到一次需要使用 (8+8+1+1+3+1) 共22字节的内存,一个月则最多需要600多字节

    成本太大!!

  2. 我们按月来统计用户签到信息,签到记录为1,未签到记录为0,使用Redis的BitMap来实现

    image-20260422093722615

  3. BitMap

    • BitMap 是一种利用 Bit(位)来存储数据状态的数据结构,通过 0 或 1 来表示某个元素“是否存在”或“某种状态”。

    • 常见应用场景

      • 快速去重与排序:如果你有 10 亿个不重复的正整数,想知道某个数是否存在,用常规 Hash 映射会占用数 GB 内存。使用 BitMap 只需要约 125MB($10^9 / 8$ 字节)。

      • 用户签到/活跃统计

        • 签到: 每一位代表一天,1 表示已签到。一个月只需 31 位(不到 4 字节)。

        • 日活: 每个用户对应一个 BitMap 中的位,登录则置为 1。通过 ANDOR 运算,可以瞬间算出“连续三天活跃的用户”或“今日总活跃人数”。

      • 布隆过滤器 (Bloom Filter):布隆过滤器的底层就是 BitMap。它通过多个 Hash 函数将一个元素映射到 BitMap 的多个点上,用于快速判断“某样东西一定不存在”或“可能存在”。

  4. BitMap 命令速查

    image-20260422094918040

    操作分类命令功能描述复杂度典型示例
    写入SETBIT key offset value在指定偏移量(offset)设置位的值(0或1)$O(1)$SETBIT login:20260421 10086 1 (用户10086签到)
    读取GETBIT key offset获取指定偏移量上的位值$O(1)$GETBIT login:20260421 10086 (查询是否签到)
    统计BITCOUNT key [start end]统计位图中值为 1 的数量$O(N)$BITCOUNT login:20260421 (计算今日总签到人数)
    复合计算BITOP operation destkey key...对多个 BitMap 做位运算(AND, OR, NOT, XOR)$O(N)$BITOP AND result key1 key2 (计算连续两天活跃的用户)
    检索BITPOS key bit [start end]查找第一个出现的 0 或 1 的位置$O(N)$BITPOS login:20260421 1 (查找今天第一个签到的用户ID)

1.2 签到功能

1.2.1 接口文档

image-20260423085602414

1.2.2 代码实现

java
    @Override
    public Result sign() {
        // 1. 获取当前用户
        UserDTO dto = UserHolder.getUser();
        // 2. 判断用户是否存在
        if (dto == null) {
            return Result.fail("请先登录!");
        }
        // 3. 存在进行签到
        // 键 : sign:用户id:年月
        String key = RedisConstants.USER_SIGN_KEY + dto.getId() + ":" + LocalDateTime.now().format(SystemConstants.DATE_FORMATTER_YYYYMM);
        Boolean success = stringRedisTemplate.opsForValue().setBit(key, LocalDateTime.now().getDayOfMonth() - 1, true);
        // 4. 返回结果
        if (Boolean.FALSE.equals(success)) {
            return Result.fail("签到失败!");
        }
        return Result.ok();
    }

1.3 签到统计

  1. 需求

    image-20260423104854315

  2. 代码实现

    java
        @Override
        public Result signCount() {
            // 1. 获取当前用户
            UserDTO dto = UserHolder.getUser();
            // 2. 判断用户是否登录
            if (dto == null) {
                return Result.fail("请先登录!");
            }
            // 3. 获取该用户的签到数据
            String key = RedisConstants.USER_SIGN_KEY + dto.getId() + ":" + LocalDateTime.now().format(SystemConstants.DATE_FORMATTER_YYYYMM);
            // 3.1 获取本月截止到今天的所有签到记录
            // 这里返回的是一个十进制数字,表示当前月的签到情况
            List<Long> bitField = stringRedisTemplate.opsForValue().bitField(
                    key,
                    // 从0开始,获取今天是本月的第几天,就获取多少位签到记录
                    BitFieldSubCommands.create()
                            .get(BitFieldSubCommands.BitFieldType.unsigned(LocalDateTime.now().getDayOfMonth())).valueAt(0)
            );
            // 4. 对数据进行处理得到本月的连续签到天数
            // 获取所有连续签到天数,比较得到最大连续签到天数
            if (bitField == null || bitField.isEmpty()) {
                return Result.ok(0);
            }
            Long signSituation = bitField.get(0);
            if (signSituation == null ||  signSituation == 0) {
                return Result.ok(0);
            }
            // 从今天的位置开始向前统计,直到遇到第一次未签到为止,为一个连续签到天数
            int nowCount = 0;
            // 如果为0,说明未签到,结束
            // 如果为1,说明已签到,连续签到天数加1,继续统计下一位
            while ((signSituation & 1) != 0) {
                // 4.1 将该数字与1进行与运算,得到最后一位的签到情况
                // 4.2 判断是否为0
                nowCount++;
                // 4.3 对该数字进行右移操作,继续统计下一位
                signSituation >>>= 1;
            }
            return Result.ok(nowCount);
        }