1. 签到系统的核心挑战与设计思路
最近在技术社区看到一个高频面试题:如何设计一个高并发、低存储的签到系统?这确实是个经典场景——想象一下双十一期间电商平台的每日签到,或者在线教育平台的连续打卡功能,动辄百万级用户同时操作,传统数据库方案根本扛不住。我在实际项目中用Redis的BitMap结构完美解决了这个问题,今天就把整套方案拆解给你看。
为什么说这是个典型的高并发场景?首先签到行为具有明显的时间聚集性(比如早高峰时段),其次这个操作需要保证原子性(不能重复签到),最后数据量极大但单个数据体积很小(只需要记录是否签到)。传统方案用MySQL记录用户ID和签到日期,百万用户一个月就能产生3000万条记录,不仅存储压力大,查询统计更是灾难。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis BitMap的底层原理
2.1 位图数据结构解析
BitMap本质上是String类型的二进制操作扩展,每个bit位表示一个状态。比如我们用偏移量代表日期,bit值0/1表示未签到/已签到。一个用户一年的签到数据只需要365bit≈46字节,比传统方案节省98%以上存储空间。
Redis的SETBIT命令时间复杂度是O(1),这意味着无论数据量多大,单次签到操作都能在恒定时间内完成。统计操作通过BITCOUNT实现,其底层使用查表法和SWAR算法优化,实测百万级数据统计仅需3-5ms。
2.2 内存优化对比实验
我们做过对比测试:存储100万用户30天的签到记录
- MySQL方案:约1.2GB存储空间
- Redis普通KV存储:约480MB
- BitMap方案:仅14.3MB
在阿里云8核16G的Redis实例上,BitMap方案的QPS轻松突破10万,而MySQL在5000QPS时就已经出现明显延迟。
3. 详细实现方案
3.1 键值设计规范
bash复制# 用户月度签到键
user:sign:{userId}:{yyyyMM}
# 示例:用户10086在2023年7月的签到记录
SETBIT user:sign:10086:202307 15 1 # 7月16日签到(偏移量从0开始)
这里有几个关键设计点:
- 按月分键避免单个键过大(超过512MB会报错)
- 偏移量=日期-1,方便直接映射日历
- 键名包含业务前缀防止冲突
3.2 核心操作命令
bash复制# 签到(7月20日)
SETBIT user:sign:10086:202307 19 1
# 检查某天是否签到
GETBIT user:sign:10086:202307 19
# 统计当月签到次数
BITCOUNT user:sign:10086:202307
# 获取连续签到天数(需配合脚本实现)
3.3 连续签到统计脚本
这是面试官最爱问的难点,通过BITFIELD命令和位运算实现:
lua复制-- KEYS[1] 键名
-- ARGV[1] 当前日期偏移量
local result = {}
for i=tonumber(ARGV[1]),0,-1 do
if redis.call('GETBIT', KEYS[1], i) == 0 then
break
end
table.insert(result, 1, i+1)
end
return result
4. 生产环境优化策略
4.1 冷热数据分离
- 热数据:最近3个月的签到记录保留在Redis
- 冷数据:历史数据转储到MySQL,键名保持不变
- 查询时先查Redis,未命中再查MySQL(加缓存标记)
4.2 大Key预防方案
当用户量激增时要注意:
- 单个键值不超过1MB(约8万用户日活)
- 超过阈值时按用户ID分片,如:
bash复制user:sign:{userId%10}:202307 # 分10片 - 定期执行BITCOUNT巡检,监控大Key增长
4.3 缓存穿透防护
针对恶意查询不存在的用户数据:
bash复制# 布隆过滤器初始化
BF.RESERVE sign_users 0.001 1000000
# 用户注册时添加标记
BF.ADD sign_users 10086
# 查询前先过滤
BF.EXISTS sign_users 10086
5. 踩坑实录与性能调优
5.1 位溢出问题
某次线上事故:用户在美国西部时间23:50签到,由于服务器在东八区,实际记录到了第二天。解决方案:
python复制# 时区处理示例
import pytz
from datetime import datetime
def get_offset(user_timezone):
utc_now = datetime.utcnow()
user_tz = pytz.timezone(user_timezone)
return (user_tz.localize(utc_now).day - 1)
5.2 内存碎片优化
长期运行的BitMap会产生内存碎片,建议:
- 每月初对新键执行
MEMORY PURGE - 设置
activedefrag yes自动整理 - 监控
mem_fragmentation_ratio指标
5.3 集群环境注意事项
在Redis Cluster模式下:
- 同一个键必须落在同一个slot
- 使用hash tag确保数据分布:
bash复制user:{userId}:sign:202307 # 只对{}内的userId做hash - 跨slot操作需要客户端聚合结果
6. 扩展应用场景
这个方案稍加改造就能支持:
- 用户行为标记(是否完成新手引导)
- 特征开关系统(AB测试分组)
- 实时DAU统计(日活去重)
- 安全风控(异常登录检测)
比如实现周活跃用户统计:
bash复制# 周一至周日分别设置bit位
SETBIT weekly_active 0 1 # 周一活跃
SETBIT weekly_active 6 1 # 周日活跃
# 统计周活跃天数
BITCOUNT weekly_active
实际项目中,我们配合Pipeline批量操作将签到接口的RT控制在2ms以内,GC次数下降90%。有个反常识的发现:当value长度≤8字节时,Redis的String类型比BitMap更省内存,所以对于低频业务(如年度签到)反而建议用String+位运算方案。
