当前位置: 首页 > news >正文

【黑马点评日记】:用户签到功能详解——从Bitmap入门到避坑指南

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:苍穹外卖日记,SSM框架深入,JavaWeb
命运的结局尽可永在,不屈的挑战却不可须臾或缺!

前言:

接着前面的内容,我们继续对黑马点评项目进行完善。

摘要:

本文详细介绍了黑马点评项目中用户签到功能的Redis位图实现方案。针对传统MySQL方案存在的数据量爆炸和内存浪费问题,提出使用Redis位图存储签到状态,仅需4字节/用户/月。文章重点剖析了签到功能实现中的五大关键点:Key设计策略、偏移量索引、空指针处理、位运算逻辑和无符号右移操作,并提供了完整的签到与统计连续签到天数的代码实现。通过对比MySQL和Redis方案的优缺点,强调了架构设计中的权衡思维,为高并发场景下的签到功能提供了高效解决方案。


一、 踩坑点:为什么签到功能不简单

前面我们刚刚学完SpringBoot+MySQL,可能会想:“签到不就是往数据库插一条记录,有什么难度。如果我们仅仅是这么想,我们就没有考虑到实际情况所带来的性能陷阱。

假设我们设计一张tb_sign表,包含user_id,year,month,date字段。

  • 数据量爆炸:如果黑马点评有1000万用户,每人每月签到10次,一年就是1亿条数据。

  • 内存浪费:签到其实只有是/否两种状态,用一条表记录(约22字节)来存一个是,太浪费了。

难点一:如何在极其节省内存的前提下,记录海量用户的签到状态


二、 解决方案:Redis Bitmap(位图)

既然只有“是/否”两个状态,为什么不用1个比特位来表示呢

核心思想
把一个月(最多31天)想象成一条由31个格子组成的纸条,每天签到了就在格子里涂黑(1),没签到就留白(0)。

  • 空间计算:一个用户一个月占用31 bit4字节

  • 对比:MySQL存一条记录要22字节,Bitmap只要4字节。内存占用降低约80%。


三、 Bitmap 核心命令速览

在写代码前,我们得先知道Redis中的这四个命令,否则代码会看不懂:

  1. SETBIT:将某一位设为1。

  2. GETBIT:获取某一位的值。

  3. BITFIELD:一次获取一段长度的位图数据(这是统计连续签到的关键)。

  4. BITCOUNT:统计有多少个1(总签到天数)。

四、 业务实现:签到与统计

我们将分两步走:点一下(签到)和看一眼(统计连续天数)。

1. 功能一:用户签到

业务流程
用户点击签到 -> 后端获取当前用户ID和日期 -> 计算偏移量 -> 将Redis中的对应位设为1。

代码详解与坑点

