直播高并发后端实战:从500并发崩溃到稳定支撑十万在线
2026/9/19 14:22:19 网站建设 项目流程

直播这行当,表面看是主播对着镜头说话,背后其实是一整套跟"秒杀"性质差不多的实时系统。我刚开始接触这块的时候,以为搭个推流服务器、配个 CDN 就完事了,结果第一次做压测,500 个并发连接就把服务打挂了——弹幕延迟飙到十几秒,礼物消息丢了一大半,排行榜数据直接错乱。那次之后我才明白,直播的高并发跟电商秒杀完全是两码事:秒杀是瞬时脉冲,扛过那一秒就结束了;直播是持续高压,一场两小时的直播,峰值可能持续四十分钟以上,系统得在这种状态下稳定运行,不能有任何抖动。

这篇笔记记录的是我从零搭一套能扛住直播场景的高并发后端环境的完整过程。我不是什么架构大牛,就是个普通后端,踩过的坑比看过的文档多。所以这篇东西不会跟你扯太多理论,重点放在"我当时为什么这么选""这个参数为什么调成这个值""哪里差点翻车"这些实际问题上。如果你也是刚接触直播后端,或者正在为公司的直播功能做技术选型,这篇应该能帮你少走不少弯路。

1. 直播高并发到底难在哪:先把问题定义清楚

1.1 直播场景的流量特征跟普通 Web 完全不是一回事

普通 Web 应用的流量曲线通常是平缓的,白天高一点、夜里低一点,峰值和均值差个两三倍就算波动大了。直播不一样,它的流量特征可以用三个词概括:持续高压、脉冲叠加、读写极度不对称

持续高压好理解,一场直播开播后,观众是陆续进来的,但一旦进入热门推荐位,在线人数会在几分钟内从几百涨到几万甚至几十万,而且这个量级会维持到直播结束。这意味着你的系统不能靠"弹性扩容扛过峰值然后缩容"这种策略,因为峰值持续时间太长,扩容速度跟不上涌入速度。

脉冲叠加是指直播过程中会不断出现各种事件:主播喊一句"扣 1",弹幕瞬间爆炸;整点抽奖,几万人同时点按钮;PK 环节,两边粉丝疯狂刷礼物。这些事件会在原本就很高的基础流量上再叠一层脉冲,系统要能扛住这种叠加。

读写极度不对称是直播最特殊的地方。一个直播间里,发弹幕的人是少数,看弹幕的人是多数,比例可能是 1:50 甚至 1:100。也就是说,写请求相对少,但读请求量极大,而且读请求要求极低延迟——弹幕晚到三秒,观众就觉得"卡了",体验直接崩。

我一开始用传统的 MVC 架构,每个弹幕都写库、每个观众都轮询查库,数据库连接池瞬间被打满。后来才想明白,直播场景的核心矛盾不是"写不进去",而是"读不过来"。

1.2 三个必须解决的核心问题

把问题定义清楚之后,我发现直播后端要解决的核心问题其实就三个:

第一,消息的实时分发。弹幕、礼物、系统通知、在线人数变化,这些消息需要以极低延迟推送到所有在线观众。用 HTTP 轮询肯定不行,延迟高、浪费带宽、服务器压力大。必须用长连接方案,让服务器主动推。

第二,热点数据的读写分离。直播间信息、在线人数、排行榜、礼物列表,这些数据被高频读取,但更新频率相对低。如果每次都查数据库,数据库扛不住。必须加缓存层,而且缓存的设计要考虑到"热点 key"问题——一个热门直播间的 key 可能被每秒几十万次访问。

第三,突发流量的削峰填谷。抽奖、秒杀礼物、PK 冲榜这些场景,瞬间写入量会暴涨。如果直接打到数据库,轻则慢查询,重则锁表。需要引入消息队列做缓冲,把同步写变成异步写。

