
做后端这几年我一直觉得自己离“高并发”这三个字挺远的。日常业务接口的QPS能过百就算不错了数据库能跑个几十毫秒的慢查询都能当成事故来分析。直到去年朋友拉我一起做一个直播项目的后端我才算真正把手伸进了高并发这个泥潭里而且是以一个后端小白的身份从零到一一点点把整套直播环境搭起来。这篇笔记记录了我从需求分析、技术选型、代码设计到压测验证的完整过程包括踩过的坑和最后想明白的道理。如果你也是后端开发准备做直播或者做活动秒杀这类高并发场景或者只是想看看直播平台背后是怎么支撑几万人同时在线的这篇内容应该对你有用。文中涉及的方案不是唯一解但都是我在实际项目里验证过、能直接落地的思路。1. 从需求倒推直播高并发到底难在哪里1.1 直播后端和普通业务后端的本质区别做普通业务系统的时候我们的思维一般是“一个请求进来处理完返回结果”这是一个非常干净的“请求—响应”模型瓶颈主要看并发量和数据库连接数。但直播场景完全不一样它不是一个“请求—响应”模型而是一个“持续连接 高频消息 海量状态同步”的模型。你要处理的不是用户偶然点开的一个页面而是他打开直播间之后长达几十分钟甚至几个小时的持续互动。这带来三个非常现实的问题连接数高、状态变化快、流量突增猛。连接数高指的是直播间里的用户要一直在线每个人都要和服务端保持心跳或长连接一个直播间几万人连接就是几万个状态变化快指的是聊天区的弹幕、礼物、点赞每一秒可能产生成千上万条流量突增猛就是如果某个主播上了推荐位或者平台做一波推广流量可能在几分钟内翻好几倍。我第一次意识到这个问题是在写在线人数统计接口时。接口逻辑很简单查数据库统计在线人数返回给前端。本地自己玩的时候一切正常但一想到上万人都轮询同一个接口数据库根本扛不住。所以从需求倒推回去架构设计的核心就不该是“把接口写对”而是“怎么减少数据库压力、怎么扛住突发流量、怎么保证消息实时性”。整条技术路线都要围绕这三件事展开。1.2 哪些环节最容易成为瓶颈在动手写代码之前我先把直播项目里需要后端参与的场景列了一个清单然后逐个标出可能出问题的点。这里列几个最典型的直播间基础信息接口。用户一进直播间客户端第一时间会去请求直播间的标题、封面、主播信息、开播状态。这个接口是所有接口里调用量最高的进房瞬间所有用户都在请求。在线人数统计。直播间的“xxx人在线”看起来是一个数字背后却是海量的进房、退房、断线事件要非常快速地更新和展示。聊天弹幕和礼物消息。这是高并发下最头疼的部分因为它是全量广播。一个用户发弹幕理论上要推送给房间内的所有用户送礼物特效通知也一样。商品列表和订单/榜单。如果直播带货那商品库存扣减、订单创建也要跟上但这块更接近秒杀场景。我当时的判断是在线人数和弹幕是全链路中最需要高性能的点基础信息接口可以用缓存挡掉大部分压力订单和商品则单独走一套库存设计。于是我把整条后端链路拆成了三个层次接入层负载均衡和网关、业务层Spring Boot 服务、数据层Redis MySQL 消息队列。这样一来每一层的职责都清楚了后续做压测和排查问题时也更容易定位。2. 技术选型与关键组件设计2.1 服务端框架选型为什么是 Spring Boot Vue 前后端分离说句实在话技术选型有一部分原因是“我只会这些”。但冷静下来看对于直播中台这种项目这套组合不仅够用而且很合适。Spring Boot 生态成熟各种中间件的客户端都集成得很好Vue3 在前端领域普及度极高社区里能找到很多现成的直播管理后台模板前后端分离之后接口只负责返回 JSON页面完全独立部署扩展时互不干扰。选型的时候我还纠结过一个问题要不要直接上微服务网上一搜“直播 高并发 架构”到处都是注册中心、配置中心、网关、分布式事务看起来很唬人。但我当时的实际水平和一个直播中台项目的规模直接上微服务是很蠢的。微服务解决的是组织复杂性和独立扩展的问题不是解决高并发问题的银弹。一个直播后端在初期单体应用 缓存 消息队列就已经能支撑很大的体量硬拆微服务只会增加排查问题的难度。最终定下来的基础技术栈是Spring Boot 3.x业务服务主体MySQL 8业务数据落库Redis 7缓存、计数、排行榜RabbitMQ消息异步化和削峰填谷Nginx接入层负载均衡和静态资源分发Docker Compose本地环境和云上环境统一编排整体部署在云服务器上先用 Docker Compose 把一套基础环境拉起来后面压测验证通过后再考虑迁移到云厂商的容器服务。2.2 Redis 缓存设计给数据库挡枪的第一道防线直播高并发场景下Redis 几乎是必选项。原因很简单MySQL 的并发能力和响应速度都有限扛不住上万次对同一份数据的高频读而 Redis 是纯内存操作单实例读性能可以到十万级 QPS正好能把接口压力从数据库上解放出来。我在设计缓存的时候主要做了三件事第一直播间基础信息缓存。以live:room:info:{roomId}为 key将直播间信息序列化为 JSON 存进去设置过期时间 10 分钟。用户进房请求先查 Redis查不到再查 MySQL并把结果写回 Redis。为了防止缓存失效瞬间大量请求同时打到数据库我加了互斥锁当缓存失效时只允许一个请求去回源数据库其他请求短暂等待或者返回之前缓存的老数据。第二在线人数计数。这是一个典型的写多读多场景。如果用 MySQL 字段去存每秒钟几万次加一减一会把库打爆。我直接用 Redis 的INCR、DECR命令来维护在线人数。用户进房时就对live:room:online:{roomId}执行INCR退房或连接断开时执行DECR。查询接口直接GET这个 key完全不需要碰数据库。第三直播间热度榜。这是很多直播业务都要的功能我用 Redis 的ZSet来做。每个直播间的热度值用ZADD写入用ZREVRANGE取 Top 榜操作复杂度是 O(logN)比在 MySQL 里反复ORDER BY高效太多了。这三块设计不复杂但非常实用。核心思路就一句话把热的、变化频繁的数据尽量从数据库里拿出来放到内存里数据库只负责最终一致性落盘。2.3 消息队列让弹幕和礼物“飞”得更稳弹幕是直播场景里消息量最大的一块。如果每个用户发一条弹幕后端直接同步推送直播间内所有在线用户那这不仅是数据库压力的问题整个服务都会被拖垮。粗略算一笔账一个 2 万人的直播间如果有 5% 的人同时发弹幕就是 1000 条消息每条要广播到 2 万个客户端瞬间要处理 2000 万次推送量。同步做谁也扛不住。所以我用 RabbitMQ 做削峰填谷。客户端发弹幕请求后端先把消息扔进队列立即返回“发送成功”。真正的广播逻辑由消费端从队列里取消息再通过 WebSocket 推送给在线用户。哪怕瞬间消息量暴涨队列也能先攒住消费端用稳定的速率慢慢消费不会把服务打挂。选择 RabbitMQ 而不是 Kafka是因为直播场景更看重消息的即时性和可靠性RabbitMQ 配置简单、生态成熟对后端小团队非常友好。Kafka 吞吐虽然更高但运维复杂度也高等真有百万级 QPS 的需求再换也不迟。3. 实操过程从0到1搭建整套环境3.1 环境准备与容器化部署搭建这套环境我踩的第一个坑就是“本地能跑”不等于“云上能跑”。所以我一开始就用 Docker Compose 定义好所有依赖组件保证本地和云上环境完全一致。基础编排文件里面主要定义了四个服务MySQL、Redis、RabbitMQ 和 Nginx。MySQL 挂载了数据卷重启不丢数据Redis 开启了 AOF 持久化防止纯缓存模式宕机后在线人数归零RabbitMQ 设置了默认账号密码并把管理台端口暴露出来方便看队列积压情况Nginx 负责把/api前缀的请求反向代理到 Spring Boot 服务同时托管前端打包后的静态文件。容器都起来之后我会先做一次“环境自检”连一下 MySQL 看看能不能正常建表redis-cli ping看看 Redis 通不通RabbitMQ 管理台里能不能看到交换机绑定。这些验证看起来基础但真的能省很多后面排查问题的时间。3.2 核心接口设计与编码细节直播环境搭好之后我写的最核心的接口是“进房初始化”接口。它承载了前端进入直播间后的一次性数据拉取逻辑比较复杂根据roomId从 Redis 查直播间基本信息没有就查 MySQL 并回填。给这个房间的在线人数执行INCR。把用户昵称和进入时间写进 Redis 里一个以live:room:members:{roomId}命名的 Hash同时在在线玩家的ZSet中记录时间戳用于后面做离线清理。返回直播间信息、当前在线人数、服务器时间和 WebSocket 连接地址。这里有个很重要的细节在线人数的统计不应该只依赖用户主动调“退出房间”接口因为很多用户是直接关掉页面或者断网的如果只靠退房事件去做DECR人只会越加越多。我后面是用一种“心跳续期”的方案客户端每隔 30 秒上报一次心跳服务端把心跳时间刷新到 ZSet服务端每隔 1 分钟扫一次 ZSet把超过 90 秒没有心跳的用户踢出在线列表同时执行DECR修正在线人数。这样统计出来的在线人数才比较真实。弹幕接口的逻辑相对简单但设计思路上有一点值得多说弹幕消息体的结构一定要包含roomId、userId、content、timestamp并且在后端要做一个简单的合法性校验。无效消息不要进队列直接丢弃避免消费端做无畏的广播。提示缓存 key 的设计要统一规范。我用的格式是业务域:业务对象:主键例如live:room:info:9527、live:room:online:9527这样在 Redis 里按前缀搜索时一目了然清理临时数据也方便。3.3 压测验证用 JMeter 给环境上上强度环境搭完接口写完不压测一遍就上线心里永远没底。我用 JMeter 做了一个简单的压测脚本模拟的核心场景就是“用户同时涌进直播间”。线程组配置思路是并发用户数从 1000 开始逐步增加到 5000每个线程循环 5 次。每个线程做的动作是先调用“进房初始化”接口拿到直播间信息和在线人数。随机等待 200~800 毫秒模拟页面浏览。再调用“发送弹幕”接口。最后调用“退出房间”接口。为了防止请求压力集中在一个瞬间打满我在线程组里加了随机延迟定时器让真实流量分布更平滑一些。结果聚合报告里我最关注的几个指标是指标含义我当时的可接受范围Average平均响应时间300ms90% Line90% 请求的响应时间500msError%错误率0.1%Throughput每秒请求数越高越好第一次压测结果说实话很惨5000 并发时错误率直接飙到 5%很多请求超时。后面排查发现是数据库连接池配置太小加上 Nginx 的worker_connections默认值不够调整参数后再压稳定多了。具体调优内容我在第 4 部分详细说。4. 踩坑实录与排查技巧4.1 高并发下最典型的“翻车现场”整个搭建过程里我记录了一堆翻车现场这里挑几个最有代表性的分享第一个坑数据库连接池被打满。每次请求都要操作 Redis 和 MySQLHikariCP 默认最大连接数只有 105000 并发一上来连接池瞬间耗尽所有线程排队等待接口响应时间直接飙升到几秒。后来我把最大连接数调到了 100同时给部分接口加了 Redis才把连接池压力降下来。第二个坑Redis 连接未释放。一开始我用 Jedis 直连方式写缓存工具类压测跑着跑着就报Could not get a resource from the pool。原因是每次操作都从连接池拿连接使用完没归还。换成 Spring Data Redis 的封装之后这个问题就很少再出现了但这也提醒我不管用什么组件连接的生命周期管理永远是高并发场景的前提。第三个坑缓存穿透。有很多不存在的直播间 ID 被恶意刷进来每次都会直接打到 MySQL导致数据库负载升高。我后来在查不到数据的场景下把一个空值也写进 Redis设置 2 分钟过期并且把不合法请求的 ID 放入了布隆过滤器的拦截范围。第四个坑接口响应体过大。进房接口一开始返回了弹幕历史列表这个列表最多可能返回几百条消息JSON 体积很大5000 并发时带宽直接被占满。后来我把历史弹幕改成只返回最近 50 条并且单独分页接口按需拉取整个响应体从 200KB 降到了 3KB。4.2 缓存穿透、击穿、雪崩的实战应对网上关于这“三兄弟”的文章已经很多了但我还是想用自己的话说一遍因为这是高并发设计绕不开的问题。穿透请求缓存和数据库里都不存在的数据比如恶意构造一个不存在的房间 ID。应对方法就是上面说的缓存空值布隆过滤器先拦截。击穿某一个热点 key 在失效的瞬间大量请求同时打到数据库。应对方法是用互斥锁。只有一个线程真正去回源查询数据库其他线程先查 Redis查不到就短暂 sleep 再重试。我用了一段简单的代码来实现这个逻辑String value redisTemplate.opsForValue().get(key); if (value null) { String lockKey lock: key; if (redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 3, TimeUnit.SECONDS)) { try { value queryFromDatabase(); redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES); } finally { redisTemplate.delete(lockKey); } } else { Thread.sleep(50); value redisTemplate.opsForValue().get(key); } }这段代码在高并发下是管用的但也要注意分布式锁的过期时间设置不能太长也不能太短。我自己把锁超时设成 3 秒是考虑到数据库查询一般都在毫秒级3 秒足够完成回源。雪崩大量的 key 在同一时间失效导致请求全部落到数据库。这个问题最容易在设置缓存过期时间时犯因为最容易“图省事”把所有 key 都设置成相同时间。解决方法是给过期时间加一个随机值比如 600 秒 ± 60 秒让 key 的失效时间错开。另外可以用多级缓存本地缓存一层Redis 一层数据库兜底这样 Redis 就算挂了本地缓存还能挡一波。4.3 连接池与线程池的参数调优心得这部分是我压测调整环境时感受最深的。很多后端开发在写业务代码时基本不会去管连接池和线程池的参数但高并发场景下这些参数的合理配置往往比业务逻辑更能影响系统的生死。数据库连接池HikariCP 的参数里有几个很关键。maximumPoolSize决定了能同时打开的数据库连接数我之前调到 100压测 5000 并发时勉强够用minimumIdle是空闲连接数量建议和maximumPoolSize保持一致避免频繁创建和销毁连接connectionTimeout默认 30 秒太长我调到 3 秒如果连接池满就直接快速失败让上层感知到错误而不是让请求无限阻塞。另外千万不要盲目把连接池调得过大因为数据库能同时处理的连接数是有限的我试过调到 300数据库 CPU 反而飙升。Redis 连接池如果用的是 Lettuce 客户端需要注意它本身是线程安全的不用每次操作都新建连接。默认的maxTotal是 8对高并发场景来说太小我调到了 50maxIdle调到 20并且设置了testOnBorrow为 true确保拿到手里的连接一定可用。HTTP 线程池Spring Boot 内置的 Tomcat 线程池server.tomcat.max-threads默认是 200。也就是说即使你的 Redis、MySQL 全都扛得住Tomcat 最多也只能同时处理 200 个请求。我把这个值调到 1000并把accept-count调到 800让系统在突发流量下更从容一些。但要注意线程数也不是越大越好线程切换的开销很现实具体值还是要以压测数据为准。注意参数调优一定要结合自己的业务形态反复压测不要照抄网上的配置。每个项目的接口耗时、数据库资源、机器配置都不一样只有自己压出来的数据才是可信的。最后再分享一个小技巧整套环境从搭建到压测通过前后花了两周时间。我没有用多高深的技术架构依然是单体应用加缓存加消息队列但正是这些“普通”的组合在压测中承受住了几万并发。我个人最大的体会是高并发不是某个框架或者中间件的功劳而是一整套链路设计的结果——数据往缓存层放流量削峰走队列连接和线程池参数通过压测反复校准每一环都做到位了整体才能稳定。如果这个项目再往后推进我接下来会做两件事一是把 Nginx 后面挂两个应用实例用简单的负载均衡分担压力二是给服务加一套可观测体系把接口耗时、GC、Redis 命中率这些指标都采集起来。毕竟一台机器靠优化其实已经有明显上限横向扩展才是支撑更大流量最踏实的路子。希望这篇手搓笔记能给你搭建自己的直播高并发环境提供一点参考。