java @Override public Result sign() { // 1. 获取当前登录用户 Long userId = UserHolder.getUser().getId(); // 2. 获取当前日期时间 LocalDateTime now = LocalDateTime.now(); // === 坑点1:Key的设计策略 === // 千万不能只用一个Key存所有用户,也不能不分月份。 // 标准格式:sign:用户ID:年月 // 好处:方便按月清理/统计,防止Key过大。 String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); String key = USER_SIGN_KEY + userId + keySuffix; // 3. 获取今天是本月的第几天 // 注意:1号是第1天,但在位图中offset是从0开始的 int dayOfMonth = now.getDayOfMonth(); // === 坑点2:偏移量的索引 === // 如果今天是1号,dayOfMonth=1,但位图第0位才代表1号。 // 所以必须 dayOfMonth - 1 int offset = dayOfMonth - 1; // 4. 写入Redis // true 表示签到(即设置为1),false表示未签到(0) stringRedisTemplate.opsForValue().setBit(key, offset, true); return Result.ok(); }
2. 功能二:统计连续签到天数

这是最难的部分,也是面试最爱问的。需求是:计算当前用户截止今天,连续签到了几天?

核心逻辑
从今天(最后一位)往前数,遇到第一个0就停。

技术难点
如何一次性拿到本月1号到今天的所有签到数据?不能循环调用30次GETBIT,那样效率太差。

解决方案BITFIELD命令。

  • 命令:BITFIELD key GET u[天数] 0

  • 解释:u表示无符号整数。它会返回一个十进制数字,这个数字的二进制表示就是我们这N天的签到情况。

代码详解与避坑

java @Override public Result signCount() { Long userId = UserHolder.getUser().getId(); LocalDateTime now = LocalDateTime.now(); String key = USER_SIGN_KEY + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM")); int dayOfMonth = now.getDayOfMonth(); // 1. 获取本月的签到十进制数 // 这里指定了 unsigned(dayOfMonth),意思是取从0到dayOfMonth位的值 List<Long> result = stringRedisTemplate.opsForValue().bitField( key, BitFieldSubCommands.create() .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)) .valueAt(0) ); // === 坑点3:空指针与默认值 === // 如果用户从来没签过到,result可能为null或空集合,此时直接返回0 if (result == null || result.isEmpty()) { return Result.ok(0); } Long num = result.get(0); if (num == null || num == 0) { return Result.ok(0); } // 2. 核心算法:位运算计数 // 此时 num 是一个十进制数,比如 3 (二进制 11) 代表前两天都签到了。 int count = 0; while (true) { // === 坑点4:与运算逻辑 === // (num & 1) 是取出二进制最右边的一位 // 如果是 0,说明这天没签到,中断循环 if ((num & 1) == 0) { break; } else { count++; } // === 坑点5:无符号右移 === // 必须使用 >>> 而不是 >> // 因为 >> 会在左边补符号位(如果是负数补1),导致死循环。 // >>> 无论正负,左边都补0,保证数据正确归零。 num >>>= 1; } return Result.ok(count); }

五、 深度避坑点总结

  1. 月份变化Key中必须包含年月(yyyyMM)。如果不包含月份,跨月时位图会乱套(2月的28号和3月的1号会冲突)。

  2. 索引对齐:Redis位图的offset是从0开始的,而现实中日期是从1开始的。dayOfMonth - 1这步绝对不能省。

  3. 右移运算符:在统计连续天数时,一定要用>>>(无符号右移)。如果用>>(有符号右移),一旦数字是负数,高位会一直补1,导致num永远不等于0,程序死循环

  4. BITFIELD 的返回类型unsigned一定要用对。如果用有符号signed,当最高位为1时,数字会变成负数,破坏后面的位运算逻辑。

六、 追问

问:如果要统计年度连续签到奖励(比如连续7天送积分),该怎么改造

答:

  1. 连续7天检测:可以在签到成功后(或使用定时任务),利用BITFIELD获取最近7天的数据组成二进制串。判断1111111(十进制127)是否等于该串。

  2. 跨月连续:这是难点。比如 9月30日 和 10月1日 连续。我的思路是:当查询今天(如10月1日)连续签到时,如果发现10月1日签到了但10月之前的位断了,就递归/循环去查上个月的位图,看上个月月底是否是签满的,直到遇到未签到的那一天。

七、 整体回顾

通过这个签到功能,我们希望帮助你建立一种意识:架构设计就是做选择题

  • MySQL:逻辑简单,但面对海量数据,IO压力大,资源浪费严重。

  • Redis Bitmap:逻辑稍复杂(需要位运算),但内存占用极低(一个用户一个月仅占4字节),适合高并发场景。

结语:

如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!

http://www.jsqmd.com/news/780865/

相关文章:

  • SDQM:合成数据质量评估框架解析与实践
  • 从 repo-ready 看项目环境自动化配置:提升开发效率的工程实践
  • 从零构建多功能Discord机器人:技术架构、核心模块与实战部署
  • 2026年口碑好的芜湖全包装修公司/芜湖毛坯房装修公司/装修公司/芜湖二手房翻新装修公司TOP排行榜 - 品牌宣传支持者
  • 六自由度灵巧手机械特性与混合力控策略解析
  • 大语言模型特征导向方法解析与应用实践
  • 基于AI的抖音自动回复系统:架构、部署与高阶运营实战
  • BentoML与OpenLLM:标准化部署开源大模型的生产级实践
  • 保姆级教程:在Windows上用QT Creator 6.5.2调用USBCAN-II+库(附完整源码)
  • 避开创新点陷阱:手把手教你用CPO算法做自己的第一个SCI创新实验(附完整Matlab对比代码)
  • 多模态检索技术:MetaEmbed架构与工业实践
  • 开发者如何构建个人编码计划管理工具:从设计到部署全栈实践
  • AI智能体防幻觉与目标漂移:七项心智锚点实践指南
  • 深度分析 DeepSeek API 计费规则如何优化长文本输入降低成本
  • Arm CoreLink MHU-320AE架构与通信协议深度解析
  • AdamW与Muon优化器在FFN中的谱崩溃对比研究
  • AI自动生成单元测试:原理、实践与最佳应用指南
  • 多模态大语言模型在视频推理中的高效优化实践
  • 本地运行MusicGPT:基于Rust与MusicGen的AI音乐生成工具实践
  • FET-OR电源切换技术:高效低损耗的双电源管理方案
  • GenAI与LLM发展时间线:从业者的知识图谱与趋势洞察工具
  • Agent Lightning:无侵入式AI智能体强化学习训练框架实战指南
  • 基于LLamaworkspace的LLM应用开发:从RAG原理到私有知识库实战
  • STM32 LL库实战:手把手教你用SysTick写一个精准的微秒延时函数(附CubeMX配置避坑点)
  • ARM SIMD指令集:VADD与VBIC深度解析与优化实践
  • Transformer中LayerNorm位置对模型性能的影响分析
  • MCP安全审计实战:用mcp-audit守护AI助手配置安全
  • 基于多智能体系统的自动化任务管理:从LLM到工作流引擎的工程实践
  • 别再死记硬背PBR公式了!从光到颜色的物理基础,彻底搞懂渲染为啥要这么算
  • Arm Neoverse V3AE核心RAS寄存器架构与错误处理机制详解