这三个问题对应到技术选型上,就是长连接网关、Redis 缓存集群、消息队列三件套。后面我会逐个拆解我是怎么选、怎么配、怎么调的。

1.3 一个容易被忽略的坑:连接数不等于并发数

这里有个概念必须先掰扯清楚,不然压测数据会骗你。很多人把"同时在线人数"和"并发连接数"混为一谈,实际上两者差得很远。

一个观众打开直播间,可能建立一条 WebSocket 长连接,但他在直播间里可能同时触发多个请求:拉取历史弹幕、查询主播信息、获取礼物列表、上报心跳。这些请求如果都走同一条连接,那连接数就是在线人数;但如果部分走 HTTP 短连接,实际并发请求数可能是在线人数的三到五倍。

更麻烦的是,长连接本身有开销。每条 WebSocket 连接在服务器端都要占内存、占文件描述符,还要定期发心跳包维持。一台 4 核 8G 的机器,理论上能维持几万条连接,但实际能稳定维持多少,取决于你的心跳策略、内存管理和内核参数调优。

我第一次压测的时候,用 JMeter 模拟了 5000 个并发用户,结果显示服务正常。但真实上线后,3000 人在线就开始出现连接断开。后来才发现,JMeter 的 WebSocket 插件默认不复用心跳,而真实客户端会持续发心跳,实际连接开销比压测时大得多。

2. 技术选型:为什么我最终选了这套组合

2.1 长连接网关:Netty 还是 Go 还是 Node.js

长连接网关是整个直播后端的地基,选错了后面全白搭。我当时对比了三个方案:

方案优势劣势适用场景
Java + Netty生态成熟,跟现有 Java 后端无缝集成,线程模型可控内存占用高,JVM 调优门槛高团队是 Java 技术栈,需要跟业务系统深度集成
Go + Goroutine并发模型天然适合长连接,内存占用低,部署简单跟 Java 业务系统集成需要跨语言调用团队有 Go 经验,追求极致性能
Node.js + Socket.io开发速度快,前后端同构单线程模型,CPU 密集场景弱,大规模连接下 GC 压力大快速原型验证,中小规模场景

我最终选了Netty,原因很实际:我们后端本来就是 Java 技术栈,用 Netty 可以直接复用现有的用户认证、权限校验、日志体系,不用搞跨语言调用。而且 Netty 的EpollEventLoopGroup在 Linux 上用的是 epoll 边缘触发模式,单机维持十万连接不是问题。

但 Netty 有个坑必须提前说:它的内存管理是手动式的ByteBuf用完必须释放,否则会内存泄漏。我一开始没注意,跑了两天发现堆外内存一直涨,最后用-Dio.netty.leakDetection.level=paranoid才定位到问题。

2.2 缓存层:Redis 集群怎么部署才不翻车

Redis 在直播场景里承担了太多职责:缓存直播间信息、存储在线用户集合、做排行榜、做消息去重、做限流计数器。可以说 Redis 挂了,整个直播后端就瘫了。

我一开始用单机 Redis,压测到 8000 QPS 的时候开始出现超时。后来换成主从加哨兵,读性能上去了,但写还是单点。最终方案是Redis Cluster,6 个节点,3 主 3 从,每个主节点负责一部分槽位。

这里有几个关键决策:

为什么是 3 主而不是更多?因为 Redis Cluster 的槽位是固定的 16384 个,主节点越多,每个节点负责的槽位越少,但节点间通信开销越大。3 主在中小规模直播场景下足够,单主能扛 5 万到 8 万 QPS。

为什么用 Cluster 而不是 Codis?Codis 是代理模式,多一层转发,延迟会高一点。Cluster 是去中心化的,客户端直连节点,延迟更低。但 Cluster 的客户端要支持槽位重定向,我用的是 Lettuce,它对 Cluster 支持比较好。

