场景四 · 热搜榜 / 在线统计
高并发场景 · Scenario 04

场景四:实时热搜榜 / 在线人数统计

全网用户都在给同一个计数器 +1,又都在盯着同一个榜单看——写和读的洪峰砸向同一小撮数据。这是"热 Key"问题的原产地,也是"用一点点误差换百倍性能"的最佳课堂。

读写双高 热 Key 滑动窗口 概率算法
百万/秒计数事件写入峰值
10万+榜单读取 QPS
分钟级热搜更新周期
~0.8%可接受的统计误差
📑 本页目录
01

场景描述与业务特征

微博热搜、抖音在线观众数、B站实时弹幕热度榜、电商"实时销量 TOP100"……这些场景的共同点是:数据在两端同时爆炸——亿万用户的行为都在往少数几个统计对象上砸(写热点),同时所有人又都在读同一份榜单(读热点)。

好消息是:这类数据几乎都不要求精确。热搜第 3 名的热度是 812 万还是 809 万,没有任何人在意;直播间在线人数显示 "10.2万" 还是 "10.3万",主播都一样开心。"误差换性能"是这个场景的合法货币。

🎯 写热点集中

百万用户的点击都落在同一个话题的计数器上——天然的单点热 Key,分库分表都救不了(它就一个 Key)。

👀 读热点同样集中

所有人打开 App 都拉同一份 TOP50 榜单,读 QPS 十万起步,但内容全网一致——缓存命中率理论上可到 100%。

⏱️ 滑动窗口语义

热搜说的是"最近1小时的热度",窗口一直在滑动:新事件进来、旧事件过期出去,统计要"边进边出"。

🎲 容忍近似

UV 统计、在线人数、热度分——1% 的误差没人察觉,却可以让内存占用从 GB 降到 KB(概率数据结构的魔法)。

🚀 突发性极强

爆炸性新闻出现时,某个 Key 的写入量秒级暴涨千倍,且完全无法预测是哪个 Key。

🧹 反作弊需求

热搜是流量入口,刷榜产业链盯着它——去重(同一用户只算一次)和异常检测是刚需。

02

整体运作流程

核心链路是"本地攒 → 分片聚 → 定时算 → 缓存读"四步:写入端先在应用本地把 1000 次 +1 攒成一次 +1000,再分片写入计数层;榜单不实时算,而是定时(如每 10 秒)汇总生成一份"成品",所有读请求只读这份成品缓存。

🌊 行为事件 点击/搜索/播放 百万事件/秒 ① 本地预聚合 应用内存攒批 1秒 1000次+1 → 1次+1000 写量直降 3 个数量级 ② 分片计数层 热Key拆10个子Key topic:1024:s0~s9 分散到多个Redis分片 ③ 定时聚合任务 每10秒汇总子Key 算分→排序→生成 TOP50 榜单快照 ④ 榜单快照缓存 本地缓存+Redis多副本 全网读同一份成品 📱 十万级读QPS 读的是快照,不碰计数器 ⑤ Flink 滑动窗口(可选) 精确窗口/去重UV/趋势检测 Kafka→Flink→结果表 核心思想:写入先"攒"再"拆",读取只读"成品快照" —— 读写两头都不直接碰那个热点计数器
🧠
为什么榜单要"定时算"而不是"实时算"?10 万读 QPS × 每次都现场排序 = 灾难;改成每 10 秒算一次、算好放缓存,排序开销从"每秒 10 万次"降到"每 10 秒 1 次",读请求变成纯粹的"拿现成的"。用户感知:榜单最多延迟 10 秒——完全无感。
03

核心瓶颈与数据一致性挑战

🔥 热 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%,代价是不能反查"某人来过没"。

💥
事故现场还原:某平台明星塌房事件冲上热搜,该话题计数 Key 所在的 Redis 节点 CPU 瞬间 100%,连带同节点上的购物车服务缓存一起卡死——热 Key 的可怕之处在于"殃及池鱼"。此后该团队立下军规:计数类业务独立 Redis 集群 + 热 Key 自动拆分中间件
04

优化手段、获益与代价

① 本地预聚合(攒批上报) 写优化

每台应用服务器在内存里累加计数,每秒(或每千条)批量上报一次到 Redis/Kafka。

✅ 获益:下游写压力降低 100~1000 倍,成本最低、收益最大的一招。
⚠️ 代价:应用实例宕机丢失最后 1 秒的计数(对热度类业务无伤大雅);计数可见性延迟约 1 秒。

② 热 Key 拆分(分片计数) 核心

count:topic1024 拆成 10 个子 Key(后缀 s0~s9),写入随机挑一个 INCR,读取时把 10 个加起来。

✅ 获益:单点热 Key 变成 10 个"温 Key"分散到不同节点,写吞吐横向扩展 10 倍。
⚠️ 代价:读取要聚合 N 个子 Key(由定时任务代劳);拆分数要预估,动态调整有迁移成本。

