redis--黑马点评--用户签到模块详解

发布于:2025-08-09 ⋅ 阅读:(17) ⋅ 点赞:(0)

用户签到

假如我们使用一张表来存储用户签到信息,其结构应该如下:

CREATE TABLE `tb_sign` (
     `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
   `user_id` bigint unsigned NOT NULL COMMENT '用户id',
   `year` year NOT NULL COMMENT '签到的年',
   `month` tinyint NOT NULL COMMENT '签到的月',
   `date` date NOT NULL COMMENT '签到的日期',
   `is_backup` tinyint unsigned DEFAULT NULL COMMENT '是否补签',
   PRIMARY KEY (`id`) USING BTREE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT

假设有1000万用户,平均每人每年签到次数为10次,那么这张表一年的数据量为1亿条。还是保守估计,因此,用数据库表来存储太过浪费内存空间。

并且每一个用户签到一次需要使用(8+8+1+1+3+1)共22字节的内存,并且没有包括隐藏字段,一个月最多需要600多字节。

因此这种方式既耗内存,数据库压力还大。

那有没有比较好的方法呢?

我们按照月来统计用户签到信息,签到记录为1,未签到记录为0,这样我们只需要最多31bit就可以表示一个用户一个月的签到情况,非常节省空间,这种做法的核心思想就是把每一个比特位对应当月的每一天,形成了映射关系,用0和1表示业务状态。

这种思路就叫做位图BitMap)。

而在redis底层是利用String类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是2^32个bit位。

BitMap用法

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出现的位置

命令演示:

添加

setbit:签到则为1,不签到可以不输入,默认为0

image-20250806220447565

查看redis客户端:

image-20250806220545220

查询

image-20250806220836773

BITFIELD

image-20250806221103566

在查询时 offset指定从哪读,type指定读多少bit位,并且还要指定返回的是否带符号。(因为返回的是十进制,因此要说明是否带符号,如果带符号,二进制第一位则为符号位,因此u代表无符号,i代表有符号,一般使用无符号)

举例说明:

image-20250806221721635

BITPOS

image-20250806221953803

签到功能

案例实现:签到功能

需求:实现签到接口,将当前用户当天签到信息保存到redis中

接口请求解析:

说明
请求方式 Post
请求路径 /user/login
请求参数
返回值

在请求解析中,我们发现请求参数与返回值都为空,这是因为我们签到所需的用户以及当天日期都可以在后端直接获取,因此不需要前端传参,也不需要返回值,但如果是补签功能的话,就需要前端传递日期参数了

注意:因为BitMap底层是基于String数据结构,因此其操作也都被封装在字符串相关操作中了。

key组成:用户+日期(原因:签到往往是以月为统计单位的,因此每个用户每个月的签到情况放在一个BitMap中,方便统计)

代码实现:

controller层:

 @PostMapping("/sign")
 public Result sign(){
     return userService.sign();
 }

Service层:

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

运行效果:

image-20250806224553581

image-20250806231348363

至此签到功能完成。

签到统计

签到统计有很多种:比如统计该月总签到次数、该月截止今天的连续签到次数等等,

那么什么叫做连续签到天数呢?

从最后一次签到开始向前统计,直到遇到第一次未签到为止,计算的总的签到次数,就是连续签到天数。

那么如何使用Java代码实现统计连续签到天数?

方法1:给每个bit位拼接逗号,然后spit(0),最后一个数组长度就是连续天数,最长的数组就是最长连续天数

方法2:从最后一个比特位开始遍历,并定义一个计数器,为1则加一,为0则终止。其中有些关键问题:

问题1:如何得到本月到今天为止的所有签到数据?

在BitMap的指令中:bitfield可以获取指定范围内的所有签到数据,而该指令需要两个参数,一个是从哪开始,另一个是查多少。因为要得到本月到今天为止的所有签到数据,因此起始脚标为0,而offset则为日期值,

由此得到指令:bitfield key get u[dayOfMonth] 0

问题2:如何从后往前的遍历每一个bit位

解答:与1做与运算,就能得到最后一个比特位。随后在右移一位,下一个bit位就成为了最后一个bit位,随后同上操作,以此类推,便可以从后向前的遍历每一个bit位。

至此,思路理顺,付诸实践

案例展示:实现签到统计功能

需求:实现下面接口,统计当前用户截止当前时间在本月的连续签到天数

请求解析:

说明
请求方式 GET
请求路径 /user/sign/out
请求参数
返回值 连续签到天数

代码实现:

Controller层:

 @GetMapping("/sign/count")
 public Result signCount(){
     return userService.signCount();
 }

Service层:

 public Result signCount() {
     //1.获取当前登录用户
     Long id  = UserHolder.getUser().getId();
     //2.获取当前日期
     LocalDateTime now = LocalDateTime.now();
     //3.拼接key
     String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyy/MM"));
     String key = USER_SIGN_KEY + id + keySuffix;
     //4.获取今天是本月的第几天
     int dayOfMouth = now.getDayOfMonth();
     //5.获取本月截止今天为止的所有的签到记录 返回的是一个十进制的数字 bitfield sign:1:2025/08 get u6 0
     List<Long> result = stringRedisTemplate.opsForValue().bitField(
             key,
             BitFieldSubCommands.
                     create().
                     get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMouth)).
                     valueAt(0)
     );
     if (result == null || result.isEmpty()){
         return Result.ok(0);
     }
     Long number = result.get(0);
     if (number == null || number == 0){
         return Result.ok(0);
     }
     //6.循环遍历
     int count = 0;
     while (true){
         //6.1让这个数字与1做与运算,得到数字的最后一个bit位  //判断bit位是否为0
         if ((number & 1) == 0) {
             //如果为0,说明未签到,结束
             break;
         }else {
             //如果不为0,说明已签到,计数器加一
             count++;
         }
     //把数字右移一位,抛弃最后一个bit位,继续下一个bit位的判断
     // 将number无符号右移一位,相当于将number除以2,并将结果赋值给number
     number >>>= 1;
     }
     return Result.ok(count);
 }

效果展示:

image-20250807212259015

至此用户签到功能完成

希望对大家有所帮助


网站公告

今日签到

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