热点 key 怎么处理?这是直播场景最头疼的问题。一个热门直播间的在线用户集合 key,可能被每秒几十万次访问,单个 Redis 节点根本扛不住。我的做法是给热点 key 加随机后缀做分片,比如live:room:123:online:0live:room:123:online:9,读的时候随机选一个,写的时候写所有分片。这样把一个 key 的压力分散到 10 个 key 上。

但分片有个代价:获取在线人数时需要聚合 10 个分片的值,有一点点延迟。对于在线人数这种允许几秒误差的数据,完全可以接受。但如果是礼物排行榜这种要求强一致的,就不能这么搞。

2.3 消息队列:Kafka 还是 RocketMQ 还是 RabbitMQ

消息队列在直播场景里主要做两件事:削峰解耦。抽奖请求先写队列,后端慢慢消费;礼物消息写队列,多个下游系统(排行榜、统计、通知)各自消费。

我选了Kafka,理由是高吞吐。直播场景的消息量很大,一场热门直播每秒可能产生几万条弹幕消息,Kafka 的单分区写入能到十万级 QPS,完全够用。而且 Kafka 的持久化机制成熟,消息不会丢。

但 Kafka 有个问题:它的延迟比 RocketMQ 高一点。Kafka 为了吞吐做了批量发送,默认linger.ms=0的时候延迟还好,但一旦调大批量参数,延迟就上去了。直播弹幕对延迟敏感,所以我把linger.ms设成 5ms,batch.size设成 16KB,在吞吐和延迟之间找了个平衡。

RocketMQ 其实更适合直播场景,它的延迟更低,而且支持事务消息和延迟消息,做抽奖倒计时之类的功能很方便。但我们团队对 Kafka 更熟,运维成本低,所以最终选了 Kafka。

2.4 数据库:MySQL 分库分表还是 TiDB

数据库这块我纠结了很久。直播场景的写入量不算特别大,但读取量很大,而且数据增长快——弹幕消息一天可能产生几千万条。

MySQL 单表超过 500 万行性能就开始下降,弹幕表肯定要分表。我一开始想用 ShardingSphere 做分库分表,但配置复杂,而且跨分片查询很麻烦。后来考虑 TiDB,它是分布式数据库,自动分片,兼容 MySQL 协议,但运维成本高,至少需要 3 个节点起步。

最终我的方案是:核心业务数据用 MySQL 主从,弹幕历史消息用 MongoDB。MySQL 存用户、直播间、礼物、订单这些结构化数据,MongoDB 存弹幕这种文档型数据。MongoDB 的写入性能好,而且支持 TTL 索引,可以自动过期清理老弹幕。

这个选型不一定适合所有人。如果团队没有 MongoDB 运维经验,老老实实用 MySQL 分表也行,只是要提前规划好分片键。我见过有人用room_id做分片键,结果热门直播间的数据全落在一个分片上,分表等于没分。

3. 核心模块的落地细节:从连接建立到消息分发

3.1 连接建立:认证、限流、心跳一个都不能少

WebSocket 连接建立的过程比普通 HTTP 请求复杂得多,因为它是长连接,一旦建立就会持续占用资源。所以连接建立阶段必须做好三件事:认证、限流、心跳协商

认证这块,我的做法是:客户端先用 HTTP 请求拿到一个短期 token,然后用这个 token 发起 WebSocket 连接。网关收到连接请求后,先校验 token 有效性,再从 Redis 里查用户信息,最后把用户信息和连接绑定。

这里有个细节:token 校验不能查数据库。连接建立是高频操作,如果每次都查库,数据库扛不住。我的做法是 token 里直接加密了用户 ID 和过期时间,网关本地解密校验,只有解密失败才回源查库。

限流是防止恶意连接的关键。我用了两层限流:IP 维度用户维度。同一个 IP 每秒最多建立 10 条连接,同一个用户最多维持 3 条连接(多设备登录)。限流用 Redis 的滑动窗口实现,INCREXPIRE就够了。

