ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

秒杀系统高并发架构设计:Redis预扣库存与MQ异步削峰实战

2026/9/30 8:36:49 拓冰建站 浏览量
秒杀系统高并发架构设计:Redis预扣库存与MQ异步削峰实战 1. 秒杀系统的核心难题拆解1.1 高并发场景到底难在哪做秒杀系统之前我先说句实在话秒杀本身不是一个业务功能问题而是一个系统容量问题。你用 SpringBoot 写一个普通 CRUD 接口能响应正常请求不代表它能扛住秒杀流量——这是两个完全不同的维度。所谓秒杀就是某个商品的库存有限比如 1000 件但用户请求可能在瞬间达到几十万甚至上百万。这一瞬间的流量冲击会把一个平时表现正常的接口直接打挂。我见过太多人犯的错误拿着普通订单模块的思路去做秒杀直接在 MySQL 里做库存扣减结果压测一上来数据库连接池先被冲垮紧接着整个服务不可用。问题的本质不在于代码写得对不对而在于你没有在系统入口把流量挡住。拆解下来秒杀系统的核心难题其实只有四个高并发读秒杀未开始、进行中用户不断刷新页面和查询状态热点商品的数据读取量极大高并发写一旦秒杀开始大量用户在同一瞬间提交下单请求集中写入订单和扣减库存库存一致性不能超卖卖的数量超过库存也不能少卖库存扣了但订单没生成这是资金相关的硬约束系统防崩溃任何单一组件数据库、缓存、服务节点都不能成为整个链路中的单点瓶颈。这四个问题不是独立存在的它们互相牵连要解决高并发读就要引入缓存要解决高并发写就要削峰限流削峰限流又会影响下单的实时性而一致性要求又对异步链路的设计提出了更高要求。所以秒杀系统是一个典型的系统性的技术方案不是单点优化就能解决的。1.2 秒杀系统的性能瓶颈地图我习惯在动手写代码之前先画一张“瓶颈地图”——也就是当流量进来时每一层组件会在什么情况下先撑不住。这样我才能知道该在哪个环节做保护。系统环节容量上限大致参考最先出现的故障现象浏览器/客户端取决于用户设备和网络白屏、请求超时Nginx 网关单机约 5 万并发连接连接拒绝、499 错误SpringBoot 应用取决于线程池配置默认 Tomcat 约 200 线程请求排队、超时、CPU 飙高Redis 缓存单实例约 10 万 QPS连接超时、内存不足MySQL 数据库单库大约 1000~3000 TPS 写入死锁、连接池耗尽、锁等待超时这张表不是精确数据但它给出了一个直观的概念每一层能扛的并发量级是不同的差距甚至是一两个数量级。所以秒杀架构的核心思想就一句话把流量拦截在越靠近入口的位置越好让最终打到数据库的请求数量降到数据库能承受的范围之内。也就是说我们要做的所有设计都是围绕着一个目标——削峰填谷层层防护。前端的静态资源走 CDN页面秒杀按钮的点击请求走 Nginx 限流业务请求先经过 Redis 过滤真正到 MySQL 的写操作只有极少数“幸运儿”。这就是秒杀架构的底层逻辑。2. 系统整体架构与技术选型2.1 总体分层架构设计一个完整的秒杀系统从上到下可以分成五层。我按实际请求的流转顺序说明。客户端层用户打开 H5 页面或小程序看到秒杀倒计时和商品信息。这一层要解决的唯一问题是“不要把你的后端打死”所以秒杀按钮要在抢购开始前置灰不能点开始后也要防止用户脚本狂点。接入层Nginx做最粗粒度的保护比如按 IP 限制请求频率、拦截明显恶意请求。Nginx 这一层性能极高单机能扛数万并发适合做全局流量过滤。应用层SpringBoot核心业务逻辑所在。这一层处理秒杀令牌校验、Redis 预扣库存、发送 MQ 消息等逻辑。应用层要做线程池配置、接口级别限流。缓存层Redis秒杀系统的“主力军”。商品数量、用户是否已抢过、预扣库存等全部都放 Redis目的就是不用 MySQL 来扛流量。数据层MySQL MQ最终的订单落库和库存最终扣减。到这一层的请求数量在设计上应当已经被削减到系统容量的 10% 甚至 1% 以内。我见过有人觉得“多一层就多一次网络开销何必呢”但实际情况恰恰相反多一层保护系统能抗的流量是几何级数增长的。秒杀系统的设计目标不是让每个请求都走完整个流程而是让绝大多数请求在中间某层就被快速返回——比如“已抢完”“您已参与过秒杀”。用生活里的例子来类比这就好比景区限流。一个热门景点一天只能容纳 1 万人但早上来了 10 万人排队你不能让所有人都挤到景点门口再劝退而是要在外围停车场、售票口、检票口多层分流。秒杀系统也是一样的每一层都在做“劝退”和“筛选”的工作。2.2 技术选型分析SpringBoot 为核心的优势秒杀系统选择 SpringBoot不是因为 SpringBoot 本身有“秒杀功能”而是因为它是最适合快速构建高并发服务的基础框架。我基于实际开发经验给你梳理选择 SpringBoot 的几个关键原因。第一生态成熟接第三方组件几乎零成本。秒杀系统必须依赖 Redis、MQ、MySQL 这些组件而 SpringBoot 的 starter 机制让这些集成变成了几行配置的事。你用spring-boot-starter-data-redis引入 Redis 客户端、spring-boot-starter-amqp引入 RabbitMQ自动配置基本开箱即用。换成其他框架光是把这些组件整合好就要花不少时间。第二自带 Web 容器部署方便。SpringBoot 内嵌 Tomcat一个java -jar就能启动服务。对于秒杀这种需要快速弹性扩容的系统来说意味着你可以在流量高峰期快速多启几个实例配合 Nginx 负载均衡把压力分摊到多台机器上。第三自动装配机制适合做分层架构。秒杀项目中常见的“过滤器Filter→ 拦截器Interceptor→ 业务 Service”这套链路在 SpringBoot 里的实现非常规整。你可以轻松地在拦截器里做用户登录校验、在 AOP 切面里做接口限流、在 Service 里做核心业务代码结构清晰不会变成一个大杂烩。第四社区资料丰富遇到问题能快速找到答案。这一点看似不起眼但在实际开发中非常重要。秒杀系统涉及的技术点非常多——缓存一致性、分布式锁、消息可靠性投递等。任何一个细节踩坑了你都需要快速查到解决方案。SpringBoot 的社区积淀保证了你遇到的大多数问题前人已经踩过并写下了解决方案。当然也要说明SpringBoot 不是银弹。真到了百万级并发你还需要引入更复杂的分布式组件比如分库分表中间件、分布式事务框架等。但这些都是在 SpringBoot 基础上做演进框架本身不会成为瓶颈。2.3 设计容量估算与预期效果做技术方案前先估算容量。这一步很多人会跳过直接开始写代码但我要强调——容量估算是秒杀系统设计的前置输入没有它后面所有的限流参数、线程池大小、MQ 并发数都只是瞎猜。假设我们的目标是10 万用户同时抢购 1000 件商品。10 万用户点击秒杀按钮产生的瞬时 QPS每秒请求数假设为 5 万考虑到用户点击不可能是完全同时的但最终只有 1000 人能抢到这就意味着 98% 的请求注定是“陪跑”容量设计的目标就是用 1% 的资源去处理 100% 的请求但要让 99% 的请求以极快的速度被返回失败或等待只有真正有资格的请求才进入完整链路。我做容量规划的步骤如下估算总请求量10 万人参与每人点击 1~2 次预估总请求 15~20 万确定核心接口 QPS秒杀接口瞬时峰值 5 万 QPS确定单机容量基于 SpringBoot Tomcat 默认配置单机约能支撑 1000~2000 QPS不经过特殊优化计算所需机器5 万 QPS / 每台 2000 QPS 需要约 25 台应用服务器。但如果我们用 Redis MQ 做削峰应用层实际承受的 QPS 可能只有 2000~3000只需要 2~3 台机器就够了。这就是“削峰填谷”的收益——你花了很大力气做系统设计省下的机器成本是实打实的。在实际的毕业设计或项目汇报中这个容量分析过程也是一个很好的加分项因为它体现的不是“我会用框架”而是“我理解系统为什么这样设计”。3. 核心模块设计与实现细节3.1 数据库表结构设计秒杀系统的数据库表设计相比普通商城多了几个关键考量点。我给出一个经过实际项目验证的表结构方案并解释为什么这么设计。秒杀商品表seckill_product字段名类型说明idbigint主键product_namevarchar(64)商品名称product_imagevarchar(255)商品图片地址seckill_pricedecimal(10,2)秒杀价格original_pricedecimal(10,2)原价stock_countint秒杀库存总量start_timedatetime秒杀开始时间end_timedatetime秒杀结束时间statustinyint上下架状态0下架 1上架秒杀订单表seckill_order字段名类型说明idbigint主键user_idbigint用户 IDproduct_idbigint商品 IDorder_novarchar(32)订单编号唯一索引seckill_pricedecimal(10,2)下单时的秒杀价格statustinyint订单状态0待支付 1已支付 2已取消create_timedatetime下单时间pay_timedatetime支付时间两条表设计上的关键经验第一订单表要建user_id product_id的唯一索引。这是为了防止同一个用户对同一商品重复下单。秒杀系统里经常出现用户狂点按钮的情况如果没有数据库唯一索引兜底即便应用层做了校验高并发下仍然可能插入重复订单。第二库存字段的类型用int而不是bigint并且在 SQL 层做扣减条件控制。扣减库存的 SQL 必须写成“条件更新”而不是“先查再改”这是防止超卖的最后一道防线UPDATE seckill_product SET stock_count stock_count - 1 WHERE id #{productId} AND stock_count 0;这条 SQL 的意义在于库存扣减操作本身就是原子性的加上stock_count 0条件数据库层面保证不会扣成负数。3.2 Redis 缓存设计与库存预扣减秒杀场景下Redis 的职责不仅仅是“缓存商品信息”更关键的用途是库存的预扣减。整个秒杀流程的核心思路分为三步。第一步是系统启动时把秒杀商品的库存加载到 Redis。这一步也叫“缓存预热”。通常用一个 SpringBoot 的ApplicationRunner或者定时任务实现。预热的目的是让秒杀开始后库存判断完全走 Redis而不是去查 MySQL——因为 MySQL 扛不住这个量级的读请求。第二步是用户请求到来时先在 Redis 里做库存预扣减。Redis 的扣减操作可以用Lua 脚本来实现保证原子性。这是最关键的一步。我用一段伪代码说明-- redis 秒杀库存预扣减脚本 if redis.call(get, KEYS[1]) false then return -1 -- 商品未预热 end local stock tonumber(redis.call(get, KEYS[1])) if stock 0 then return 0 -- 已售罄 end redis.call(decr, KEYS[1]) return 1 -- 扣减成功在 SpringBoot 中调用这个 Lua 脚本时要注意DefaultRedisScript的返回类型要设置为Long.class否则会有类型转换异常。这个坑我踩过这里先给你提个醒。第三步是异步发送 MQ 消息通知后端创建订单。Redis 预扣库存成功后用户端其实已经能拿到“抢购成功”的结果了但此时订单还不存在。我们要把真正耗时的 SQL 操作放到后面去执行。这里有一个非常容易想不通的问题如果 Redis 扣了库存但 MQ 消息发送失败或者订单创建失败怎么办我的方案是给秒杀商品增加一个** Redis 中的“已扣未支付”计数器**并且在 MySQL 订单表中保留“待支付”状态的记录。如果订单创建失败系统通过定时任务扫描 Redis 中已扣减但长时间未生成订单的 key做库存回补。这个补偿机制是秒杀系统的最后一道保险。3.3 库存防超卖的三种方案对比库存防超卖是秒杀系统最核心、最容易出错的地方。我在开发过程中尝试过三种方案各有优劣这里做个详细对比。方案一使用 MySQL 乐观锁UPDATE seckill_product SET stock_count stock_count - 1, version version 1 WHERE id #{productId} AND stock_count 0 AND version #{version};优点实现简单不依赖额外组件缺点乐观锁在高并发下会产生大量的更新失败重试数据库压力大适用场景低并发或内部系统方案二Redis Lua 脚本预扣减最终采用优点Redis 单线程模型天然串行化请求性能极高Lua 脚本保证原子性库存操作完全脱离数据库缺点需要处理 Redis 与 MySQL 的数据一致性引入补偿机制适用场景中等及以上并发秒杀方案三分布式锁Redisson 实现RLock lock redissonClient.getLock(seckill:product: productId); lock.lock(); try { // 查库存、扣库存、创建订单 } finally { lock.unlock(); }优点逻辑简单、容易理解彻底避免超卖缺点锁粒度太大相当于把所有请求串行化了性能上不去适用场景小规模限时抢购总请求量在千级别最终项目采用方案二Redis Lua 脚本做预扣减 MySQL 条件更新做兜底 定时任务对账补偿。这一套组合拳下来既能保证不超卖也能保证系统扛得住高并发。3.4 MQ 异步下单与流量削峰秒杀系统的另一个重要设计是异步下单。用户点击秒杀后请求在 Redis 层完成库存预扣减紧接着就要把“生成订单”这个耗时操作交给 MQ让消费者后台慢慢处理。这背后的逻辑值得展开说说。秒杀场景下如果同步创建订单一次下单操作需要校验用户、校验商品状态、扣减库存、生成订单这串数据库操作下来至少耗时 10~50 毫秒。在 5 万 QPS 的冲击下这些操作会在极短时间内耗尽数据库连接池。但如果走 MQ请求只需要向 MQ 发一条消息就立即返回耗时可能只有 1~2 毫秒。这就是削峰——把瞬间的大流量变成一段时间内的平稳流量。我用 SpringBoot 集成 RabbitMQ 时有几个关键参数需要说明。消费者的并发数要控制住。默认情况下消费者是单线程消费的。如果生产速度远大于消费速度MQ 中消息会大量堆积。合理的做法是设置消费线程池spring.rabbitmq.listener.simple.concurrency: 10最小并发消费线程数spring.rabbitmq.listener.simple.max-concurrency: 50最大并发消费线程数spring.rabbitmq.listener.simple.prefetch: 50每次预取 50 条消息prefetch这个参数容易被忽略它决定了每个消费者在向 MQ 请求消息时一次拉取多少条。prefetch设置太小消费效率低设置太大会导致单个消费者占用过多消息其他消费者空闲。对于秒杀订单这种消息体小、处理逻辑相对短的任务prefetch50是一个经验值你可以根据压测结果调整。消费者要开启手动 ACK不能使用自动确认。如果消费者在处理订单时抛异常自动确认会将消息丢弃导致订单丢失。手动 ACK try-catch 重试机制才能保证可靠消费RabbitListener(queues seckill.order.queue) public void handleSeckillOrderMessage(Message message, Channel channel) throws Exception { long deliveryTag message.getMessageProperties().getDeliveryTag(); try { String orderMsg new String(message.getBody(), StandardCharsets.UTF_8); // 解析消息执行业务逻辑 seckillOrderService.createOrderByAsync(orderMsg); // 处理成功确认消息 channel.basicAck(deliveryTag, false); } catch (Exception e) { // 处理失败拒绝消息并重新入队 channel.basicNack(deliveryTag, false, true); } }关于 MQ 的集成SpringBoot 的 starter 已经把大量样板代码简化掉了你只需要关注队列定义、交换机绑定和消费者逻辑的可靠性。3.5 接口幂等与重复下单防护秒杀系统还有一个非常隐蔽但极其重要的问题——接口幂等。什么叫幂等就是用户同一个请求发 100 次系统最终只创建 1 个订单。如果不做幂等用户在网络波动时多点几次按钮就可能产生多个重复订单。我在系统中设计了三层幂等防护第一层是前端拦截。秒杀按钮点击成功后立即置灰JavaScript 端用标志位防止重复提交。这是最基础的防护但说实话这层只能拦住普通用户拦不住技术用户他们会绕过前端直接调接口。第二层是 Redis 用户标记。用户点击秒杀接口时先判断 Redis 中是否存在seckill:user:{userId}:{productId}这个 key。如果存在直接返回“您已参与过秒杀”如果不存在用SETNX命令设置这个 key 并加上过期时间例如 5 分钟覆盖整个秒杀流程。SETNX保证原子性即使并发请求同时到达也只有一个能成功设置 key。这层防护能拦截绝大多数重复请求。第三层是数据库唯一索引。在seckill_order表中对user_id product_id建立唯一索引。这是最终兜底方案。即使前两层都没拦住数据库的约束也能保证不会产生重复订单。当插入重复订单时SQL 会抛出DuplicateKeyException我们需要在代码里捕获并转为业务异常“您已抢购过该商品”。这三层防护缺一不可前端防普通用户Redis 防高并发重复数据库做最终兜底。在压测时你会发现90% 以上的重复请求会在第二层就被轻松拦截掉每平请求不会给数据库带来任何压力。4. 接口限流与前端倒计时联动4.1 基于拦截器的接口限流实现刚才讲过削峰的整体思路这里再单独说一下接口限流的具体实现方案。秒杀系统的所有接口本质上都可以被脚本刷所以限流不是“要不要做”的问题而是“怎么做”的问题。我使用的是 Google Guava 的RateLimiter做单机限流配合 SpringBoot 拦截器实现。GUAVA 的RateLimiter是令牌桶算法的实现它的特点是允许一定的突发流量非常适合秒杀这种瞬间流量集中的场景。实现一个简单的拦截器Component public class RateLimitInterceptor implements HandlerInterceptor { // 每秒钟放行 1000 个请求 private final RateLimiter rateLimiter RateLimiter.create(1000); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!rateLimiter.tryAcquire()) { response.setStatus(429); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:500, \msg\:\系统繁忙请稍后再试\}); return false; } return true; } }RateLimiter.create(1000)表示每秒最多放行 1000 个请求。超过这个数字请求会直接返回“系统繁忙”的提示不再进入业务逻辑。单机限流的缺点是它不是全局的——如果你部署了 5 台机器每台机器限流 1000总吞吐就是 5000。但秒杀系统通常配合 Nginx 负载均衡使用单机限流反而是一个简单好用的方案。如果要全局限流可以考虑 Redis 的INCR 过期时间实现或者接入 Sentinel 这类流量治理组件但复杂度会显著上升。秒杀开始前的热点问题更值得重视。很多人会在秒杀开始前 10 分钟就疯狂刷新页面这些请求同样会消耗系统资源。所以对于商品详情查询接口我也要做限流——可以接受较低的 QPS因为用户刷新的目的只是看到商品信息而不是抢购。4.2 秒杀倒计时与状态联动设计秒杀系统的前端部分有一个涉及前后端联动的核心问题——倒计时。倒计时的准确性决定了秒杀开关的时机但前端时间并不可靠因为用户本地时钟可能被修改或者与服务器时间存在偏差。正确的做法是前端不直接使用本机时间而是以服务器时间作为基准。具体实现如下页面加载时前端调用后端接口获取服务器当前时间serverTime前端计算本地时间与服务器时间的差值offset serverTime - localTime倒计时用localTime offset来驱动倒计时归零时立即将秒杀按钮从置灰改为可点击并向后端发起秒杀请求。这个方案能解决 99% 的时钟不一致问题。但还有一个必须处理的情况服务器时间到达秒杀开始时间但前端网络延迟导致倒计时尚未归零。这种情况下用户点击按钮时后端也要判断秒杀是否已开始。所以后端接口在业务逻辑之前必须校验if (LocalDateTime.now().isBefore(seckillProduct.getStartTime())) { return Result.error(秒杀尚未开始); } if (LocalDateTime.now().isAfter(seckillProduct.getEndTime())) { return Result.error(秒杀已结束); }有同学问“这岂不是要把服务器时间校验也做了”对后端必须做主校验。前端倒计时的作用只是“用户体验”后端的校验才是最终防线。如果秒杀还没开始就有请求打进来说明有人绕过了前端直接攻击接口此时应该在拦截器层直接拒绝而不是放行到业务逻辑里再判断。5. 常见问题与排查技巧实录5.1 超卖问题与死锁问题的完整排查在做秒杀系统压测时我遇到过两个非常典型的问题这里记录完整的排查过程。问题一并发下库存出现负数第一次压测时我用 JMeter 模拟 5000 并发用户同时抢购 100 件商品。压测结束后我查数据库发现stock_count变成了 -23。这是一个典型的超卖事故——库存被扣了 123 件但只有 100 件应该被卖出去。排查思路是从代码层开始。最初我的业务逻辑是“先查询库存再判断库存是否足够最后执行扣减”SeckillProduct product seckillProductMapper.selectById(productId); if (product.getStockCount() 0) { seckillProductMapper.reduceStock(productId); }这个逻辑在并发环境下是必然超卖的。两个线程同时查询到库存为 1都判断stock_count 0成立然后都执行了扣减。问题就出在“查询和更新”不是原子操作中间没有任何锁机制来阻止并发。解决方案就是前面提过的将扣减 SQL 改为条件更新并在 Java 代码里检查更新影响的行数。int rows seckillProductMapper.reduceStockByCondition(productId); if (rows 0) { // 库存不足返回失败 return Result.error(已售罄); }这样改完之后即使 5000 个并发请求同时执行这条 SQL数据库也会通过行锁让它们排队执行并且只有库存大于 0 时更新才会成功。最终压测结果是库存从 100 减到 0没有再变成负数。问题二高并发更新同一行导致死锁解决超卖后我又遇到了新问题——压测日志里频繁出现Deadlock found when trying to get lock异常。原因是秒杀商品行是被所有请求同时更新的对象多个事务并发修改同一行时MySQL 的 InnoDB 行锁机制在特定情况下会检测到死锁并回滚其中一个事务。这事的根源还是“小流量没关系高并发问题全暴露了”。解决思路是控制数据库的并发度。我的做法是将核心扣减库存操作与订单创建操作拆分到不同事务中减少事务持锁时间在扣库存时使用 Redis 预扣减让真正走 MySQL 扣减库存的请求大幅减少消费者处理 MQ 消息时限制线程池并发为 10~20避免大量线程同时争抢同一行记录。调整后将压测规模提升到 1 万并发数据库死锁异常消失事务成功率从 96.3% 提升到 99.97%。这个提升效果非常明显也说明秒杀系统的问题往往不在于单条 SQL 的性能而在于并发执行时彼此之间的竞争关系。5.2 Redis 缓存与数据库一致性保障秒杀系统中一个让很多人头疼的问题是Redis 里的库存和 MySQL 里的库存怎么保证最终一致先说我的结论秒杀系统不追求强一致只追求最终一致。强一致需要引入分布式事务成本和复杂度都太高对秒杀这种短期、高频、最终结果明确要么成功要么失败的场景不划算。我使用的方案是这样的秒杀开始前把商品库存加载到 RedisMySQL 中的库存作为“账本”保留不变秒杀进行中所有请求只操作 Redis 库存不碰 MySQL 库存秒杀结束后通过对比 Redis 中剩余库存和 MySQL 中剩余库存对账并修正。具体做法是从 MySQL 中查出商品的初始库存totalStock从 Redis 中查出已扣减数量soldCount每次预扣减时用INCR记录到单独 key如果totalStock - soldCount 0说明出现了超卖需要回滚库存并人工介入如果一切正常将最终剩余库存回写到 MySQL 的stock_count字段。这种对账机制通常是定时任务执行的。对于毕业设计或小型项目每 5 分钟执行一次对账已经足够对于商用的秒杀系统可以缩短到 30 秒甚至秒级。我还要强调一个细节不要把“Redis 库存扣没了”当作订单一定成功的标志。Redis 只负责控制“能不能参与秒杀”真正生成订单是在 MQ 消费者里异步进行的。如果消费者处理失败Redis 库存已经被扣减了这时存量差就会在对账任务中被发现并自动回补。这是整个系统的兜底机制。5.3 压测数据与系统性能调优实录项目完成后我用 JMeter 做了一轮完整的压力测试这里分享压测方案和调优过程方便你复现和对比。压测环境服务器4 核 8G 内存数据库MySQL 8.0与 SpringBoot 同机部署Redis6.x单实例压测工具JMeter 5.x压测规模5000 并发线程持续 60 秒第一轮压测未优化指标数据TPS每秒事务数约 820平均响应时间680ms95% 响应时间1.4s错误率11.5%数据不太理想。错误主要是数据库连接池耗尽。Tomcat 默认最大连接数 100支持不了这个并发量。同时日志里大量出现 MySQL 连接超时。第一轮优化调整线程池和连接池配置# Tomcat 线程池配置 server.tomcat.threads.max500 server.tomcat.threads.min-spare50 # 数据库连接池 spring.datasource.hikari.maximum-pool-size60 spring.datasource.hikari.minimum-idle20 spring.datasource.hikari.connection-timeout3000第二轮压测调整后指标数据TPS每秒事务数约 1300平均响应时间420ms95% 响应时间920ms错误率3.2%TPS 提升明显但错误率还是略高。排查发现是spring.datasource.hikari.connection-timeout3000太短。在高并发下部分请求等待连接超过 3 秒被直接丢弃。这里要说明一个经验连接池超时时间不宜设置太短否则偶尔的慢查询会放大为大量请求失败。第二轮优化引入 Redis MQ架构优化这就是前面一直在讲的架构方案。Redis 过滤 MQ 削峰后压测数据发生了质变指标数据TPS每秒事务数约 4600平均响应时间85ms95% 响应时间180ms错误率0.35%MySQL 单库峰值 TPS约 300看到这个数据你应该能理解为什么秒杀系统要费那么多心思去设计 Redis 和 MQ 的协作最终打到 MySQL 的 TPS 从 1300 降到了 300但系统整体的 TPS 反而翻了几倍响应时间大幅缩短。这就是削峰填谷的最直接效果。调优过程中还有一个容易忽略的操作系统的参数——文件句柄数。如果压测时出现大量Too many open files错误需要调整 Linux 系统层面的限制ulimit -n 65535否则不管代码怎么优化这个上限都会成为瓶颈。5.4 开发周期中容易踩的坑清单项目做完后我复盘了整个开发过程把踩过的坑整理成清单给后来者参考。坑点出现场景解决方案Redis 序列化乱码存入 Redis 的对象或键值显示为\xAC\xED\x00\x05t...将RedisTemplate的默认序列化器从 JDK 序列化改为 Jackson JSON 序列化Redis 预扣库存回补不及时用户抢到但订单未支付设置支付过期时间如 5 分钟超时后回补库存并取消订单秒杀接口被人用脚本刷商品刚开售就被极短时间内抢光增加图形验证码或要求用户先互动获取秒杀令牌MQ 消息大量积压消费者处理速度小于生产速度增加消费线程并发数优化订单创建 SQL 索引秒杀结束按钮不及时置灰前端仍显示可点击、点击返回“已结束”后端主动推送 WebSocket 消息通知所有客户端秒杀结束SpringBoot 热更新不生效修改代码后页面不刷新引入spring-boot-devtools并配置 IDEA 的自动编译这些坑都不算技术难点但每一个都能在关键时刻“卡住”你几个小时。提前了解能省很多时间。6. 项目完整效果与实际运行表现6.1 秒杀链路关键节点的运行表现整套系统搭建完成后我用真实场景跑了一轮完整的秒杀流程。商品信息提前在后台录入秒杀时间设置为启动后 5 分钟。我人为制造了 5000 个并发请求的访问压力观察各个环节的运行表现。环节一倒计时阶段秒杀开始前 2 分钟这阶段前端有 5000 个用户不断刷新页面。每次刷新都会请求商品详情接口。由于详情接口没有走 Redis 缓存而是直接查数据库压力测试显示这个简单接口的响应时间在持续攀升。我意识到必须给详情接口也加缓存。后来我把商品信息在 Redis 中缓存了一份用StringRedisTemplate保存 JSON 字符串。缓存加好后这个接口的响应时间稳定在 10ms 以内。环节二秒杀瞬间开始前 0~5 秒这个阶段是整个系统的最大考验。5000 个用户同时点击秒杀按钮。由于我在拦截器层做了限流加上 Redis Lua 脚本预扣库存实际进入业务逻辑的请求远小于 5000。观察 Redis 监控面板库存从 1000 快速递减到 0整个过程不到 2 秒。Redis 单实例的 QPS 峰值到了 18500没有出现连接失败说明 Redis 完全扛得住这个流量。环节三订单异步创建秒杀开始后 2~30 秒MQ 消费者以每秒约 20 条消息的速度处理订单创建。先不评价这个速度快慢——故意做慢是为了防止数据库被打爆。事实上有 90% 以上的用户根本没被发送到订单创建环节因为他们没有抢到库存。真正走到这里的请求只有 1000 个。对于消费者来说处理 1000 条消息只需要 50 秒。用户视角的体验是点击按钮后立即显示“抢购成功”2~3 秒后可以在“我的订单”里看到待支付订单。整体体验完全可以接受。从这轮运行表现来看一个核心结论再次被验证系统的性能峰值不是由“最快环节”决定的而是由“最慢环节”决定的。把最慢的环节数据库写入与最快的环节Redis 扣库存分离开系统的整体性能会得到数量级的提升。6.2 复盘这套方案还能怎么演进项目完成后我经常被问到这套方案能支撑多大流量什么情况下要变成别的架构我的回答是当前的架构方案SpringBoot Redis MQ能稳定支撑单商品 10 万级并发。如果需求变成 100 万级并发或者出现多个商品同时秒杀的情况有几个需要演进的方向Redis 集群化单实例 Redis 在 10 万 QPS 下会接近极限需要扩展为 Redis Cluster 或 Codis让多个 Redis 节点分担读写压力MQ 从 RabbitMQ 升级为 KafkaRabbitMQ 在单机 1 万 消息/秒的吞吐下表现不错但 Kafka 在百万级消息/秒的吞吐能力上有明显优势数据库分库分表订单量积累到单表千万级后需要按用户 ID 或订单 ID 做水平拆分引入 ShardingSphere 或 MyCat 中间件引入更完整的秒杀令牌机制提前发放秒杀令牌只在令牌有效期内允许参与秒杀进一步压低瞬间请求量。但你要清楚这些演进的核心思想并没有变削峰填谷、层层拦截、Redis 扛读写、MQ 做异步。框架和中间件可以换设计思想是通用的。7. 个人经验总结与建议最后再分享一些我的实际操作体会。这套秒杀系统写下来最大的收获不是学会了一堆工具和框架的用法而是建立了一套“面对高并发场景如何思考和拆解问题”的方法论。第一点体会技术方案永远是围绕业务约束设计的。秒杀系统的所有复杂性都来源于一个最朴素的业务需求1000 件商品的库存不能卖成 1001 件。这个约束意味着我们不能图简单而放弃正确性。任何技术在正确性面前都必须让步——这就是为什么我们要在 Redis、MQ、MySQL 三层都设置防线而不是赌其中某一层不会出问题。第二点体会没有压测的秒杀系统不值得上线。我在项目开发中反复强调压测的重要性。没有压测你就永远不知道自己的系统在 5000 并发下会产生多少个死锁、多少次超时、多少条错误日志。纸上谈兵的设计与实际跑起来的表现之间差距巨大。用 JMeter 或者 wrk 做一轮压测你会看到系统最真实的一面。第三点体会SpringBoot 让你很快搭出系统但决定系统水平的永远是底层细节。框架帮你解决了“连接 Redis”“发消息到 MQ”这种样板代码但解决不了“库存会不会超卖”“订单会不会丢失”“消息会不会积压”这些真正致命的问题。使用 SpringBoot 的姿势应该是把它当成一个高效的工具箱聚焦点永远在业务逻辑和系统设计上。如果你现在也在做秒杀系统或者准备把它作为毕业设计题目我的建议是不要一开始就追求面面俱到的架构先把最核心的“秒杀接口响应 库存不超卖”跑通再逐步加上 Redis 预扣减、MQ 异步下单、接口限流这些进阶设计。每一步做完都用压测验证效果。这样你在最终交付时对系统里每一个模块的取舍都能讲出背后的原因——这在任何技术评审或答辩中都是很大的加分项。