实时在线观看人数统计 — editorial cover photo

直播间那个在线人数,是怎么实时算出来的

直播和视频场景里「当前有多少人在看」这个数字,看着简单,百万级并发下要做准、做快、做省,每一项都是工程取舍。

2025年4月12日·返回文章列表
文章大纲

直播间的在线人数,是那种看起来特别简单、做起来全是坑的需求。用户想看到的只是一个数字,比如「当前 12.3 万人在看」。但这个数字背后,是百万级并发连接、实时心跳、去重统计、前端轮询这一整套链路。

而且不同业务对「在线人数」的定义不一样,精度要求也不一样,用错了方案要么数字差得离谱,要么成本扛不住。这篇把这个看似简单的数字背后,几种主流的实现方案和它们的取舍讲清楚。

先搞清楚,你统计的是什么「在线」

PlantUML
正在加载图表…
PlantUML diagram

图:三种"在线"定义成本递增——累计 UV 用概率算法最省,同时在线要维护心跳状态最重,先定业务要哪种再选方案

这是最容易被忽略的一步,但选错了方案就全错了。

第一种是「累计观看人数」,从直播开始到现在,有多少人进来过。这个数字只增不减,统计的是 UV(独立访客)。直播平台为了好看,通常会显示这个数字,因为它会一直涨,给人「很火」的感觉。

第二种是「同时在线人数」,某一时刻真正在看的用户数。这个数字会随观众进出实时波动,是技术含量最高的一个。它需要知道谁还在看(心跳还活着),谁走了(心跳断了或者关了)。

第三种是「实时观看人数」,比同时在线更松一点,通常指最近一个短时间窗(比如 5 分钟)内有活跃的用户数。它允许一定的延迟和误差,实现比同时在线简单。

这三种的实现难度和成本天差地别。累计 UV 用 HyperLogLog 就行,误差可控、内存极省。同时在线要维护实时的心跳状态,最重。实时观看是折中。先想清楚业务要的是哪种,再选方案。

累计 UV,HyperLogLog 是性价比之王

累计观看人数这种去重计数,最经典的方案是 Redis HyperLogLog。

它的原理是一种概率算法,用极小的内存(标准 12KB)估算大量数据的基数(去重后的数量)。误差在 0.81% 左右,对于「显示给用户看的人数」这种场景完全够用。用户不会在意你显示 100 万还是 100.8 万,但你在意的是不能用 100GB 内存去精确存 1 亿个用户 ID。

用法很简单。每个进来的用户 ID,PFADD 进 HyperLogLog。要显示的时候,PFCOUNT 出来。合并多个 HyperLogLog(比如多天、多直播间)用 PFMERGE

HyperLogLog 的代价是精度。如果你需要精确到个位的去重计数(比如「前 1000 名观众送奖励」这种需要精确排名的),它就不行,得换 Bitmap 或者精确的 Set。但显示给用户的「人气数字」,HyperLogLog 几乎是最优解。

同时在线人数,最重的那个

PlantUML
正在加载图表…
PlantUML diagram

图:三种存储方案各有取舍——ZSet 用时间戳做 score 兼顾实时判断与批量清理,是同时在线的推荐方案

同时在线人数是最难做的,因为你必须实时知道每个用户的状态。

主流的方案基于心跳。客户端定期(比如每 10 秒)向服务端发一个心跳,表示「我还在看」。服务端记录每个用户最后心跳的时间,判断「在线」的标准是「最近 30 秒内有心跳」。30 秒没收到心跳的用户算离线。

存储方案有几种。最朴素的是 Redis 的 key 加过期,每个在线用户一个 key(比如 online:{userId}),TTL 30 秒。心跳来了就刷新 TTL,统计在线人数用 DBSIZE 或者维护一个计数器。这个方案简单,但用户量大的时候 key 数量爆炸,几百万在线就是几百万个 key,内存和扫描成本都高。

更高效的方案是用 Redis Bitmap。每个用户 ID 对应一个 bit 位,在线就置 1,超时置 0。统计在线人数就是 BITCOUNT。1 亿用户只需要约 12MB 内存(1 亿 bit)。心跳来了 SETBIT,后台任务定期把超时的 bit 清零。这个方案内存极省,但清零超时用户的逻辑要设计好,不能每个用户都去检查。