// 简化的连接限流逻辑 public boolean allowConnection(String ip, String userId) { String ipKey = "limit:ip:" + ip; String userKey = "limit:user:" + userId; Long ipCount = redis.incr(ipKey); if (ipCount == 1) { redis.expire(ipKey, 1); // 1秒窗口 } if (ipCount > 10) { return false; } Long userCount = redis.incr(userKey); if (userCount == 1) { redis.expire(userKey, 60); // 60秒窗口 } return userCount <= 3; }

心跳协商经常被忽略,但它直接影响连接稳定性。WebSocket 协议本身有 ping/pong 帧,但很多客户端不主动发。我的做法是服务端每 30 秒发一次 ping,客户端收到后回 pong,如果连续 3 次没收到 pong,就主动断开连接。这样能及时清理死连接,释放资源。

心跳间隔不能太短,否则浪费带宽和 CPU;也不能太长,否则死连接清理不及时。30 秒是我实测下来比较平衡的值。如果是移动端场景,可以放宽到 60 秒,因为移动网络切换频繁,太短的心跳容易误判。

3.2 消息分发:怎么把一条弹幕推给十万人

消息分发是直播后端最核心的逻辑。一条弹幕从主播发出,到十万观众看到,中间要经过好几个环节。

第一步,消息接收。主播客户端发弹幕到网关,网关做基础校验(长度、敏感词、频率),然后写 Kafka。这里不直接写 Redis 或数据库,因为写 Kafka 最快,而且能削峰。

第二步,消息消费。后端有个消费者服务从 Kafka 拉消息,做业务处理:存 MongoDB、更新 Redis 里的弹幕列表、触发敏感词二次校验、计算弹幕热度。

第三步,消息推送。消费者处理完后,把消息推给网关集群。这里有个关键问题:网关是多台的,观众连接分散在不同网关上,怎么保证消息推给所有相关网关?

我的方案是用Redis Pub/Sub 做网关间广播。每个网关订阅自己负责的直播间频道,消费者把消息发到对应频道,所有订阅了该频道的网关都能收到,然后各自推给自己持有的连接。

// 网关订阅直播间频道 public void subscribeRoom(String roomId) { redisPubSub.subscribe("live:room:" + roomId, (channel, message) -> { // 收到消息后推给本网关持有的该直播间连接 pushToLocalConnections(roomId, message); }); }

但这个方案有个问题:Redis Pub/Sub 不保证消息可靠投递,如果网关在消息发布时正好重启,这条消息就丢了。对于弹幕这种允许少量丢失的场景可以接受,但礼物消息不能丢。

所以礼物消息我走了另一条路:写 Kafka,网关作为消费者直接消费。每个网关消费所有礼物消息,然后过滤出自己负责的直播间。这样虽然每个网关都要消费全量消息,但保证了可靠性。

这里有个取舍:弹幕走 Pub/Sub,延迟低但可能丢;礼物走 Kafka,可靠但延迟略高。实际场景里,弹幕丢一两条没人发现,礼物丢一条用户会投诉,所以这个取舍是合理的。

3.3 在线人数统计:怎么做到既准确又不拖垮系统

在线人数是个看起来简单、做起来很烦的功能。观众进进出出,你要实时统计当前在线人数,还要保证这个数字不能太离谱。

我试过三种方案:

方案一:Redis Set 存储。每个直播间的在线用户存在一个 Set 里,用户进来SADD,出去SREM,人数用SCARD查。这个方案准确,但问题是大直播间 Set 可能有几十万个元素,SCARD是 O(1) 的还好,但SREM在元素多的时候会慢,而且内存占用大。

方案二:Redis HyperLogLog。HyperLogLog 是概率数据结构,用极小的内存估算基数,误差在 0.81% 左右。对于在线人数这种不需要精确到个位的场景,完全够用。但 HyperLogLog 不支持删除元素,用户退出后没法从计数里减掉。

