十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

直播高并发环境搭建实战:从缓存设计到压测调优

直播高并发环境搭建实战:从缓存设计到压测调优 先交代背景我是一个平时主要写CRUD、偶尔调调接口的后端开发突然被安排去搭一套能支撑直播业务的高并发环境。第一次看到高并发三个字的时候我心里是发虚的——毕竟平时压测到几百QPS就觉得顶天了。但真正硬着头皮做下来发现直播场景的高并发并没有想象中那么神秘它只是把读多写少热点集中瞬时脉冲这几个特征放大到了极致。这套直播环境的搭建笔记我按照自己实际的推进顺序来写从需求拆解到环境选型从基础接口到弹幕实时链路再到缓存设计、压测调优和踩坑记录。适合那些和我一样对高并发只有概念、没有实操经验的后端同学参考。我尽量把每一步为什么这么做讲清楚而不是只贴配置。1. 直播高并发到底在高什么先拆需求再动手接手这个任务的第一件事不是写代码而是搞清楚直播业务的流量模型。如果按普通业务的思路去设计大概率会把系统做得很重却解决不了真正的问题。1.1 直播场景和普通Web业务的本质区别普通业务系统比如电商的商品详情页虽然也有高并发但用户的请求是相对分散的——有人看A商品有人看B商品压力被天然地分摊到了大量数据上。直播场景完全不是这样。一场热门直播开播的瞬间几千上万人同时涌进同一个直播间所有人的请求都打在同一个资源上同一个直播间信息、同一条弹幕流、同一个礼物排行榜。这就是典型的热点数据读多写少模型。另外还有几个特征需要注意瞬时脉冲开播前的预约提醒、主播上播通知会导致流量在几秒内集中涌入而不是平滑上升。实时性要求高弹幕、礼物、在线人数这些数据需要秒级甚至毫秒级到达用户端普通轮询接口根本扛不住。连接长时间占用用户进入直播间后会长时间停留WebSocket连接数会持续累积这对网关和后端服务的连接管理能力提出了额外要求。1.2 明确要支撑的核心指标在动手之前我和团队把目标定成了这样一组数字作为后续所有技术选型的依据指标目标值说明直播间同时在线人数1万初期目标架构上预留到5万弹幕峰值QPS5000集中在几个头部直播间礼物赠送QPS2000含排行榜更新接口P99响应时间300ms以内普通查询接口弹幕端到端延迟2秒以内用户发送到其他人看到这组数字看起来很吓人但拆到技术层面核心就三件事缓存扛读、队列削峰、连接复用。后面的所有搭建工作都是围绕这三件事展开的。2. 从 0 到 1 的技术选型一个后端小白的取舍过程选型这一步我纠结了很久因为网上的方案太多了——微服务、容器化、Service Mesh、云原生随便一搜全是高大上的名词。但作为一个后端小白我的判断标准很简单团队能不能维护、问题能不能排查、成本能不能接受。2.1 我最终选定的技术栈组件选择理由开发语言/框架Java Spring Boot 2.7团队主力技术栈资料多遇到问题容易查数据库MySQL 8.0存主播信息、直播间配置等基础数据缓存Redis 6.2扛住99%的读请求缓存热点数据消息队列RabbitMQ削峰填谷处理礼物、弹幕等写请求实时通信WebSocket Redis Pub/Sub弹幕、在线人数实时推送网关/负载均衡Nginx反向代理、静态资源缓存、连接负载前后端分离Vue3 Nginx部署前端静态资源独立部署接口走API没有上微服务没有上K8s。原因很简单初期1万在线的规模单体应用合理缓存完全能扛住。微服务带来的服务发现、链路追踪、配置中心等复杂度对于一个小团队来说是沉重的负担。2.2 为什么消息队列和缓存是必须的这两个组件是直播高并发环境的基石我分别说一下我的理解。缓存解决的是读的问题。直播间信息、主播信息、热门礼物列表这类数据读的频率极高但变化频率很低。把它们从MySQL挪到Redis里QPS从几百提升到几万响应时间从几十毫秒降到几毫秒。这是性价比最高的优化手段没有之一。消息队列解决的是写的问题。弹幕和礼物这类写请求如果直接写数据库5000 QPS的写入会把MySQL拖垮。引入RabbitMQ之后请求先进入队列由消费者按数据库能承受的速度异步落库。用户感知不到这几十毫秒的异步延迟但数据库的压力降了一个数量级。我把这个过程理解成削峰填谷——高峰期把请求囤在队列里低峰期慢慢消费保证系统不会被瞬时洪峰冲垮。2.3 开发环境与生产环境的差异搭建过程中最容易忽视的是环境差异。本地开发时Redis、RabbitMQ、MySQL都是本机的网络延迟几乎为零。但到了生产环境这些组件分布在不同的服务器上网络往返、连接池大小、超时时间都会变成瓶颈。我的做法是从第一天起就按生产环境的标准来配置连接参数。连接池大小、超时时间、重试机制这些在本地就按生产标准设定避免本地没问题上线就崩的尴尬局面。3. 直播基础接口搭建从普通CRUD开始的实战笔记环境搭好之后第一步是实现最基础的直播间接口——创建直播间、查询直播间信息、获取礼物列表。这些接口看起来和普通CRUD没区别但里面的细节决定了后面能不能扛住高并发。3.1 创建Spring Boot项目并配置核心依赖我用Spring Initializr创建了项目核心依赖如下依赖配置pom.xml中的关键部分dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency dependency groupIdorg.springframework.amqp/groupId artifactIdspring-boot-starter-amqp/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency这里有个小坑Spring Boot 2.7默认使用Lettuce作为Redis客户端在高并发下偶尔会出现连接池饥饿的问题。我后来换成了Jedis稳定性好了不少。原因是Jedis的连接池实现更朴素没那么多的异步和重连逻辑出问题的时候也更好排查。3.2 直播间信息查询接口的缓存设计直播间信息是所有请求里最热的数据每个用户进入直播间都要查一次。这个接口的缓存设计直接决定了系统的上限。我的设计方案是两级缓存本地缓存Caffeine每台服务器内存中缓存一份直播间信息过期时间60秒。这一层扛住了大部分请求连Redis都不用打。Redis缓存本地缓存未命中时查RedisRedis未命中时才查MySQL。代码实现大致是这样Service public class LiveRoomService { Autowired private StringRedisTemplate redisTemplate; Autowired private LiveRoomMapper liveRoomMapper; private CacheString, LiveRoomVO localCache Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(60, TimeUnit.SECONDS) .build(); public LiveRoomVO getLiveRoom(Long roomId) { // 第一级本地缓存 LiveRoomVO vo localCache.getIfPresent(String.valueOf(roomId)); if (vo ! null) { return vo; } // 第二级Redis缓存 String key live:room: roomId; String json redisTemplate.opsForValue().get(key); if (StringUtils.hasText(json)) { vo JSON.parseObject(json, LiveRoomVO.class); localCache.put(String.valueOf(roomId), vo); return vo; } // 第三级MySQL数据库 LiveRoom room liveRoomMapper.selectById(roomId); vo convertToVO(room); // 回填缓存 redisTemplate.opsForValue().set(key, JSON.toJSONString(vo), 10, TimeUnit.MINUTES); localCache.put(String.valueOf(roomId), vo); return vo; } }3.3 前后端分离的接口联调细节前后端分离是直播项目的标配。我用Vue3写了简单的直播间页面通过Nginx把静态资源和API请求分开处理。Nginx配置的关键部分server { listen 80; server_name live.example.com; # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # 后端API代理 location /api/ { proxy_pass http://127.0.0.1:8080; 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升级 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } }联调时最常遇见的坑是跨域。前端在localhost:5173开发后端在localhost:8080直接请求会被浏览器拦截。我的解决办法是开发环境用Vite的代理插件解决跨域生产环境通过Nginx同域代理不在后端代码里开CORS。这样既不影响开发效率也不会在生产环境暴露不必要的跨域策略。4. 弹幕实时链路的搭建过程从轮询到WebSocket弹幕系统是直播场景里最具代表性的高并发应用。刚开始我想用HTTP轮询实现——前端每3秒拉一次最新弹幕。但随着在线人数增多轮询的弊端很快暴露出来。4.1 为什么HTTP轮询行不通假设一个直播间有1万人在线轮询间隔3秒那么每秒就有大约3333个弹幕请求。这些请求大部分是无效的——如果没有新弹幕服务器返回的就是空数组。更严重的问题是弹幕的实时性无法保证。3秒的轮询间隔意味着用户发出的弹幕最坏情况要3秒后才能被别人看到。对直播场景来说这个延迟是致命的弹幕的本质是一起看直播的人在同一时刻的共鸣延迟超过2秒体验就完全不同了。HTTP轮询扛不住、长轮询有连接数和超时问题最终方案只能是WebSocket。4.2 WebSocket单机版实现Spring Boot集成WebSocket很简单Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new DanmuWebSocketHandler(), /ws/danmu) .setAllowedOrigins(*); } }WebSocket处理器Component public class DanmuWebSocketHandler extends TextWebSocketHandler { // 会话管理roomId - 会话集合 private static final ConcurrentHashMapString, CopyOnWriteArraySetWebSocketSession ROOM_SESSIONS new ConcurrentHashMap(); Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload message.getPayload(); JSONObject msg JSON.parseObject(payload); // 提取房间ID和内容 String roomId msg.getString(roomId); String content msg.getString(content); // 保存会话到房间分组 ROOM_SESSIONS.computeIfAbsent(roomId, k - new CopyOnWriteArraySet()) .add(session); // 广播给同房间的所有客户端 JSONObject response new JSONObject(); response.put(type, danmu); response.put(roomId, roomId); response.put(content, content); response.put(timestamp, System.currentTimeMillis()); TextMessage responseMsg new TextMessage(response.toJSONString()); for (WebSocketSession s : ROOM_SESSIONS.get(roomId)) { if (s.isOpen()) { s.sendMessage(responseMsg); } } } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { // 从所有房间分组中移除 ROOM_SESSIONS.forEach((roomId, sessions) - sessions.remove(session)); } }这个实现存在一个明显的性能问题广播是串行发送的如果房间里有1000人1条弹幕就要循环发送1000次。直播间人多的时候发送弹幕的线程会被卡住。4.3 广播性能优化从逐条发送到批量发送优化思路是把广播逻辑改成异步批量发送。具体做法是收到弹幕消息后先放入队列由专门的发送线程每20毫秒批量发送一次。这里我踩了个坑直接用WebSocketSession.sendMessage是线程安全的但如果多个线程同时往一个session发送消息各种连接池报错就会相继出现。最终我用了队列加单线程消费的模式确保每个session的消息发送都是串行的。4.4 多实例部署时的WebSocket会话共享单机版跑通之后很快面临新的问题如果后端部署多台服务器用户A连的是服务器1用户B连的是服务器2那么用户A发的弹幕服务器1的广播无法到达服务器2上的用户B。这个问题的解决方案是引入Redis Pub/Sub。每台服务器订阅同一个频道收到弹幕消息后先发布到Redis频道同时所有服务器都能收到这个发布消息再各自广播给自己连接的客户端。关键代码// 订阅Redis频道 Component public class RedisMessageSubscriber implements MessageListener { Override public void onMessage(Message message, byte[] pattern) { String payload new String(message.getBody()); JSONObject msg JSON.parseObject(payload); String roomId msg.getString(roomId); broadcastToClients(roomId, payload); } }多实例部署后广播逻辑变成收到客户端弹幕消息异步写入MySQL通过RabbitMQ队列使用Redis消息队列发布弹幕消息到公共频道所有实例的订阅者收到消息广播给自己维护的WebSocket连接这个方案能覆盖95%的直播场景需求。如果后续规模大到Redis Pub/Sub成为瓶颈再考虑引入专门的实时通信中间件但至少在初期阶段这个方案已经足够稳定。5. 礼物与热度数据的缓存设计排行榜、计数器与一致性礼物系统是直播平台的核心营收功能它的高并发特性比弹幕还要凶猛——发礼物是一个写操作但送礼之后要立刻更新主播的礼物排行榜、热度值、用户贡献榜等多个读接口这是典型的一次写入、多处读取场景。5.1 用Redis ZSet实现实时礼物排行榜礼物排行榜的需求是展示某个直播间当日送礼价值TOP50的用户列表。如果用MySQL实现每次查询都要做一次GROUP BY ORDER BY的聚合操作在高并发下性能极差。我用Redis的有序集合ZSet来实现这个功能。Service public class GiftRankService { Autowired private StringRedisTemplate redisTemplate; private static final String RANK_KEY_PREFIX live:gift:rank:; // 送礼时更新排行榜 public void addGiftRank(Long roomId, Long userId, int giftValue) { String key RANK_KEY_PREFIX roomId; // Zincrby给指定member增加分值 redisTemplate.opsForZSet().incrementScore(key, String.valueOf(userId), giftValue); // 设置过期时间当天有效 redisTemplate.expire(key, 24, TimeUnit.HOURS); } // 获取排行榜TOP50 public ListUserRankVO getTopRank(Long roomId) { String key RANK_KEY_PREFIX roomId; SetString members redisTemplate.opsForZSet().reverseRange(key, 0, 49); // 遍历生成排行列表 ListUserRankVO result new ArrayList(); int rank 1; for (String userId : members) { Double score redisTemplate.opsForZSet().score(key, userId); result.add(new UserRankVO(rank, Long.valueOf(userId), score.longValue())); } return result; } }这里有个典型的Redis一致性设计细节礼物进程和排行榜进程是分离的不需要实时同步。用户送礼后请求结果里直接带上最新排行榜流量低时再异步落库持久化。5.2 热度值计数器的原子自增直播间热度值的更新频率极高每次用户送礼、发弹幕、点击屏幕都会触发。用MySQL做自增操作会有严重的行锁竞争正确做法是使用Redis的原子自增操作。// 热度值自增 public void increaseHotValue(Long roomId, int value) { String key live:hot: roomId; redisTemplate.opsForValue().increment(key, value); } // 定期从Redis同步热度值到MySQL Scheduled(fixedDelay 5000) public void syncHotValueToDB() { // 获取所有直播间的key批量同步 }这里要做一个定时同步策略每5秒把Redis中的热度值增量同步到MySQL。如果Redis宕机最多丢失5秒的热度值数据这对直播场景是可接受的——但对收益结算等关键数据就不能用这个方案得走消息队列的可靠投递了。5.3 缓存穿透、击穿与雪崩的处理高并发环境下缓存系统的敌人有三个我的直播间接口就遇到了完整的打击。这三个问题不解决Redis宕机或缓存失效的瞬间大量请求会直接打到MySQL上系统瞬间崩溃。缓存穿透查询一个不存在的直播间ID每次都会穿透到MySQL。解决方案是缓存空值把不存在的ID也缓存起来过期时间设短一些比如5分钟。// 缓存空值防止穿透 public LiveRoomVO getLiveRoom(Long roomId) { String key live:room: roomId; String json redisTemplate.opsForValue().get(key); if (StringUtils.hasText(json)) { // 如果缓存的是空值标记 if (EMPTY.equals(json)) { return null; } return JSON.parseObject(json, LiveRoomVO.class); } LiveRoom room liveRoomMapper.selectById(roomId); if (room null) { // 缓存空值防止穿透 redisTemplate.opsForValue().set(key, EMPTY, 5, TimeUnit.MINUTES); return null; } // 缓存真实数据 redisTemplate.opsForValue().set(key, JSON.toJSONString(convertToVO(room)), 10, TimeUnit.MINUTES); return convertToVO(room); }缓存击穿某个热点直播间的缓存过期瞬间大量请求同时打到MySQL。解决方案是使用互斥锁只有第一个线程能查数据库其他线程等待后读取缓存。public LiveRoomVO getLiveRoomWithLock(Long roomId) { String key live:room: roomId; String json redisTemplate.opsForValue().get(key); if (StringUtils.hasText(json)) { return JSON.parseObject(json, LiveRoomVO.class); } // 尝试获取分布式锁 String lockKey live:room:lock: roomId; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 双重检查 json redisTemplate.opsForValue().get(key); if (StringUtils.hasText(json)) { return JSON.parseObject(json, LiveRoomVO.class); } // 查数据库 LiveRoom room liveRoomMapper.selectById(roomId); redisTemplate.opsForValue().set(key, JSON.toJSONString(convertToVO(room)), 10, TimeUnit.MINUTES); return convertToVO(room); } finally { redisTemplate.delete(lockKey); } } else { // 等待其他线程写入缓存这里可以用自旋或者直接返回 Thread.sleep(100); return getLiveRoom(roomId); } }缓存雪崩大量缓存同时失效导致所有请求都落到MySQL。解决方案是设置不同的过期时间在基础过期时间上加一个随机数避免缓存同时失效。从这些问题的处理过程中我最大的感受是高并发系统的复杂点不在于某个技术有多高级而在于极端情况下各个组件如何配合。缓存、消息队列、数据库每个组件单独看都不复杂但把它们组合起来应对极端场景才是真正的难点。5.4 送礼写请求的削峰处理礼物系统在高并发下最容易出现的问题是瞬时写入量过大。用户抢购热门礼物、活动期间集中送礼QPS会瞬间飙升。我的处理方案是引入RabbitMQ把写请求先放入消息队列再异步落库。Service public class GiftService { Autowired private RabbitTemplate rabbitTemplate; // 送礼接口入口 public void sendGift(Long userId, Long roomId, Long giftId, int count) { // 参数校验、余额检查等前置逻辑 // 构建消息 JSONObject msg new JSONObject(); msg.put(userId, userId); msg.put(roomId, roomId); msg.put(giftId, giftId); msg.put(count, count); // 发送到消息队列立即返回 rabbitTemplate.convertAndSend(gift.exchange, gift.queue, msg.toJSONString()); } // 消息消费者异步落库 RabbitListener(queues gift.queue) public void consumeGiftMessage(String message) { JSONObject msg JSON.parseObject(message); // 落库更新用户余额、礼物记录、主播收益等 // 同时更新Redis中的排行榜和热度值 } }消息队列在这里起到了两个作用一是削峰把瞬时高流量的写请求转化成稳定的数据库写入二是解耦用户送礼的接口不需要关心排行榜、收益计算等后续流程只需要确认消息已入队。6. JMeter压测实录从崩溃到稳定调优的全过程环境搭建和开发工作全部完成之后就进入验证阶段。我用JMeter编写了压测脚本对整套系统做了多轮高并发测试这个过程最有价值也最有教训。6.1 压测环境与脚本准备压测环境是一台4核8G的云主机作为压测机应用服务器是两台4核8G的ECSMySQL和Redis分别部署在独立主机上。JMeter脚本我主要配置了三类请求直播间信息查询模拟用户进入直播间GET请求发送弹幕POST请求走WebSocket或HTTP接口送礼接口POST请求走RabbitMQ异步链路压测参数配置参数第一轮压测第二轮压测第三轮压测线程数50010002000循环次数100100100总请求数50000100000200000期间间隔1秒1秒1秒6.2 第一轮压测系统直接崩溃第一轮压测的结果惨不忍睹500个线程并发访问系统直接超时错误率高达60%Tomcat线程池耗尽接口响应时间全部超过5秒。查看日志后发现几个严重问题MySQL数据库连接池耗尽默认的HikariCP连接池大小是10500个并发请求同时访问数据库直接把连接池打满了后续请求全部排队等待。Redis连接池太小Lettuce默认连接池大小只有8高并发下出现连接等待超时。直播间信息缓存未生效本地缓存过期时间设置太短导致大量请求直接穿透到Redis和MySQL。6.3 调优过程中的关键修改针对第一轮压测暴露的问题我做了以下几项调优MySQL连接池调大spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 30000Redis连接池调大换用JedisBean public JedisConnectionFactory jedisConnectionFactory() { RedisStandaloneConfiguration config new RedisStandaloneConfiguration(); config.setHostName(your-redis-host); config.setPort(6379); JedisClientConfiguration.JedisClientConfigurationBuilder builder JedisClientConfiguration.builder(); builder.usePooling() .poolConfig(jedisPoolConfig()); return new JedisConnectionFactory(config); } Bean public JedisPoolConfig jedisPoolConfig() { JedisPoolConfig poolConfig new JedisPoolConfig(); poolConfig.setMaxTotal(200); poolConfig.setMaxIdle(50); poolConfig.setMinIdle(10); poolConfig.setMaxWaitMillis(3000); return poolConfig; }本地缓存过期时间调整直播间信息接口的Caffeine缓存过期时间从60秒调整为30秒减少穿透率。这个调整基于一个判断直播间信息变更频率很低30秒的延迟完全可接受但能大大减轻Redis的压力。6.4 第二轮压测从崩溃到勉强可用经过调优第二轮压测结果明显改善1000个并发线程错误率降到了5%以下P99响应时间在800ms左右。虽然达标还差得远但至少系统不会崩了。但出现了一个新问题WebSocket连接的广播延迟明显增加。原因是广播逻辑中每个客户端发送消息是串行的房间内在线人数越多单条弹幕广播的耗时越长最终导致弹幕的端到端延迟超过了1秒。我对广播逻辑做了并发优化将每个房间的广播拆分为多个任务放入线程池并行处理。核心改动是// 并发广播按session分片每200个session一个任务 ListListWebSocketSession partitions Lists.partition(new ArrayList(sessions), 200); for (ListWebSocketSession partition : partitions) { executor.submit(() - { for (WebSocketSession s : partition) { if (s.isOpen()) { s.sendMessage(responseMsg); } } }); }通过并发广播弹幕延迟从1秒降到了200ms左右。6.5 第三轮压测达到目标的调优组合拳第三轮压测2000个并发线程总请求量20万最终结果指标目标值实测值错误率1%0.2%P99响应时间300ms268ms平均响应时间150ms82ms吞吐量(QPS)50006200这个结果达到了预期的目标。整个过程让我强烈意识到高并发没有银弹就是基础设施调优、缓存设计、异步化改造这几个手段的组合应用每一个环节缺一不可。7. 从崩溃到稳定的关键踩坑记录搭建过程中踩过的坑比想象中多得多下面这几个最典型价值最高。7.1 跨域问题前后端分离的第一道坎前端开发服务器在localhost:5173后端接口在localhost:8080跨域问题几乎是百分百会遇到。我在后端加了CrossOrigin注解和CORS配置类虽然解决了问题但留下了安全隐患——生产环境下也被允许任意来源跨域访问。后来改为生产环境用Nginx同域代理后端关闭CORS配置。这个调整不只为了安全更关键的是减少了预检请求OPTIONS请求降低了Nginx和后端的请求量。7.2 日志爆炸高并发下最容易忽视的问题第一轮压测时系统日志量巨大日志文件在几分钟内增长了几个GB直接把磁盘打满了。原因是我在业务代码里使用了大量调试级别的日志输出一个弹幕消息会产生十几条日志。应对措施把日志级别从DEBUG调整为INFO生产环境甚至调整为WARN加日志切面统一记录异常和慢请求日志而不是每个方法都打日志配置日志滚动策略按照大小和日期自动清理7.3 MySQL连接池耗尽与死锁问题弹幕、礼物的写入并发一高MySQL就频繁出现死锁报错。排查后发现是因为使用了MyBatis-Plus的批量插入接口在高并发下多个事务同时操作同一张表导致间隙锁冲突。我的解决方法是把所有写入操作串行化到消息队列中由消费者单线程批量写入。这样MySQL的压力虽然降低了吞吐上限但换来了极致的稳定性没有任何死锁产生。7.4 WebSocket连接泄漏压测过程中发现WebSocket连接的CPU和内存占用持续增长排查后确认是CopyOnWriteArraySet中的session没有及时移除——如果客户端断网而没有正常关闭连接session会长期留在集合中。解决方案是增加了心跳检测机制服务端每30秒向客户端发送ping消息60秒没收到pong响应就强制关闭会话并清理。这个机制对生产环境的稳定性至关重要。7.5 Nginx与WebSocket的配合在Nginx后面部署WebSocket服务必须加上升级头配置否则WebSocket握手会失败。这个坑我在联调阶段卡了很久最终配置如下location /ws/ { proxy_pass http://backend_servers; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }关键点是proxy_read_timeout必须设置得足够长否则Nginx默认60秒就会断开空闲的WebSocket连接导致客户端不断重连。8. 写在最后一个后端小白对高并发的真实感受整个项目从零开始到完成压测大约用了三周时间。作为一个平时写CRUD居多的后端开发我对高并发有了全新的认识。在高并发环境下真正影响系统稳定性的往往不是复杂的业务逻辑而是每个基础组件的配置细节和容错能力。我最大的发现是高并发架构不是堆技术而是做减法。每一个中间件都会带来额外的运维成本和故障点如果Redis能解决问题就不需要引入更多中间件。这也是为什么我的最终技术栈只有Nginx、Spring Boot、Redis、RabbitMQ、MySQL这五个组件。如果让我给同样从零开始做高并发系统的同学建议就三条先把缓存用好别急着上微服务压力测试从第一天就开始做别等写完所有功能再测日志和监控系统提前搭好线上出问题的时候这是救命的工具。希望这份笔记能帮到还在走这条路的人。
返回列表