写在开头
昨天帮一位粉丝复盘美团二面,他说自己最委屈的是倒在了“Redis 海量数据统计”上。他在现场和面试官经历了这样一段“死亡对话”:
面试官推了推眼镜:“我们现在的 App 有 2 亿注册用户,大促期间预计有 1 亿用户登录。如何设计 Redis 存储方案来统计今天的日活(DAU),并支持快速判断某个用户是否登录?”
老哥心里暗喜(这题我会啊): “这简单,用Set 集合!用户登录一个SADD一个,利用 Set 的去重特性,最后SCARD一下就是日活。”
面试官冷笑了一声: “那你知道存 1 亿个用户 ID(假设是 Long 类型),Set 需要消耗多少内存吗?Redis 单线程处理这么大的 BigKey,扩容和持久化时会发生什么?”
老哥当场懵了,支支吾吾答不上来。
这一刻他才明白,面试官考的根本不是 API 怎么调,而是你对“底层成本”怎么算。
在小数据量下 Set 确实方便,但到了亿级流量,这简直是生产事故。
今天咱们就把这道题拆透,用“计算逻辑”和“大厂架构视角”告诉你,为什么顶级架构师在这类场景下都不单纯用 Set。
第一步:算“笨办法”成本,揭露 Set 的致命陷阱
咱们先别急着写代码,先拿出计算器,算算朋友口中那个“简单”的 Set 方案。
假设用户 ID 是连续的 Long 类型,占 8 字节。
你以为的内存:1 亿 × 8 字节 = 800 MB?错!大错特错!
真实的 Redis 内存:Redis 的 Set 底层存储不仅存数据,还有
dictEntry、robj等元数据开销,加上哈希表的负载因子导致的预分配。
在 64 位操作系统下,存储一个 Long 类型的字符串或数字,Set 内部每个元素平均大概需要50 - 64 字节的空间。
真实的账是这么算的:
结论:仅仅为了统计一天的日活,就要吃掉6.4G 内存!
但这还不是最致命的。最致命的是你制造了一个核弹级的 BigKey:
BigKey 阻塞:这是一个 6G 的巨型 Key,删除或过期时,主线程直接卡死;
RDB 风险:生成快照时,fork 子进程复制页表,内存瞬间翻倍,甚至导致 OOM;
网络风暴:主从切换时,传输这个 6G 的 Key 会把网卡瞬间打满。
所以,Set 方案 = 生产事故。
第二步:算“位图”优势,空间压缩 250 倍
看着面试官失望的眼神,这时候你必须祭出 Bitmap(位图)。
针对这种“只有 0 和 1 两种状态”(登录/未登录)的场景,Bitmap 是物理上的极致。它的底层是 String,但可以按 bit 操作。我们将用户 ID 作为 bit 的偏移量(offset),1 表示登录,0 表示未登录。
关键计算逻辑:
Bitmap 的大小不取决于“有多少人登录”,而取决于“最大的 UserID 是多少”。
题目中有 2 亿注册用户,假设 UserID 是连续的(1 ~ 200,000,000)。
对比冲击:
Set 方案:6.4 GB
Bitmap 方案:~24 MB
差距高达 250 倍以上!哪怕你要统计一整年的日活,Bitmap 加起来也不过 8GB 左右,完全在可控范围内。
第三步:生产环境的“进阶坑”,别忘分片
面试到这一步,如果你只答出 Bitmap,只能算“合格”。要拿“S card”,你得指出 Bitmap 的隐患:
单个 Bitmap 还是 BigKey 怎么办?
虽然 24MB 不算超大,但在高并发下,对同一个 Key 的频繁 SETBIT 操作(尤其是 Cluster 模式)会导致请求全部打向某一个分片(Slot),造成单点过热。
解法:分片(Sharding)。
将用户 ID 模 1000,拆分成 1000 个小的 Bitmap:
Key = dau:20251215:001
...
Key = dau:20251215:999
这样既打散了流量,又避免了单个 Key 过大。
注意:统计总数时,需要在应用层并发查询这 1000 个 Key 的 BITCOUNT 然后累加。
第四步:架构师视角(大厂是如何做日活统计的?)
如果面试官继续追问:“如果我的 ID 不是连续的数字,或者是雪花算法(Snowflake)生成的 19 位 ID 怎么办?老板想看北京地区的日活怎么办?”
这时候,Redis Bitmap 就不是银弹了。大厂通常采用“组合拳”:
1. 稀疏 ID 场景 -> Roaring Bitmap
痛点:如果 ID 是 19 位的,直接用 Redis Bitmap 会申请一段巨大的内存(PB级),中间全是 0,极度浪费。
解法:使用Roaring Bitmap(压缩位图)。它在 Java 内存或大数据引擎中使用,对于稀疏数据,它使用数组存储;对于稠密数据,自动转换为位图。既省空间,又保留了位运算的高效。
2. 允许误差的大盘数据 -> HyperLogLog
痛点:几千个业务线都要统计日活,内存还是不够用。
解法:Redis HyperLogLog。无论日活是 1 万还是 10 亿,固定只占 12KB 内存。虽然有 0.81% 的误差,但对于实时大盘展示完全够用。
3. 多维度 BI 分析 -> ClickHouse
痛点:老板要看“北京地区、使用 iPhone、版本 v2.0 的日活”。
解法:Redis 做不到多维交叉。这时候数据要清洗入库ClickHouse或Doris,利用它们内置的
bitmap函数进行 SQL 级的多维聚合分析。
面试标准答案模板:这样说,技术总监会觉得你是“自己人”
把上面的推导串起来,这就是可以直接输出的“专家级”回答:
“面试官您好,这道题不能直接拍脑袋选数据结构,需要结合数据基数、ID 特征和业务场景来分层设计。
在 C 端实时场景(如判断用户是否登录):
我会排除 Set 方案,因为它有 6.4GB 的内存开销和 BigKey 风险。
我会首选 Redis Bitmap。对于 2 亿用户,内存仅需 24MB。为了防止热点,我会按 UserID 取模进行 分片 处理。
针对特殊 ID 场景(如雪花 ID):
如果 ID 极其稀疏,Redis Bitmap 会造成空间浪费。我会引入 Roaring Bitmap 算法,或者在 Redis 外部维护一套‘UserID -> 自增 ID’的映射表。
在 B 端分析场景(如多维统计):
如果业务需要统计‘北京地区+安卓’的日活,Redis 很难胜任。我会建议将数据落入 ClickHouse,利用其内置的 Bitmap 类型进行多维交叉分析。
总结就是:实时查状态用 Redis 分片 Bitmap,多维分析用 ClickHouse,大盘估算用 HyperLogLog。”
写在最后
Redis 面试题从来不是考你“API 熟不熟”,而是考你“对资源的掌控力”。
Set是逻辑上的集合。
Bitmap是物理上的极致。
Roaring Bitmap和ClickHouse是架构上的宽深。
遇到海量数据,先拿出计算器算内存,这才是资深开发该有的本能。
https://mp.weixin.qq.com/s/jjIG8AkM3rN8YJHMKvnwqQ