方案三:分片计数 + 定时校准。这是我最终用的方案。把在线用户按用户 ID 哈希分散到 10 个 Redis 分片,每个分片用 Set 存储。查人数时把 10 个分片的SCARD加起来。同时每 5 分钟做一次全量校准,清理掉因为异常断开而残留的用户。

// 分片存储在线用户 public void userOnline(String roomId, String userId) { int shard = Math.abs(userId.hashCode()) % 10; String key = "live:room:" + roomId + ":online:" + shard; redis.sadd(key, userId); redis.expire(key, 3600); // 1小时过期,防止残留 } public long getOnlineCount(String roomId) { long total = 0; for (int i = 0; i < 10; i++) { String key = "live:room:" + roomId + ":online:" + i; total += redis.scard(key); } return total; }

这个方案的好处是:写入分散到 10 个分片,单分片压力小;查询时 10 次SCARD可以 pipeline 批量执行,延迟很低;定时校准保证数据不会偏差太大。

校准逻辑要小心,不能直接把 Set 清空重建,那样会误删真实在线用户。我的做法是遍历 Set 里的用户,检查他们的连接是否还活着(通过网关的心跳记录),不活的才删。

3.4 礼物与排行榜:强一致场景下的缓存设计

礼物和排行榜是直播里最赚钱的功能,也是最容易出问题的功能。用户刷了一个礼物,排行榜必须立刻更新,而且不能算错,否则用户会投诉。

这块我没用缓存分片,因为排行榜要求强一致。我的方案是:Redis Sorted Set 做实时排行榜,MySQL 做持久化,定时对账

礼物消息进来后,先写 Kafka,消费者处理时做两件事:ZINCRBY更新 Redis 排行榜,同时写一条流水到 MySQL。Redis 排行榜用ZREVRANGE查前 N 名,延迟极低。

但这里有个坑:Redis 和 MySQL 的数据可能不一致。比如 Redis 更新成功但 MySQL 写入失败,或者反过来。我的做法是以 MySQL 为准,Redis 只做展示。每 5 分钟跑一次对账任务,从 MySQL 聚合出正确的排行榜,覆盖 Redis 里的数据。

// 对账任务:从 MySQL 聚合排行榜覆盖 Redis public void reconcileRanking(String roomId) { List<RankItem> dbRank = mysql.query( "SELECT user_id, SUM(amount) as total FROM gift_record " + "WHERE room_id = ? AND create_time > ? GROUP BY user_id", roomId, startTime ); String redisKey = "live:room:" + roomId + ":rank"; redis.del(redisKey); for (RankItem item : dbRank) { redis.zadd(redisKey, item.getTotal(), item.getUserId()); } }

对账的代价是 Redis 排行榜会短暂为空,所以我的做法是双 key 切换:维护rank:Arank:B两个 key,对账时写备用 key,写完原子切换。这样用户无感知。

礼物场景还有个特殊问题:大额礼物需要事务保证。用户余额扣减和礼物记录写入必须在一个事务里,否则会出现扣了钱没收到礼物的情况。这块我用的是本地消息表 + 定时补偿,保证最终一致。

4. 压测与调优:那些只有真跑起来才知道的事

4.1 压测环境搭建:别在本地测,数据会骗你

压测这块我踩的坑最多。一开始我在本地 Mac 上压测,结果 2000 并发就卡得不行,以为是代码问题,排查了半天才发现是本地网络和文件描述符限制。

后来我把压测环境搬到云上,用独立的压测机(4 核 8G)跑 JMeter,被测服务在另一台机器上,两台机器同区域内网互通。这样压测结果才靠谱。

压测脚本我用的是 JMeter + WebSocket 插件,模拟真实用户行为:建立连接、发心跳、随机发弹幕、随机刷礼物、定时查询在线人数。脚本里加了随机思考时间(1 到 5 秒),模拟真实用户的节奏。

