用户签到
1.1 BitMap的用法
1.1.1 背景
实现签到功能,如果我们存到数据库中,数据库表应如下

假设有1000万用户,平均每人每年签到次数为10次,则这张表一年的数据量为1亿条
一个人,每签到一次需要使用 (8+8+1+1+3+1) 共22字节的内存,一个月则最多需要600多字节
成本太大!!
我们按月来统计用户签到信息,签到记录为1,未签到记录为0,使用Redis的BitMap来实现

BitMap
BitMap 是一种利用 Bit(位)来存储数据状态的数据结构,通过 0 或 1 来表示某个元素“是否存在”或“某种状态”。
常见应用场景
快速去重与排序:如果你有 10 亿个不重复的正整数,想知道某个数是否存在,用常规 Hash 映射会占用数 GB 内存。使用 BitMap 只需要约 125MB($10^9 / 8$ 字节)。
用户签到/活跃统计
签到: 每一位代表一天,1 表示已签到。一个月只需 31 位(不到 4 字节)。
日活: 每个用户对应一个 BitMap 中的位,登录则置为 1。通过
AND或OR运算,可以瞬间算出“连续三天活跃的用户”或“今日总活跃人数”。
布隆过滤器 (Bloom Filter):布隆过滤器的底层就是 BitMap。它通过多个 Hash 函数将一个元素映射到 BitMap 的多个点上,用于快速判断“某样东西一定不存在”或“可能存在”。
BitMap 命令速查

操作分类 命令 功能描述 复杂度 典型示例 写入 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 接口文档

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 签到统计
需求

代码实现
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); }