③ HyperLogLog / Bitmap 概率去重 UV统计

UV、在线人数用 HyperLogLog(12KB 固定内存,误差 0.81%);需要精确或反查的场景用 Bitmap(1 亿用户 ≈ 12MB)。

✅ 获益:内存占用从 GB 级降到 KB/MB 级;PFMERGE 还能合并多个时间桶算"最近N小时UV"。
⚠️ 代价:HLL 有 <1% 误差且无法查明细;Bitmap 要求用户 ID 连续紧凑,稀疏 ID 浪费严重。

④ 时间分桶滑动窗口 窗口

"最近1小时"切成 60 个分钟桶(每桶一个 Key,TTL 略大于窗口),统计时累加最近 60 个桶。

✅ 获益:窗口滑动 = 整桶进出,无需逐事件记录;内存可控、实现简单、TTL 自动清理。
⚠️ 代价:窗口边界有最大 1 桶的精度误差;桶粒度越细越准,但 Key 数量和聚合开销也线性上涨。

⑤ 榜单快照 + 多级缓存 读优化

定时任务生成 TOP-N 快照(JSON 成品),写入 Redis 多副本 + 应用本地缓存(1~5秒过期),CDN 可再兜一层。

✅ 获益:十万读 QPS 几乎全部被本地缓存消化,Redis 只承接缓存过期的零星回源。
⚠️ 代价:各节点快照更新有秒级参差(两台手机榜单顺序短暂不同——无人在意);多一条定时任务链路要监控。
🛡️
降级策略:① 聚合任务故障 → 榜单冻结在最后一次快照(显示旧榜比显示空榜好一万倍);② 计数层过载 → 采样计数(只统计 1/10 的事件再乘 10,误差可接受);③ 极端热点 → 该 Key 直接切到"本地计数+延迟合并"模式,先扛住再对账。三角站位:性能至上,一致性可以商量,误差是合法货币。
05

多种解决方案对比与选型建议

方案写吞吐精确度窗口能力适用场景
数据库 COUNT/GROUP BY
每次现查现算
精确SQL 灵活但慢后台报表、低频查询
Redis INCR + ZSet
计数器+有序集合排行
中 · 单Key瓶颈精确配合分桶可实现中小规模实时榜单的主力方案
Redis 分片计数 + 快照
热Key拆分 · 本页主推
高 · 可横向扩精确(延迟秒级)分桶实现大流量热搜/销量榜标准答案
HyperLogLog / Bitmap
概率结构去重
HLL约0.8%误差按桶合并UV/在线人数等去重统计
Kafka + Flink 流计算
事件时间滑动窗口
极高精确+乱序容忍原生最强复杂窗口/趋势检测/反作弊
🎯
选型口诀:小流量榜单一个 ZSet 打天下;写入上十万 QPS 就预聚合 + 热Key拆分 + 快照读三件套;UV/在线人数无脑 HyperLogLog(除非法务要求精确);需要"5分钟内搜索量环比暴涨3倍"这种复杂规则,才值得上 Flink。别用大炮打蚊子——多数"实时大屏"用分钟级快照就能让老板满意。
06

推演检验:换个条件还成立吗?

背下架构图不算学会,能判断"条件变了方案还成不成立"才算。先自己推,再展开参考。

推演 1:如果法务要求 UV 统计必须精确(一个都不能差),HyperLogLog 还能用吗?整套"误差换性能"的思路要推翻吗?

💡 参考推演

不推翻,但要分层:对外大屏、运营看板继续用 HLL(0.8% 误差没人在乎);结算、审计这条链路单独走精确通道——Bitmap(用户 ID 连续时内存可控)或离线批处理精确去重。误差换性能的前提永远是"业务能容忍",一旦某条链路不容忍,就为它单开精确通道,而不是全局退回精确方案陪葬性能。

推演 2:如果榜单从"全站一个"变成"每个城市一个"(300 个城市榜),热 Key 问题是解决了还是转移了?

💡 参考推演

稀释但没消失:北上广深的城市榜依然是热 Key,而三四线城市的榜单流量很低——继续给所有城市统一上"分片计数+快照"反而浪费。正确姿势是按流量分级:头部城市保留完整三件套,长尾城市退化成单 ZSet 直读。同一个业务里,不同实例可以配不同档位的方案,架构不必一刀切。

推演 3:如果运营要求"违规词条删除后榜单必须秒级消失",而快照是分钟级刷新的,怎么办?

💡 参考推演

快照是拉模式(定时重算),天然做不到秒级剔除。解法不是把快照周期压到秒级(成本爆炸),而是加一条推模式补丁通道:删词事件直接写入黑名单,读接口返回前先过滤黑名单,下个快照周期自然收敛。这就是"最终一致 + 实时补丁"的组合拳——为 1% 的紧急需求单开小门,别推翻 99% 场景够用的主架构。