压测机本身也可能成为瓶颈。我用 4 核 8G 的机器跑 JMeter,最多模拟 5000 个 WebSocket 连接就到顶了。如果要压更大规模,得用多台压测机分布式压测,或者用更轻量的压测工具比如 wrk 或 Gatling。

4.2 第一次压测:500 并发就崩了

第一次正式压测,我设了 500 并发,结果服务在 3 分钟内就出现大量超时。排查过程如下:

第一步,看监控。CPU 使用率不高(40%),内存正常,但 Redis 的 QPS 飙到了 8 万,而且有大量慢查询。

第二步,定位慢查询。SLOWLOG GET查 Redis 慢日志,发现大量SMEMBERS操作。原来是我在查在线用户列表时用了SMEMBERS,这个命令在 Set 元素多的时候是 O(N) 的,几十万元素的 Set 直接卡死 Redis。

第三步,修复。SMEMBERS换成SRANDMEMBER抽样,或者用SSCAN分批遍历。在线用户列表这种数据,前端只需要展示一部分,不需要全量拉取。

第四步,复测。修复后重新压测,500 并发稳定通过,Redis QPS 降到 2 万左右。

这个坑的教训是:Redis 的命令复杂度必须心里有数。O(N) 的命令在数据量大的时候就是灾难。常用的 O(N) 命令有KEYSSMEMBERSHGETALLLRANGE(大范围),这些在生产环境都要慎用。

4.3 第二次压测:连接数上去了,但消息延迟高

修完 Redis 问题后,我把并发提到 3000,连接建立没问题了,但消息延迟很高——弹幕从发出到收到平均要 3 秒,峰值到 8 秒。

排查发现两个问题:

问题一:Kafka 消费者处理慢。消费者里做了太多同步操作:写 MongoDB、更新 Redis、调敏感词服务。每个操作都要几十毫秒,累积起来就慢了。

修复:把非核心操作异步化。写 MongoDB 改成批量写,每 100 条或每 500ms 写一次;敏感词校验改成异步,先推送再校验,发现违规再撤回。

问题二:网关推送是单线程的。我一开始用单个线程遍历连接推送消息,连接多了之后遍历本身就很慢。

修复:改成多线程推送,每个网关起一个线程池,按连接哈希分配到不同线程。同时用 Netty 的ChannelGroup批量写,减少系统调用。

// 用 ChannelGroup 批量推送 ChannelGroup group = roomChannelGroups.get(roomId); if (group != null) { group.writeAndFlush(new TextWebSocketFrame(message)); }

修复后重新压测,3000 并发下弹幕延迟降到 200ms 以内,达标。

4.4 第三次压测:模拟真实场景的混合负载

前两次压测都是单一场景,第三次我做了混合负载:70% 用户在观看(只收弹幕不发),20% 用户偶尔发弹幕,5% 用户刷礼物,5% 用户在抽奖。

这次压测暴露了一个新问题:抽奖场景下 Redis 出现热点 key。抽奖按钮的计数器 key 被每秒几万次INCR,单个 Redis 节点 CPU 飙到 90%。

修复方案:抽奖计数器做本地缓存 + 批量提交。每个网关本地维护一个计数器,用户点击时先加本地计数,每 100ms 把本地计数批量提交到 Redis。这样 Redis 的写入量降低了两个数量级。

// 本地计数器批量提交 private AtomicLong localCounter = new AtomicLong(0); public void onLotteryClick() { localCounter.incrementAndGet(); } // 定时任务每100ms提交一次 @Scheduled(fixedRate = 100) public void flushCounter() { long count = localCounter.getAndSet(0); if (count > 0) { redis.incrBy("lottery:count:" + roomId, count); } }

这个方案的代价是计数有最多 100ms 的延迟,而且网关重启会丢失未提交的计数。对于抽奖这种场景,100ms 延迟可以接受,丢失几条计数也无所谓。但如果是涉及金钱的场景,就不能这么搞。

4.5 调优清单:那些必须改的内核参数

