redis--黑马点评--用户签到模块详解
用户签到
假如我们使用一张表来存储用户签到信息,其结构应该如下:
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层:
@PostMapping("/sign")public Result sign(){return userService.sign();}
Service层:
@Overridepublic Result sign() {//1.获取当前登录用户Long id = UserHolder.getUser().getId();//2.获取当前日期LocalDateTime now = LocalDateTime.now();//3.拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyy/MM"));String key = USER_SIGN_KEY + id + keySuffix;//4.获取今天是本月的第几天int dayOfMouth = now.getDayOfMonth();//5.写入Redis,setbit key offset 1stringRedisTemplate.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层:
@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.拼接keyString 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 0List<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位是否为0if ((number & 1) == 0) {//如果为0,说明未签到,结束break;}else {//如果不为0,说明已签到,计数器加一count++;}//把数字右移一位,抛弃最后一个bit位,继续下一个bit位的判断// 将number无符号右移一位,相当于将number除以2,并将结果赋值给numbernumber >>>= 1;}return Result.ok(count);}
效果展示:
至此用户签到功能完成
希望对大家有所帮助