
手搓一套直播高并发环境这事听起来挺唬人尤其当一个后端业务开发平时干的最多的就是增删改查突然要面对直播这种流量洪峰第一反应基本是“我顶得住吗”和“这从哪下手”。但真把一个直播场景从零拆开你会发现高并发并没有那么玄学它考验的不是你写了多少花哨代码而是你对整条链路有没有清醒的认识流量从哪进来、在哪汇聚、哪一环最容易崩、崩了怎么兜底。这篇笔记不打算写成一堆理论就按我实际搭建的过程来从架构设计思路到具体落地步骤再到压测时踩过的坑一次性讲透。适合有半年到一年后端基础、但没正经做过高并发方案的开发也适合想独立搞定直播类项目、不想只当接口工具人的同学。看完不说让你直接成为架构师至少“直播高并发环境”这事你心里会有完整的地图。1. 内容整体设计与思路拆解直播高并发到底在并发什么1.1 先搞懂直播流量的真实画像在动手之前我花了一天时间梳理直播场景的特殊性发现它和普通业务系统最大的区别是流量高度汇聚、读写比例严重失衡、实时性要求极其苛刻。普通电商系统是万人逛店千人下单数据库并发写大概几百到上千但直播间是万人围观同时发弹幕、送礼、点赞你如果老老实实每次操作都打到数据库数据库当场就给你表演一个原地去世。我拆解了一下直播高并发环境的四个核心压力点视频流分发这是最重的一块但通常直接交给CDN和流媒体服务器不占用后端业务资源。这个环节决定了“卡不卡”。弹幕与聊天消息典型的高并发IM场景核心诉求是低延迟广播并不是持久化优先。这个环节决定了“活不活跃”。礼物、点赞、互动指令写操作极多但单个数据不重要核心诉求是高吞吐吸收和最终一致。这个环节决定了“氛围炸不炸”。在线人数、热度等实时统计需要精确但不代表必须实时落库往往是先聚合再异步刷新。这个环节决定了“人好多”这个数字准不准。这四个点压力模型各不相同所以后端方案绝对不是一套走天下。如果谁跟你说高并发就是一个Redis扛一切那他一定还没被线上事故教育过。1.2 为什么不能只靠加服务器硬扛我见过很多小团队的高并发方案本质是“加机器”——加负载均衡、加后端副本、加数据库连接数。这套办法在日均十万请求的规模下确实有效但直播这种秒级几万请求的场景加的机器再多也是浪费而且机器越多协调成本越高尤其是长连接场景。真正要解决的不是“机器够不够”而是“请求能不能在最短路径上被消灭掉”。什么意思一个用户进直播间如果发一条弹幕你的系统要经历“WebSocket接入→鉴权→入库→广播给其他用户”这条路径如果每一步都在数据库上跑一万条弹幕就是一万次数据库写再叠加查询在线人数、浏览计数器等数据库撑不过几分钟。所以核心思路只有一个把绝大部分流量挡在数据库之前。缓存、消息队列、本地内存、连接复用这些手段的本质都是给数据库“减负”。1.3 方案选型能用现成组件就别重复造轮子我这次搭建的直播高并发环境业务后端选择了Java Spring Boot因为生态成熟、资料多、招人好招而且对WebSocket支持很友好。但你用什么语言其实真的不重要重要的是你遵守了哪些架构原则。中间件选型我做了个对比直接照着抄也行组件选型选型理由替代方案接入层Nginx高并发IO强、配置灵活、WebSocket协议支持OKOpenResty、HAProxy应用层Spring Boot团队最熟、生态全、快速开发Go Gin、Node.js实时通信Netty WebSocket长连接性能极强、支持百万级连接Socket.IO、直接Spring WebSocket缓存层Redis Cluster高性能KV、天然支持发布订阅、排行榜Memcached但功能弱消息队列RocketMQ高吞吐、几乎不丢消息Kafka、RabbitMQ也可以存储层MySQL TiDB关系型为主必要时水平扩展PostgreSQL 分库分表这里特别说明一下我没有选Kafka是因为对于直播互动场景RocketMQ的延迟更低而且事务消息方便处理送礼这种“要有个账本”的动作但是如果你只是做日志采集Kafka更合适各干各的事。1.4 前端与后端配合的交互设计我这次搭建的直播高并发环境配套的前端是一个Vue3的项目整体用前后端分离架构。前端和我的后端只通过两条路打交道HTTP接口负责登录、进直播间拉取基本信息、送礼请求、关注/取消关注等低频动作。WebSocket连接负责弹幕、聊天、系统通知、在线人数实时更新。这里有一个值得每个后端新人记住的设计原则能用WebSocket推给前端的数据绝对不要让前端用HTTP轮询。我第一次做的时候偷懒在线人数让前端三秒拉一次接口结果两千人同时在线光轮询请求就占了后端快四成的资源后来全改成WebSocket推送资源占用立刻掉下来。前端与后端怎么交互这个问题本质上是“什么数据适合请求-响应、什么数据适合长连接推送”的决策问题。直播互动九成都是后者。2. 核心细节解析与实操要点直播间消息广播的架构演进2.1 单机版的WebSocket跑着跑着就断线直播高并发环境里最核心的能力是让一个人发的弹幕同一时间被直播间里所有人看到。这就是广播。我第一次写的广播逻辑很简单用户A发了一条弹幕WebSocket服务端收到消息后遍历当前这台机器上所有连接到这个直播间的Session逐个发消息。这就是所谓的“单机广播”。这个方案在五十人一百人同时在线的时候是完全没有问题的因为一次遍历最多也就几百个对象毫秒级就发完了。但到了三千人同时在线而且多台机器部署的时候问题就出现了我只是用Nginx做了负载均衡用户A连在机器1用户B连在机器2A发弹幕机器1根本不知道机器2上还有人在同一个直播间于是B永远收不到A的消息。这就是WebSocket横向扩容最容易踩的坑连接是分布式的但广播逻辑还停留在单机思维。要解决这个问题方案只有一条路——把某个直播间的所有连接信息从“本机私密”变成“全局共享”。2.2 引入Redis Pub/Sub解决多节点广播我的下一步是引入Redis发布订阅模式这是解决“跨节点广播”最轻量的方案之一。具体做法是每台后端服务启动的时候都订阅一个全局频道比如叫live_broadcast。当用户A发弹幕机器1收到后做两件事把消息写入Redis频道live_broadcast进行发布机器2、机器3只要订阅了这个频道就能实时收到这条消息然后各自遍历自己机器上的直播间连接把弹幕推给对应的人。这套机制的妙处在于你的业务层完全不用感知“哪个用户连接在哪台机器”所有的广播只要发给Redis一个频道所有机器都能收到再各自投递给自己的连接。等真正跑起来你会发现在三千人在线的时候机器1接收到一条弹幕需要发布到Redis然后所有机器再消费下来逐个推给本地连接。这里有个性能隐患Redis Pub/Sub是“发了就完”消费者掉线了就收不到所以本质上它不是一个可靠的消息系统如果广播过程中某一台机器的消费速度跟不上消息会越积越多最终导致内存暴涨然后整台机器被拖垮。因此在直播高并发环境里Redis Pub/Sub更适合消息量可控的场景。一旦单直播间在线人数破万弹幕每秒几千条我更推荐一种处理器级别的方案——通过订阅“直播间维度”的频道把粒度细化。一个直播间建一个Channel比如live_room_10001这样广播只推送给关心这个直播间的节点避免无关机器空转。2.3 集群会话管理千万别用Session存登录态直播场景中用户会频繁进出直播间而且WebSocket连接是长连接登录态的校验不可能像普通HTTP请求那样每次带token过来让你查一遍。我在这里踩过一个特别典型的坑刚开始图省事用传统的HttpSession来存用户登录信息。结果是单体部署的时候没啥问题一旦上集群用户的请求被Nginx分发到不同机器有的机器没有这个用户的Session直接判定未登录用户被强制踢出直播间体验非常糟糕。后来改成了无状态化设计用户登录成功后后端签发一个JWT令牌内部只存userId和过期时间用户通过WekSocket建连的时候带上这个token后端每次校验签名即可完全不依赖服务端存储Session。这样一来任何一台机器都可以独立校验用户身份这就叫“横向扩展的基础”——服务不再记忆用户只有Redis记得谁在哪个直播间。2.4 在线人数统计从实时遍历到异步聚合直播间右上角那个“在线人数”你以为真是实时的精确数字吗其实在工业级实现里它往往是“准实时”的。我第一次做在线人数统计想法很简单维护一个ConcurrentHashMap房间号, SetuserId每个WebSocket连接加入时put断开时remove前端来查就size。单机没问题集群就废了因为A机器只知道自己这一侧的连接数不知道整个直播间的实际总人数。更好的办法是所有机器都往Redis里写入在线状态用Hash结构key是直播间号field是userIdvalue是最近活跃时间戳。前端需要展示在线人数的时候后端直接HLEN一下就行毫秒级返回。但是这个方案在万人直播间也很快到了瓶颈——每次WebSocket心跳都要写一次Redis写放大很严重。最终我采用的是双层架构本地维护一个计数器每秒将增量同步到Redis在线人数展示接口优先读Redis的值Redis挂了就直接读本地均值兜底。这虽然不是绝对精确但在大促级别的流量下没人关心是9000还是9200差别只在营销文案上。2.5 直播数据与业务数据彻底分层直播间的热度榜、礼物榜、弹幕记录属于“直播数据”这类数据更新极快而且一旦直播结束历史价值就断崖式下跌。我不建议把它们实时写进MySQL理由有两点写入频率太高、无意义的历史积累还拖累检索性能。我的做法是分三层热数据全部放在Redis比如弹幕最近200条、礼物实时榜前50名直接内存操作毫秒级响应。温数据每5分钟把Redis里的增量数据异步批量刷入RocketMQ再消费落MySQL。冷数据直播结束后把完整记录归档到单独的直播历史库不再跟业务主库掺和。这样MySQL在主业务高并发期连碰都不碰直播互动数据压力自然就下来了。一个有意思的细节是RocketMQ消费端一定要做幂等因为消息队列的“至少一次投递”语义就意味着你可能会收到重复消息。我吃过这个亏有个点赞接口当时没做幂等消费者重复执行时热度直接翻倍在线所有用户看到的都是184亿热度弹幕全在刷“系统崩了”其实没崩就是数据重复了。3. 实操过程与核心环节实现从零搭建直播高并发环境全流程3.1 第一步本地环境准备与基础组件安装直接给一份我在Ubuntu 22.04上实测可用的清单照着敲就行。# 更新软件源 sudo apt update sudo apt upgrade -y # 安装JDK 17 sudo apt install openjdk-17-jdk -y # 安装Nginx sudo apt install nginx -y # 安装Redis 7.x curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg echo deb [signed-by/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main | sudo tee /etc/apt/sources.list.d/redis.list sudo apt update sudo apt install redis -y # 安装RocketMQ wget https://archive.apache.org/dist/rocketmq/5.1.3/rocketmq-all-5.1.3-bin-release.zip unzip rocketmq-all-5.1.3-bin-release.zip -d /opt/ cd /opt/rocketmq-all-5.1.3-bin-release sh bin/mqnamesrv sh bin/mqbroker -n localhost:9876 以上过程我实际跑通耗时约半小时主要时间花在RocketMQ下载上需要耐心。Redis安装完成后顺便做两个必须的配置改动。一个是bind 127.0.0.1改成内网IP或注释掉否则生产环境跨机器访问不了另一个是protected-mode yes要改成no并给Redis设置一个访问密码不然等于裸奔在公网上。3.2 第二步Spring Boot项目初始化与核心依赖我用Spring Initializr初始化了一个Spring Boot 3.1项目用的Java 17。核心依赖如下直接在pom.xml里加dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.rocketmq/groupId artifactIdrocketmq-spring-boot-starter/artifactId version2.3.0/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcom.alibaba.fastjson2/groupId artifactIdfastjson2/artifactId version2.0.25/version /dependency注意如果用jjwt 0.9.1需要额外引入javax.xml.bind依赖因为Java 17移除了一些JAXB模块或者直接换用java-jwt库省去这个麻烦。我当时用的是com.auth0:java-jwt:4.4.0一路顺滑。3.3 第三步WebSocket接入层的代码实现WebSocket在Spring Boot里实现其实很容易核心是一个处理器类继承TextWebSocketHandler。我写了一个精简版直接贴出来供参考Component public class LiveChatWebSocketHandler extends TextWebSocketHandler { // 本地维护连接映射userId - WebSocketSession private final ConcurrentHashMapLong, WebSocketSession sessionMap new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) { Long userId (Long) session.getAttributes().get(userId); sessionMap.put(userId, session); // 通知Redis在线人数1异步 } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { // 解析消息发给Redis Pub/Sub String payload message.getPayload(); ChatMessage chatMessage JSON.parseObject(payload, ChatMessage.class); redisTemplate.convertAndSend(live_broadcast, JSON.toJSONString(chatMessage)); } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { Long userId (Long) session.getAttributes().get(userId); sessionMap.remove(userId); // 通知Redis在线人数-1异步 } // 接收其他节点广播的消息推给本地连接 public void broadcastToLocal(ChatMessage message) { // 先判断本地是否有人在这个直播间没有就跳过 for (WebSocketSession s : sessionMap.values()) { if (Objects.equals(s.getAttributes().get(roomId), message.getRoomId())) { s.sendMessage(new TextMessage(JSON.toJSONString(message))); } } } }代码看起来简单但真正的复杂度在“连接鉴权”这一步。WebSocket建立连接时不能像HTTP那样随便加Header我的做法是允许前端在URL上拼一个token参数ws://localhost:8080/ws?tokenxxx然后在握手拦截器里解析token验证通过才放行。这里必须提醒一个安全细节token拼接在URL上会被Nginx的access_log记录下来存在泄漏风险。更稳妥的做法是前端先把token发给后端换取一个一次性shortCode有效期30秒再用shortCode去建连后端校验成功后立即作废。这样即使日志泄露攻击者也拿到的是一个已经失效的凭证。3.4 第四步处理WebSocket集群的订阅逻辑上面代码里有个redisTemplate.convertAndSend只是单机发布。要实现集群广播必须在每个后端实例启动的时候都监听同一个Redis频道。我加了一个订阅监听器Component public class RedisMessageSubscriber implements MessageListener { Autowired private LiveChatWebSocketHandler handler; Override public void onMessage(Message message, byte[] pattern) { String payload new String(message.getBody()); ChatMessage chatMessage JSON.parseObject(payload, ChatMessage.class); handler.broadcastToLocal(chatMessage); } }然后在配置类里注册订阅Configuration public class RedisConfig { Bean public RedisMessageListenerContainer redisContainer(RedisConnectionFactory factory, RedisMessageSubscriber subscriber) { RedisMessageListenerContainer container new RedisMessageListenerContainer(); container.setConnectionFactory(factory); container.addMessageListener(subscriber, new ChannelTopic(live_broadcast)); return container; } }到这里集群广播的最小闭环就跑通了。不过还要再上一个台阶如果直播间非常多、机器也很多全局广播总线会把所有消息广播到所有机器即使某台机器根本没有这个直播间的用户白白消耗CPU去解析和丢弃。优化方式我在前面提过就是按直播间维度拆分频道订阅的时候由一个父订阅接收所有频道的消息然后按前缀路由。container.addMessageListener(subscriber, new PatternTopic(live_room_*));这样每条消息只被订阅到对应直播间的机器拉取一次大幅降低无效消费。代价是需要手动管理频道生命周期但换来的是能扛更高并发。3.5 第五步Nginx配置WebSocket代理与负载均衡直播场景的接入层Nginx配置和普通HTTP转发略有不同重点在于Upgrade和Connection这两个Header否则WebSocket建连会失败。upstream live_backend { server 10.0.0.2:8080 weight5; server 10.0.0.3:8080 weight5; server 10.0.0.4:8080 weight5; keepalive 64; } server { listen 80; server_name live.example.com; # HTTP API转发 location /api/ { proxy_pass http://live_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # WebSocket转发 location /ws { proxy_pass http://live_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 60s; proxy_send_timeout 60s; } }注意proxy_read_timeout和proxy_send_timeout尽量设大一点我刚开始设置了默认的60秒结果用户挂着直播间不动一分钟就被Nginx掐断连接。WebSocket是需要长连接的建议设到300秒甚至更大让心跳包来维持连接。如果前端并没有实现心跳直接设成3600也行代价是Nginx会为每个死连接多保留一段时间内存。3.6 第六步带货前端接入模拟真实用户在线后端搭完我用Vue3写了一个简易直播间前端页面核心逻辑是用原生的WebSocket对象连接后端const ws new WebSocket(ws://${location.host}/ws?token${token}); ws.onopen () { ws.send(JSON.stringify({ type: 1, roomId: 10001, content: 兄弟们冲了 })); }; ws.onmessage (event) { const data JSON.parse(event.data); // 弹幕流里追加一条 messageList.value.push(data); };这里有一个小细节Vue在列表里频繁追加消息如果直接pushDOM更新会非常频繁非常吃性能。我当时做了数组截断只保留最后200条弹幕超出就shift掉最前面的页面卡顿感立刻消失不少。另外开启虚拟列表也是一个方向不过弹幕量没到万级的时候截断就够了。3.7 第七步压测前的容量预估与资源规划动手压测之前我先把物理资源摆了摆。我本地起了3台后端服务配置是4核8GNginx单独一台2核4GRedis一台2核4GRocketMQ一台2核4GMySQL一台2核4G。这套配置放云上大概一个月几百块的预算属于“能扛住事但别指望豪华”的级别。直播弹幕场景的理想QPS估算参考公式单台服务WebSocket可支撑连接数 ≈ (内存大小 - 业务预留) / 单连接占用内存单连接的内存在我的实测里大概8KB包含Session对象、发送缓冲、队列等。因此4G内存的机器扣除JVM堆和其他开销大约能支撑 3G / 8KB ≈ 40万连接但这是理论值实际上操作系统文件描述符、CPU调度都会先顶不住。正常情况下4核8G的单台机器支撑5000到8000个活跃WebSocket连接是比较舒适的区间。如果单直播间同时在线2万人用3台后端足够。注意这里说的“在线”不等于“活跃发消息”大部分观众是不说话的他们只是挂着连接。真正发消息的活跃率直播行业经验值是5%到15%。所以2万在线实际每秒弹幕峰也就几百条到一千多条这个并发量用Redis Pub/Sub完全扛得住。3.8 第八步压测执行与参数调整我用JMeter的WebSocket插件做了压测配置了5000个并发虚拟用户持续5分钟同时模拟5%的活跃用户循环发送弹幕。压测完的结果吞吐量单机约每秒处理2200条弹幕消息三台水平扩展后达到6000。端到端延迟用户A发弹幕到用户B看到平均85msp95为146msp99为210ms。后端服务CPU压测前20%压测中70%没有出现FullGC。Redis CPU稳定在40%左右内存涨幅很小。这个延迟对弹幕类场景来说完全可接受毕竟人眼感知的“实时”通常是指低于500ms。但压测也暴露了一个问题我的广播逻辑中for (WebSocketSession s : sessionMap.values())是线程不安全的线程池多个线程同时向同一个Session发送消息会偶发IOException: Broken pipe。这是WebSocket推送最常见的坑底层TCP连接被服务端/客户端关闭后session对象尚在Map中推送时才发现连接已死。修复方案是推送之前先判断session.isOpen()捕获IOException后把它从Map中移除。4. 常见问题与排查技巧实录直播环境里的典型事故档案4.1 在线人数越涨越慢最终归零断线重连风暴我第一次上线压测的时候在线人数涨到8000多之后就开始地往下掉最后直接归零。排查日志发现服务端没有报错但Nginx日志里出现大量WebSocket关闭记录客户端在超时断开之后不断重连越重连服务器压力越大循环下去所有连接都被打满最后谁都没法上线。根因分析我当时的WebSocket没有做心跳机制前端和服务端都不知道连接是不是还活着。后来我做了两件事服务端每30秒向下发一个Ping帧客户端收到后回Pong前端在5秒内没收到任何消息就主动断开重连并加上指数退避算法。这个组合拳下去断线重连风暴再也没出现过。4.2 弹幕偶尔丢几条Redis Pub/Sub的天然缺陷在某次压测中我发现一条弹幕发出去有的用户看到有的用户看不到而且不是固定丢一端。当时我在Redis Pub/Sub上做广播后来认真翻了Redis文档发现自己忽略了一个关键机制Redis的Pub/Sub消息是即时分发消费者如果断线或者消费缓慢消息直接丢弃没有积压和重放能力。这也意味着Pub/Sub只适合允许轻微丢消息的场景。如果你的弹幕系统要求“服务器重启也不能丢”必须把消息先写进RocketMQ然后从RocketMQ消费后做广播Redis Pub/Sub只是分发通道。后来我把链路升级成客户端→后端→RocketMQ持久化原始消息→消费端→Redis Pub/Sub广播→各机器→客户端。这样即使广播出了小问题最终还有MQ里的原始数据可以重新补偿。这条链路里每多一层就多5到10毫秒延迟但换来的是可靠性。弹幕不是金融交易丢了不致命但如果老板正好在直播间发了条弹幕没显示你就会被要求“彻查所有环节”所以宁可信一点。4.3 用户断开后还在直播间里一直占用名额做了踢人功能后我遇到了一个蛮有意思的问题一个用户明明关掉了浏览器在线人数却还显示他在线而且房间人数上限到了之后明明有空位却进不去了。排查后发现问题出在Session移除逻辑上WebSocket关闭通知有时候并不会触发afterConnectionClosed回调尤其是浏览器直接断电断网的情况TCP层没有发关闭帧服务端就无从感知。解决方法是心跳超时踢人每次收到Pong帧更新用户最后活跃时间到本地Map一个定时任务每10秒扫描一次发现超过60秒没有活跃记录的用户强制关闭它的Session并从Map移除。核心代码不复杂但缺了这段逻辑直播间的“幽灵观众”会越积越多直到把内存耗光。4.4 后端偶发500错误报错信息指向“Redis连接池耗尽”直播在线人数一高Redis的并发操作量剧增。我的RedisTemplate默认用的是Lettuce连接池默认连接池大小只有8个高峰期根本不够用。这个问题的本质是“连接池参数没有按场景调优”直接改配置spring: data: redis: lettuce: pool: max-active: 64 max-idle: 32 min-idle: 16 max-wait: 3000ms调完之后Redis连接池耗尽告警再没出现过。这里给个小建议max-active不是越大越好连接数等于并发数但如果每个连接都在慢查询上挂着再大也会耗尽把Redis的timeout设短一些快速失败比无限等待好。4.5 前后端联调时跨域问题CORS配置的坑直播前端单独部署在一个域名访问后端API时产生了跨域。解决方式是在Spring Boot里加一个CORS配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }但WebSocket跨域和HTTP跨域不一样它不是CORS协议而是依赖Origin头。Nginx在转发WebSocket时默认去掉Origin导致后端校验时发现Origin为空直接拒绝建连。解决方法是显式透传proxy_set_header Origin $http_origin;4.6 数据库连接被“直播热度榜”查询拖垮直播间的热度榜每秒钟被数千人刷新每次刷新都要去MySQL group by 统计数据库直接被干崩溃。后来我改成Redis的ZSet实现每个直播间热度项以“用户ID-礼物类型”作为member价值作为score累加展示榜单纯粹取ZSet前50名毫秒级返回。原本这条查询要走MySQL全表扫描现在走了内存排序业务接口RT从800ms掉到12ms这是我在直播项目里做的收益最大的一次优化。核心思路归纳成一句话任何排行榜都不应该实时查询数据库。4.7 前端播放不流畅带宽与CDN才是主角直播业务后端只是负责互动真正占流量大头的视频流最终必须走CDN。一个1080p的直播间码率大概3Mbps一万名观众同时观看理论峰值带宽是30Gbps。这个数字如果用自建服务器扛光带宽费用就是一个天文数字所以视频流的选型建议直接买云厂商的CDN加流媒体直播服务不要在自建服务器领域为难自己。后端要做的事情是配合视频流通过回调接口接收开播/断流/转码事件把这些事件写进Redis前端轮询到了状态变更再刷新播放器地址。这个联动说起来简单实际联调的时候视频流回调延迟、鉴权过期等问题一堆但那是另一篇笔记的故事了。5. 高并发环境的兜底策略从“能跑”到“扛炸”5.1 限流与降级直播场景的保护伞直播间的热闹程度是不均匀的可能平时几百人突然某个主播搞抽奖一下子冲进来几万人。这时候如果设计上没有兜底后端必挂。我在关键路径上做了三层保护网关层限流Nginx的limit_req_zone以用户IP为维度每秒最多20个请求超过的返回503。接口层限流Spring Boot使用Guava RateLimiter或Redis做分布式限流每个直播间每秒最多处理N条互动消息。系统层降级Redis如果挂了直接熔断到“只有广播没有持久化”的降级模式互动体验延迟变大但不至于全站瘫痪。5.2 热点Key打爆Redis谁背锅直播间的热度一旦高起来同一个直播间的计数器会成为一个极端的热点Key所有请求都打向同一个Redis实例。这时即使Redis集群有6个节点也只有1个节点在忙碌另外5个闲置。解决热点Key的常见方法在客户端做本地缓存副本每台后端本机缓存该直播间的热度信息设置过期时间比如5秒。前端查询热度时优先读本地本地没有才回源Redis这样上万人的请求被拦截在每台机器内部Redis只有每秒几次的请求量热点压力彻底化解。这个方法对“读多写少”的直播在线人数和热度榜特别有效。但要注意引入副本后数据一致性窗口变大文案上应为“实时热度”而非“精确人数”。5.3 服务无状态化让扩容变成加机器的事在搭建过程中我刻意把所有状态都外置到Redis服务的本地只保留连接Session这种“本来就是这台机器私有”的东西。这样做的结果是横向扩容不需要迁移任何数据新机器启动后自动注册到Nginx流量自然分过来缩容时把机器从Nginx摘掉等WebSocket连接自然断开即可。这套设计是整个直播高并发环境的底座如果没有无状态化你后面做弹性伸缩、故障替换、发布升级每一步都会非常痛苦。5.4 直播结束后的清理与归档直播结束后Redis里还留着这个直播间的各种缓存数据如果不清理Redis内存消耗越来越严重。我在服务端加了一个“关播事件”的监听器收到关播请求后把该直播间的弹幕、热度榜、礼物记录批量归档到MySQL然后删除Redis对应的Key。归档的时机很重要不要在关播瞬间进行大批量导出我选择的是异步任务错峰执行比如关播后5分钟再归档避免和“观众集体退出直播间”的流量高峰撞车。6. 直播高并发环境的扩展想象整套直播高并发环境跑通之后我心里最踏实的感触是高并发不是玄学它是一系列朴素原则的集合——能缓存就不查库、能异步就不同步、能广播就不轮询、有状态的东西尽量外置。这套环境的能力边界目前在哪用我现在的配置扛住单直播间2万人在线每秒2000条弹幕三天直播不重启没有任何问题。如果还想往上扩方向也清晰WebSocket接入层可以替换为更底层的NettyRedis可以上Codis或者Redis Cluster自动分片消息队列可以从RocketMQ换成Pulsar这些都是“方案演进”而不是“推翻重来”。最后一个实际体验中的小建议做高并发项目别一上来就面向百万并发设计先按1000并发搭一套能跑通的最小系统然后用压测工具把它压垮找到瓶颈点再针对性地优化。真正让你成长的不是架构图而是系统被你亲手压垮时那一屏幕的报错信息。我自己就是在一次次的“崩了—排查—修复”中从一个只会写增删改查的后端小白慢慢摸到了高并发的门道。这套环境就是最好的训练场。