压测到后期,瓶颈往往不在应用层,而在操作系统。以下是我调整过的关键参数:

参数默认值调整值作用
net.core.somaxconn12865535增大 TCP 连接队列
net.ipv4.tcp_max_syn_backlog102465535增大 SYN 队列
net.ipv4.tcp_tw_reuse01复用 TIME_WAIT 连接
net.ipv4.ip_local_port_range32768-609991024-65535扩大本地端口范围
fs.file-max系统默认1000000增大文件描述符上限
net.ipv4.tcp_keepalive_time7200300加快死连接回收

这些参数改完之后,单机维持的连接数从 1 万提升到了 5 万以上。但要注意,tcp_tw_reuse在某些场景下可能导致 NAT 问题,如果服务部署在容器里,要谨慎开启。

文件描述符限制要改两处:/etc/security/limits.conf里的nofile,以及 systemd 服务的LimitNOFILE。只改一处不生效,这个坑我踩过。

5. 上线后的真实表现与后续优化方向

5.1 真实流量下的表现:跟压测差距有多大

上线第一周,我盯监控盯得眼睛都快瞎了。真实流量跟压测确实有差距,主要体现在三个方面:

连接建立更分散。压测时连接是瞬间建立的,真实场景下用户是陆续进来的,这对系统反而更友好,因为压力是渐进的。

消息类型更杂。压测时我只模拟了弹幕和礼物,真实场景还有系统通知、进房欢迎、关注提醒、分享消息等十几种类型。消息类型多了之后,序列化和反序列化的开销上去了,我后来把 JSON 换成了 Protobuf,体积小了 60%,序列化速度快了一倍。

用户行为更随机。压测脚本的行为是固定的,真实用户会突然大量发弹幕、突然集体退出、突然疯狂刷礼物。这种随机性对系统的弹性要求更高。我的应对是加了自动扩容策略:当单网关连接数超过阈值时,自动触发扩容,新连接分配到新网关。

5.2 还没解决的问题:消息顺序性和重复消费

有两个问题我到现在还没完全解决,写出来给后来人提个醒。

消息顺序性。弹幕消息理论上应该按发送顺序展示,但走 Kafka 多分区 + 多消费者之后,顺序没法保证。用户看到的是"弹幕 A 在弹幕 B 后面发出,但显示在 B 前面"。我的缓解方案是给每条消息加时间戳,客户端按时间戳排序展示。但这只是缓解,不是根治。

重复消费。Kafka 的 at-least-once 语义意味着消息可能重复。我用了 Redis 做去重,每条消息带唯一 ID,消费前先查 Redis 是否已处理。但 Redis 去重有窗口期,窗口外的重复没法防。对于弹幕这种场景,偶尔重复一条用户能接受,但礼物重复就是事故了。礼物消息我加了数据库唯一索引兜底,重复插入会失败,然后走补偿逻辑。

5.3 如果重来一次,我会怎么改

如果让我重新搭这套环境,有几个地方我会改:

第一,网关用 Go 重写。Netty 虽然成熟,但 JVM 的内存占用和 GC 停顿在长连接场景下确实是负担。Go 的 goroutine 模型更适合这种场景,单机连接数能翻倍。

第二,消息队列换成 Pulsar。Pulsar 的计算存储分离架构更适合直播这种流量波动大的场景,扩容更灵活,而且支持多租户,不同直播间可以隔离。

第三,缓存层加本地缓存。现在所有读都走 Redis,其实像直播间基本信息这种变化很少的数据,可以在网关本地缓存一份,减少 Redis 压力。用 Caffeine 做本地缓存,配合 Redis 的发布订阅做失效通知。

这套环境从零搭到现在稳定运行,前后花了大概两个月。中间踩的坑、熬的夜、看的监控图,都浓缩在这篇笔记里了。直播高并发这个方向,理论是一回事,真跑起来是另一回事。希望这些实际经验能帮到正在做类似事情的你。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询