1. 项目背景与挑战
2019年夏天,我们团队接手了一个看似简单的需求:统计一款国民级App的日活跃用户数(DAU)。当时这个数字已经突破2亿,而原有的统计系统正面临严峻的内存压力。
最初的方案非常简单粗暴——使用Redis的Set结构存储每日活跃用户ID。每个用户ID占用约16字节(8字节的long型ID+8字节的Redis内部结构开销),2亿用户意味着:
code复制200,000,000 × 16B ≈ 3.2GB
这还只是单日数据!当我们需要保留7天滚动数据时,内存占用直接飙升至22GB以上。更糟的是,随着业务增长,这个数字还在以每月15%的速度递增。
关键问题:这种线性增长的内存消耗不仅导致服务器成本飙升,更严重影响了Redis集群的稳定性和查询性能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与方案设计
2.1 数据结构对比分析
我们对比了三种主流方案:
| 方案 | 数据结构 | 内存估算 | 优点 | 缺点 |
|---|---|---|---|---|
| 原始方案 | Set | 3.2GB/天 | 实现简单 | 内存占用高 |
| 方案A | HyperLogLog | ~12KB | 极小内存 | 只能计数,无法识别具体用户 |
| 方案B | Bitmap | ~24MB | 精确统计 | 需要用户ID映射 |
HyperLogLog虽然内存占用极低,但无法满足我们需要识别具体用户的需求(如新老用户分析)。最终选择了Bitmap方案,其核心思想是:
- 预先分配足够长的bit数组(2^32位≈512MB)
- 将用户ID哈希映射到特定bit位
- 用bit值1/0表示当日是否活跃
2.2 用户ID映射优化
原始用户ID是64位长整型,直接用作bit偏移量不现实。我们设计了二级映射:
java复制// 第一级:用户ID分片
int shard = userId % 1024;
// 第二级:分片内偏移量
long offset = userId / 1024;
这样每个分片最大支持2^42个用户(足够未来十年增长),而实际内存占用仅需:
code复制2^32 bits ÷ 8 ÷ 1024 ÷ 1024 = 512MB(总) ÷ 1024 ≈ 512KB/分片
3. 架构升级实施
3.1 分片集群设计
为避免单个Bitmap过大,我们采用分片集群架构:
- 用1024个Redis实例组成集群
- 每个实例负责一个分片
- 通过CRC32哈希决定数据路由
bash复制# Redis配置示例
redis-cli -p 6379 SETBIT shard_0 123456 1 # 设置第0分片第123456位
redis-cli -p 6379 BITCOUNT shard_0 # 统计该分片活跃数
3.2 冷热数据分离
- 热数据:最近3天的Bitmap保留在内存
- 冷数据:超过3天的转存到SSD(使用Redis的RDB持久化)
- 历史数据:每周合并后归档到HBase
4. 性能优化实战
4.1 内存压缩技巧
Redis的Bitmap实际是字符串实现,我们通过以下手段进一步压缩:
- 定期压缩:每天凌晨对bitmap执行
BITOP OR操作合并相邻位 - 稀疏位优化:当某分片活跃用户<1%时,改用Run-Length Encoding压缩
- 共享字典:多个业务线共用同一套用户ID映射表
4.2 查询加速方案
针对高频查询场景(如实时大屏),我们增加了:
- 预聚合层:每分钟异步计算各分片的BITCOUNT结果
- 多级缓存:
- L1:本地Guava缓存(1秒过期)
- L2:Redis缓存(1分钟过期)
- L3:持久化聚合结果
5. 效果验证与异常处理
5.1 内存对比
| 指标 | 原方案 | 新方案 | 优化比 |
|---|---|---|---|
| 单日内存 | 3.2GB | 24MB | 133:1 |
| 7天内存 | 22.4GB | 168MB | 133:1 |
| 查询延迟 | 200-500ms | <50ms | 4-10倍 |
5.2 踩坑记录
-
哈希冲突问题:
- 现象:某天DAU突然异常下降15%
- 原因:用户ID哈希碰撞导致覆盖写入
- 解决:引入二次哈希(MurmurHash3)并添加冲突检测
-
热点分片问题:
- 现象:某个分片QPS是其他的100倍
- 原因:某KOL用户粉丝集中在该分片
- 解决:动态调整哈希算法,对头部用户特殊处理
6. 扩展应用场景
这套架构后来被复用到多个业务场景:
- 留存率计算:通过
BITOP AND计算连续活跃用户 - AB测试分组:用不同bit位标识实验组/对照组
- 反作弊系统:识别异常活跃设备集群
一个特别有意思的案例:我们将用户地理位置信息编码到bitmap中,仅用50MB内存就实现了全国地级市维度的活跃用户分布统计。
经验分享:在最近一次大促中,这套系统成功支撑了单日4.2亿DAU的统计需求,而内存占用仍控制在300MB以内。实践证明,通过合理的架构设计,海量数据统计完全可以做到既精确又高效。
