用户签到
约 996 字大约 3 分钟
2025-07-11
11.1 BitMap 功能演示
如果通过 MySQL 数据库记录每次签到,在用户量大且签到频繁的情况下,会产生大量数据。例如,1000 万用户,每人每年签到 10 次,则一年会产生 1 亿条数据,占用大量存储空间。
为了简化存储,可以采用位图(BitMap)的方案。位图的核心思想是按月统计用户签到信息,将签到记录为 1,未签到记录为 0,每一个 bit 位对应当月的每一天。这种方式可以用极小的空间表示大量数据。
Redis 中使用 string 类型数据结构实现 BitMap,最大上限是 512MB,即 2^32 个 bit 位。 这意味着一个 Redis 的 BitMap 可以表示 2^32 个签到状态。
BitMap 的常用操作命令包括:
命令 | 描述 |
---|---|
SETBIT | 向指定位置(offset)存入一个 0 或 1。 |
GETBIT | 获取指定位置(offset)的 bit 值。 |
BITCOUNT | 统计 BitMap 中值为 1 的 bit 位的数量。 |
BITFIELD | 操作(查询、修改、自增)BitMap 中 bit 数组中的指定位置(offset)的值。 |
BITFIELD_RO | 获取 BitMap 中 bit 数组,并以十进制形式返回。 |
BITOP | 将多个 BitMap 的结果做位运算(与、或、异或)。 |
BITPOS | 查找 bit 数组中指定范围内第一个 0 或 1 出现的位置。 |
11.2 实现签到功能
为了高效地记录用户的每日签到信息,可以采用基于 Redis 的位图(BitMap)存储方案。具体实现方式如下:
- Key 的设计: 以年份和月份作为 bitmap 的 key,例如
sign:用户ID:202310
表示用户在 2023 年 10 月份的签到记录。 - Bit 位的含义: Bitmap 中的每一位代表当月的一天。例如,第一位(offset 为 0)代表 1 号,第二位(offset 为 1)代表 2 号,以此类推。
- 签到操作: 当用户进行签到时,将对应日期的 bit 位设置为 1。
- 判断是否签到: 检查对应日期的 bit 位是否为 1,如果是 1 则表示已签到,否则表示未签到。
控制器
/**
* 用户签到
* @return
*/
@PostMapping("/sign")
public Result sign() {
return userService.sign();
}
实现类
/**
* 用户签到
* @return
*/
@Override
public Result sign() {
// 1. 获取用户
Long userId = UserHolder.getUser().getId();
// 2. 获取日期
LocalDateTime now = LocalDateTime.now();
// 3. 写入 Redis
String key = USER_SIGN_KEY + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
stringRedisTemplate.opsForValue().setBit(key, now.getDayOfMonth() - 1, true);
return Result.ok();
}
11.3 签到统计
在实现签到功能后,通常还需要进行签到统计,例如统计连续签到天数。连续签到天数是指从最后一次签到开始向前统计,直到遇到第一次未签到为止,计算总的签到次数。
由于 BITFIELD
返回的数据是十进制,,因此需要将其转换为二进制格式进行遍历,以确定每一天的签到状态。
- 与运算(
&
): 将十进制数字与1
进行与运算,可以判断二进制的最后一位是否为1
。 如果结果为1
,则表示当天已签到;如果结果为0
,则表示当天未签到。 - 右移运算(
>>
): 将十进制数字右移一位,相当于将二进制数的所有位向右移动一位。 通过不断右移,可以遍历到数字的每一个 bit 位,从而判断每一天的签到状态。
控制器
/**
* 签到统计
* @return
*/
@GetMapping("/sign/count")
public Result signCount() {
return userService.signCount();
}
实现类
/**
* 签到统计
* @return
*/
@Override
public Result signCount() {
// 1. 获取当前登陆用户
Long userId = UserHolder.getUser().getId();
// 2. 获取日期
LocalDateTime now = LocalDateTime.now();
// 3. 获取签到记录
String key = USER_SIGN_KEY + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
int dayOfMonth = now.getDayOfMonth();
List<Long> result = stringRedisTemplate.opsForValue().bitField(key,
BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
);
if (result == null || result.isEmpty()) {
// 查询失败
return Result.ok(0);
}
Long record = result.get(0);
if (record == null || record == 0) {
// 没有签到记录
return Result.ok(0);
}
// 4. 统计连续签到天数
int count = 0;
while (record >= 1) {
if ((record & 1) == 1) {
count++;
} else {
break;
}
record >>= 1;
}
return Result.ok(count);
}