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

用户签到

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

sql 复制代码
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

查看redis客户端:

查询

BITFIELD

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

举例说明:

BITPOS

签到功能

案例实现:签到功能

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

接口请求解析:

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

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

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

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

代码实现:

controller层:

java 复制代码
 @PostMapping("/sign")
 public Result sign(){
     return userService.sign();
 }

Service层:

java 复制代码
 @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();
 }

运行效果:

至此签到功能完成。

签到统计

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

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

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

那么如何使用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层:

java 复制代码
 @GetMapping("/sign/count")
 public Result signCount(){
     return userService.signCount();
 }

Service层:

java 复制代码
 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);
 }

效果展示:

至此用户签到功能完成

希望对大家有所帮助

相关推荐
jackiehome9 分钟前
SQL数据库无法操作,日志文件损坏修复
数据库·sql·oracle
我是伪码农17 分钟前
HTML和CSS复习
前端·css·html
林恒smileZAZ18 分钟前
前端实现进度条
前端
前端老石人21 分钟前
邂逅前端开发:从基础到实践的全景指南
开发语言·前端·html
荒川之神24 分钟前
ORACLE导入导出实验
数据库·oracle
执笔为剑27 分钟前
利用逻辑备份修复误操作的库
数据库·kingbase
阿珊和她的猫31 分钟前
以用户为中心的前端性能指标解析
前端·javascript·css
木心术131 分钟前
OpenClaw网页前端开发与优化全流程指南
前端·人工智能
Amumu1213831 分钟前
HTML5的新特性
前端·html·html5
SeSs IZED37 分钟前
【Nginx 】Nginx 部署前端 vue 项目
前端·vue.js·nginx