还有一个思路是「会话集中管理」。用一个有序集合(ZSet),score 存最后心跳的时间戳,value 存用户 ID。要统计在线人数,ZCOUNT 出 score 在「现在减 30 秒」到「现在」之间的元素数。超时的用户用 ZREMRANGEBYSCORE 批量清掉。这个方案兼顾了实时性和可清理性,是我比较推荐的。

精度和成本的取舍

在线人数统计的核心取舍是「精度 vs 成本」。你要多准,愿意花多少钱。

精确方案(Bitmap 或 Set)准但贵。1 亿用户的 Bitmap 是 12MB,看着不大,但加上心跳的写入频率(每秒几十万次 SETBIT),Redis 的 CPU 和网络压力不小。而且心跳是高频写操作,Redis 单线程的处理能力会成为瓶颈。

近似方案(HyperLogLog 或者采样)省但不准。如果业务允许「显示的数字是个估算值」,用概率算法能省一个数量级的成本。直播平台的「人气值」很多就是估算的,反正用户也验证不了。

时间窗口的取舍。心跳间隔短(比如 5 秒),在线判断准但心跳量大。间隔长(比如 60 秒),心跳量小但用户离线后很久才反映到数字上。这个要看业务对实时性的要求。直播这种要实时反馈的,间隔要短。视频点播的「正在看」可以容忍长一点的间隔。

前端轮询和 WebSocket 推送的取舍

数字怎么从服务端到前端也有讲究。前端定时轮询(每 5 秒拉一次)简单,但几百万观众同时轮询,服务端压力大。WebSocket 推送(服务端有更新就推)实时性好压力小,但长连接的维护本身就是一套基础设施。规模小用轮询,规模大上 WebSocket。别为了「实时性」在小规模场景硬上 WebSocket,那是过度设计。

一个完整的链路长什么样

PlantUML
正在加载图表…
PlantUML diagram

图:客户端心跳经网关限流落到心跳服务,按直播间分片写 ZSet,后台任务每 5 秒清超时并算出在线数,前端轮询缓存展示

把上面的组合起来,一个百万级直播的在线人数统计链路大致是这样。

客户端每 10 秒发一次心跳,带上用户 ID 和直播间 ID。心跳先经过网关的限流(防止恶意高频心跳),落到心跳服务。心跳服务用 Redis ZSet 记录,key 是直播间 ID,score 是心跳时间戳,value 是用户 ID。每来一个心跳刷新 score。

一个后台任务每 5 秒跑一次,用 ZREMRANGEBYSCORE 清掉每个直播间超过 30 秒没心跳的用户。同时 ZCOUNT 出当前在线人数,写到一个缓存 key 里。前端每 5 秒轮询这个缓存 key 拿数字显示。

这套链路里,心跳的写入是瓶颈。百万级在线意味着每秒十万级的心跳写入,单 Redis 扛不住。解决思路是分片,按直播间 ID 把心跳分散到多个 Redis 实例。每个实例只负责一部分直播间,写入压力分摊。汇总的时候把各分片的在线数加起来。

蜂控平台查 TB 级发票数据时用的是「ES 做辅助索引加 HBase 存明细」的分离设计,思路是一样的。把高频的实时状态(心跳、在线判断)放轻量存储,把重查询(历史统计、趋势分析)放重存储。各司其职,不让一个组件承担所有压力。

在线人数统计这个小需求,能折射出高并发系统的几个核心课题,实时状态的维护、去重计数的取舍、高频写入的瓶颈、精度和成本的平衡。把这些想透了,类似的「实时计数」需求(弹幕数、点赞数、实时销售额)都能举一反三。

Continue Reading

关联文档推荐

查看全部
秒杀系统设计:库存防超卖与流量削峰全链路 — editorial cover photo多级缓存:从浏览器到 DB 的全链路缓存设计 — vibrant cover for technical blog articleRedis 热点 Key 与大 Key 治理 — editorial cover photo
作者:archy.shawn
声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。