在用户量达到亿级规模的互联网产品中,每日登录统计和连续签到功能看似简单,实则暗藏玄机。我曾负责过一个日活1.2亿的社交平台用户系统改造,最初用传统关系型数据库记录这些数据时,每天光是签到记录就产生近20GB数据,内存消耗更是让服务器频频告警。
这类业务有三个典型特征:高频写入(用户登录/签到时集中操作)、低频读取(主要在查询统计时使用)、数据可丢失(个别记录缺失不影响整体统计)。传统方案用MySQL等关系型数据库直接存储每条记录,就像用集装箱运沙子——资源浪费严重。我们需要更精细化的存储策略。
用位图存储登录状态是最经典的节省内存方案。假设我们为每个用户分配一个唯一数字ID,那么:
对于1亿用户,单个bitmap仅需:100,000,000 bits ≈ 12MB内存。相比传统方案(每条记录至少64字节)节省了99%以上空间。
Redis的BITMAP类型原生支持这种结构,以下是用Redis实现的示例:
bash复制# 用户ID 123456 在20230801登录
SETBIT login:20230801 123456 1
# 统计当天登录用户数
BITCOUNT login:20230801
对于只需要近似统计的场景,Bloom Filter和HyperLogLog是更节省空间的方案:
bash复制# HyperLogLog 示例
PFADD login:20230801 123456 789012
PFCOUNT login:20230801
连续签到需要记录用户最近一次签到日期和当前连续天数。采用"日期+计数器"的紧凑存储:
bash复制# 用户签到数据结构
HMSET user:123456 last_checkin 20230801 streak 5
# 签到逻辑伪代码
if (今天 - last_checkin == 1天) {
streak++
} else if (今天 != last_checkin) {
streak = 1
}
update last_checkin
我们最终采用的混合架构:
code复制┌─────────────┐ ┌─────────────┐
│ 实时计算层 │ │ 存储层 │
│ (Redis集群) │ │ (冷数据归档)│
└─────────────┘ └─────────────┘
分片策略:按用户ID范围分片,例如:
bash复制# 用户ID前2位作为分片键
SETBIT login:{shard}:20230801 {user_id} 1
过期策略:设置自动过期避免堆积
bash复制EXPIRE login:20230801 2592000 # 30天后过期
内存编码:Redis会自动选择最优编码方式,但可以手动干预:
bash复制CONFIG SET hash-max-ziplist-entries 512
采用双写+校验机制:
我们在1.2亿用户的生产环境进行了AB测试:
| 方案 | 内存占用 | QPS | 误差率 |
|---|---|---|---|
| 传统MySQL | 320GB | 1,200 | 0% |
| Redis Bitmap | 1.8GB | 85,000 | 0% |
| HyperLogLog | 144KB | 92,000 | 0.81% |
| 分层存储(我们的方案) | 4.2GB | 78,000 | 0% |
位图稀疏性问题:当用户ID不连续时,Roaring Bitmap比普通Bitmap更省空间
大Key风险:单个Bitmap过大时会导致Redis阻塞,建议:
KEYS命令,改用SCAN遍历缓存穿透防护:
bash复制# 对不存在的日期初始化空Bitmap
EXISTS login:20230801 ||
SETBIT login:20230801 0 0
EXPIRE login:20230801 86400
数据迁移技巧:从旧系统迁移时:
python复制# 批量转换旧数据到Bitmap
for user in mysql.query("SELECT id FROM users"):
redis.setbit('login:20230801', user.id, 1)
# 使用pipeline提升效率
pipe = redis.pipeline()
for i in range(1000000):
pipe.setbit('login:20230801', i, 1)
pipe.execute()
这套方案上线后,我们的服务器内存消耗从420GB降至28GB,每日登录统计的响应时间从平均780ms降到12ms。最关键的是,当用户量从1.2亿增长到2亿时,内存增长几乎可以忽略不计——这才是真正面向亿级用户的架构设计。