场景四:实时热搜榜 / 在线人数统计
全网用户都在给同一个计数器 +1,又都在盯着同一个榜单看——写和读的洪峰砸向同一小撮数据。这是"热 Key"问题的原产地,也是"用一点点误差换百倍性能"的最佳课堂。
场景描述与业务特征
微博热搜、抖音在线观众数、B站实时弹幕热度榜、电商"实时销量 TOP100"……这些场景的共同点是:数据在两端同时爆炸——亿万用户的行为都在往少数几个统计对象上砸(写热点),同时所有人又都在读同一份榜单(读热点)。
好消息是:这类数据几乎都不要求精确。热搜第 3 名的热度是 812 万还是 809 万,没有任何人在意;直播间在线人数显示 "10.2万" 还是 "10.3万",主播都一样开心。"误差换性能"是这个场景的合法货币。
🎯 写热点集中
百万用户的点击都落在同一个话题的计数器上——天然的单点热 Key,分库分表都救不了(它就一个 Key)。
👀 读热点同样集中
所有人打开 App 都拉同一份 TOP50 榜单,读 QPS 十万起步,但内容全网一致——缓存命中率理论上可到 100%。
⏱️ 滑动窗口语义
热搜说的是"最近1小时的热度",窗口一直在滑动:新事件进来、旧事件过期出去,统计要"边进边出"。
🎲 容忍近似
UV 统计、在线人数、热度分——1% 的误差没人察觉,却可以让内存占用从 GB 降到 KB(概率数据结构的魔法)。
🚀 突发性极强
爆炸性新闻出现时,某个 Key 的写入量秒级暴涨千倍,且完全无法预测是哪个 Key。
🧹 反作弊需求
热搜是流量入口,刷榜产业链盯着它——去重(同一用户只算一次)和异常检测是刚需。
整体运作流程
核心链路是"本地攒 → 分片聚 → 定时算 → 缓存读"四步:写入端先在应用本地把 1000 次 +1 攒成一次 +1000,再分片写入计数层;榜单不实时算,而是定时(如每 10 秒)汇总生成一份"成品",所有读请求只读这份成品缓存。
核心瓶颈与数据一致性挑战
🔥 热 Key:分片架构的天敌
Redis 集群靠"不同 Key 分散到不同节点"来扩容,但热搜计数器就是同一个 Key——所有流量都砸向同一个节点,集群再大也只有一台在干活。唯一出路:把一个 Key 人为拆成 N 个子 Key。
🐘 大 Key:在线人数的隐形炸弹
用 Set 存"直播间在线用户ID",千万人在线 = 单 Key 几百 MB。读一次阻塞主线程几秒、主从同步卡死。解法:HyperLogLog(12KB 数固定)或分桶 Set。
⏳ 窗口精度 vs 成本
"最近1小时"的精确滑动窗口要给每个事件记时间戳,内存爆炸。常用折中:把 1 小时切成 60 个"分钟桶",窗口滑动时整桶进出——精度损失最多 1 分钟,内存降千倍。
👥 UV 去重的代价
"1 亿次点击来自多少人"需要记住每个访客——精确记录(Set/Bitmap)内存与用户量成正比。HyperLogLog 用 12KB 估算 1 亿 UV,误差仅 0.81%,代价是不能反查"某人来过没"。
优化手段、获益与代价
① 本地预聚合(攒批上报) 写优化
每台应用服务器在内存里累加计数,每秒(或每千条)批量上报一次到 Redis/Kafka。
② 热 Key 拆分(分片计数) 核心
把 count:topic1024 拆成 10 个子 Key(后缀 s0~s9),写入随机挑一个 INCR,读取时把 10 个加起来。
③ HyperLogLog / Bitmap 概率去重 UV统计
UV、在线人数用 HyperLogLog(12KB 固定内存,误差 0.81%);需要精确或反查的场景用 Bitmap(1 亿用户 ≈ 12MB)。
④ 时间分桶滑动窗口 窗口
"最近1小时"切成 60 个分钟桶(每桶一个 Key,TTL 略大于窗口),统计时累加最近 60 个桶。
⑤ 榜单快照 + 多级缓存 读优化
定时任务生成 TOP-N 快照(JSON 成品),写入 Redis 多副本 + 应用本地缓存(1~5秒过期),CDN 可再兜一层。
多种解决方案对比与选型建议
| 方案 | 写吞吐 | 精确度 | 窗口能力 | 适用场景 |
|---|---|---|---|---|
| 数据库 COUNT/GROUP BY 每次现查现算 | 低 | 精确 | SQL 灵活但慢 | 后台报表、低频查询 |
| Redis INCR + ZSet 计数器+有序集合排行 | 中 · 单Key瓶颈 | 精确 | 配合分桶可实现 | 中小规模实时榜单的主力方案 |
| Redis 分片计数 + 快照 热Key拆分 · 本页主推 | 高 · 可横向扩 | 精确(延迟秒级) | 分桶实现 | 大流量热搜/销量榜标准答案 |
| HyperLogLog / Bitmap 概率结构去重 | 高 | HLL约0.8%误差 | 按桶合并 | UV/在线人数等去重统计 |
| Kafka + Flink 流计算 事件时间滑动窗口 | 极高 | 精确+乱序容忍 | 原生最强 | 复杂窗口/趋势检测/反作弊 |
推演检验:换个条件还成立吗?
背下架构图不算学会,能判断"条件变了方案还成不成立"才算。先自己推,再展开参考。
推演 1:如果法务要求 UV 统计必须精确(一个都不能差),HyperLogLog 还能用吗?整套"误差换性能"的思路要推翻吗?
💡 参考推演
不推翻,但要分层:对外大屏、运营看板继续用 HLL(0.8% 误差没人在乎);结算、审计这条链路单独走精确通道——Bitmap(用户 ID 连续时内存可控)或离线批处理精确去重。误差换性能的前提永远是"业务能容忍",一旦某条链路不容忍,就为它单开精确通道,而不是全局退回精确方案陪葬性能。
推演 2:如果榜单从"全站一个"变成"每个城市一个"(300 个城市榜),热 Key 问题是解决了还是转移了?
💡 参考推演
被稀释但没消失:北上广深的城市榜依然是热 Key,而三四线城市的榜单流量很低——继续给所有城市统一上"分片计数+快照"反而浪费。正确姿势是按流量分级:头部城市保留完整三件套,长尾城市退化成单 ZSet 直读。同一个业务里,不同实例可以配不同档位的方案,架构不必一刀切。
推演 3:如果运营要求"违规词条删除后榜单必须秒级消失",而快照是分钟级刷新的,怎么办?
💡 参考推演
快照是拉模式(定时重算),天然做不到秒级剔除。解法不是把快照周期压到秒级(成本爆炸),而是加一条推模式补丁通道:删词事件直接写入黑名单,读接口返回前先过滤黑名单,下个快照周期自然收敛。这就是"最终一致 + 实时补丁"的组合拳——为 1% 的紧急需求单开小门,别推翻 99% 场景够用的主架构。