资讯动态

秒杀系统高并发架构设计与实践:Redis预减库存、消息队列削峰、限流防刷全解析

发布时间:2026/10/24 4:49:43 来源:尧图企业网站定制
开业那天我们团队预计最多只有两万人会同时来抢一千台机器结果第一波请求进来的瞬间监控大屏上QPS直接冲破了六位数数据库连接池被打满订单表锁死一堆用户卡在支付页面上反复转圈。那次活动之后我们花了两周时间重新设计了整套秒杀链路。这篇文章就把这套方案的思路、代码实现和踩坑记录完整梳理一遍给同样在做秒杀系统的朋友一个参考。秒杀系统的核心问题不是“抢不到”而是“扛不住”。真正的难点在于商品库存可能只有100件但请求量是千万级的而且绝大多数请求都是无效的。系统要做的不是让每个人都成功而是保证在巨大流量冲击下能够准确识别那100个真正能买到的人同时不让数据库和其他依赖被拖垮。1. 秒杀系统整体设计思路1.1 秒杀场景的本质矛盾秒杀系统最核心的特征是严重的供需不平衡前端用户规模极大后端目标库存极少。这种不平衡会导致三个典型问题。第一是瞬时高并发流量。秒杀开始那一秒钟大量用户同时发起请求流量曲线是脉冲式的峰值可能是平时运营活动的几十倍甚至上百倍。这个峰值持续时间往往只有几秒到十几秒但就是这几秒足以压垮没有准备的系统。第二是无效请求比例极高。1000个人抢20件商品至少980个人注定失败。如果系统让这980个请求都走到数据库层等于白浪费计算和IO资源。一个合格秒杀系统的重要设计目标就是在距离数据库最远的地方把绝大多数无效流量拦截掉。第三是业务规则与数据一致性之间的冲突。秒杀虽然要求高并发但业务规则依然不能含糊不能超卖、一个用户通常限购一件、库存扣减和订单生成必须一致。高并发下的数据一致性正是秒杀系统最难处理的环节。1.2 设计原则漏斗模型与逐层过滤我的核心设计思路可以概括成一个漏斗模型在请求到达数据库之前通过四个层次逐级削减流量每个层次只做自己最擅长的事情。第一层是浏览器和客户端层负责页面静态化与CDN加速。秒杀页面的主体内容是静态的商品图片、描述、样式这些几乎不变化的内容全部放到CDN用户访问根本不经过应用服务器。真正做到动态的只有一个秒杀按钮的状态和最终的抢购结果。第二层是接入层负责负载均衡、限流和黑了名单拦截。Nginx或网关层通过识别用户身份、请求频率和访问特征直接拦截掉大部分机器人脚本和羊毛党请求剩下的流量再放行进入应用层。第三层是应用层负责资格校验、Redis库存预减和消息队列写入。这一层是秒杀系统最核心的业务逻辑所在承担了绝大部分请求处理。经过这一层流量已经被削减到一个非常低的数量级。第四层是异步处理层负责真正创建订单与扣减数据库库存。因为前面的层已经锁定了购买资格这一层并不需要处理大量请求只需要串行消化队列里的消息即可。按照这个模型假设整体请求量是100万经过层层过滤之后真正会打到数据库层的可能只有几百个这比让100万请求直接怼到数据库上要可靠得多。1.3 我最终采用的系统模块划分基于上面的漏斗原则我最终把系统拆成了六个模块。接入网关负责限流、防刷、IP与用户维度控制对恶意请求直接拒绝。秒杀应用服务执行业务校验、Redis库存扣减、消息发送是请求处理的枢纽。Redis集群承担库存预减、用户去重标记、临时资格记录等高频数据操作。消息队列集群缓冲秒杀请求实现下单流程的削峰填谷。订单服务异步消费消息创建订单、扣减数据库库存处理最终一致性。数据库主库只承接正常业务流量是最终数据的唯一权威来源。这个模块划分的出发点是让每一层都做自己最擅长的事避免跨层调用。比如Redis负责高速计数MQ负责缓冲削峰数据库负责最终落盘。任何模块都尽量不要同时承担两件性质差异很大的任务出了问题也好定位。2. 核心技术点拆解与原理分析2.1 页面静态化与动静分离秒杀页面本身可以用静态页面承载商品信息、文案、样式等都是提前生成好的HTML文件发到CDN即可。这样用户在秒杀开始前疯狂刷新页面命中的都是CDN节点不会对源站产生任何压力。秒杀页面的动态部分主要集中在三个地方倒计时状态、秒杀按钮是否可点击、结果的胜利或失败提示。这些动态信息的更新频率很低完全没有必要让前端每秒去请求后端接口可以让前端通过一个低频轮询接口统一获取状态比如每3到5秒取一次而不是每次点击都触发全量请求。静态化的关键操作是让秒杀按钮在活动开始前处于置灰状态点击无效且不发请求。这个看似简单的设计可以拦截掉大量提前刷接口的流量因为很多用户和脚本会在正式开始前就疯狂点击按钮如果按钮本身在客户端就不允许点击这部分流量根本不会到达服务器。2.2 接口防刷与用户维度限流很多人觉得限流就是限制总QPS实际上对于秒杀来说用户维度的限流比全局限流更重要。因为全局限流只是堵住了一个入口但一个真正恶意用户可以通过变换请求报文、使用多台机器来绕过。用户维度的限流可以保证同一个账号在活动开始前无法提前刷接口在活动开始后也只能触发有限次数的请求。我在网关层做了三个维度的控制。首先是用户级令牌桶限流每个用户令牌桶容量设为5个每秒补充1个令牌。用户每个请求需要取一个令牌这样即使脚本以每秒几十次的频率狂点也只能有5个请求真正进入后端其余直接拒绝。其次是 RequestId 防重。前端每次秒杀点击生成一个全局唯一的ID后端Redis以这个ID作为Key进行去重相同ID的请求第二次到达直接返回已处理。这样同一用户手滑多点几次按钮也只会有一个有效请求。最后是黑名单与行为识别。同一个IP下关联的账号数过多、请求间隔过于规律、User-Agent异常等特征都会被识别出来自动加入黑名单拦截效果比单纯限IP好很多。提示限流必须放在最前面的网关层而且最好采用分布式限流方案不能用单机内存限流。否则一台机器只能看到自己承接的流量全局限住了换个IP又打到别的机器上拦截效果就失效了。2.3 Redis预减库存与原子性保障库存扣减是整个秒杀链路中最关键的一环。很多人第一次做秒杀直接在数据库里做库存判断和扣减这是一个非常危险的方案。数据库行锁会让大量请求排队等待同一行记录一台普通数据库每秒能处理的update事务量大概只有几百到一千一旦请求量超过这个阈值数据库连接就会被打满活动直接瘫痪。正确做法是在Redis层完成库存预减只有预减成功的用户才有资格进入下一步。Redis单线程执行命令天然具备原子性而且内存操作性能极高单节点每秒可以轻松处理数万次请求。这里要强调一个细节Redis库存预减一定要用Lua脚本来执行把“检查库存是否充足”和“扣减库存”两个操作放进同一个原子操作中。如果分开两步执行在高并发下会出现“检查时有库存扣减时库存已经被其他请求减掉”的竞态导致超卖。2.4 消息队列削峰与异步下单经过库存预减之后能够继续往下走的请求数量就已经很小了。理论上既然Redis已经扣减了库存就可以直接在同步链路中创建订单。但同步创建订单有几个问题一是下单涉及数据库写入、分布式事务、库存再次核对等操作耗时长用户等待时间会集中在十几毫秒的请求响应上体验不好二是一旦订单服务出现短暂抖动整个请求链路都会阻塞。我最终选择了异步下单方案。Redis预减库存成功后秒杀应用只做一件事情把包含用户ID、商品ID、秒杀场次ID的消息写入消息队列然后立即返回“抢购中请稍候查看结果”给前端。真正创建订单的消费者服务从队列中拉取消息执行数据库事务。异步化带来的直接好处是请求处理时长大幅缩短应用服务的吞吐能力成倍提升。秒杀活动峰值大概是几万QPS写入MQ的能力可以轻松扛住而消费者端可以根据数据库的处理能力调节消费速率实现削峰填谷。数据库永远只承受可控的写入压力不会再被瞬时峰值击穿。2.5 数据库层最终一致性保障Redis预减库存只是“锁定资格”真正的一致性还要靠数据库层来保证。订单消费者收到消息后需要在数据库事务里执行三个操作插入订单记录、扣减商品库存、记录用户秒杀明细。这三个操作必须放在同一个数据库事务中要么全部成功要么全部回滚。订单创建成功后数据库库存成功扣减库存记录里扣减的数量必须与Redis预减的数量保持一致。如果订单创建失败比如用户抽奖信息不完整、商品状态异常则需要通知Redis回补库存把预减的额度还回去同时记录一条失败日志方便追踪。这里特别容易出现一种错误做法直接在事务中先查库存再更新库存。实际上秒杀系统在数据库层不应该再“查库存判断是否超卖”因为库存预减阶段的判断已经做了数据库层只需要以Redis预减成功的消息为准直接扣减库存即可。如果每个订单事务都再查一次库存那数据库压力又会上来失去了预减的意义。3. 核心代码实现与关键步骤3.1 秒杀请求的完整处理流程我先梳理一下一个用户从点击秒杀按钮到最终看到订单结果整个链路经历哪些状态流转。这个流程图很关键所有代码都是围绕它写的。用户点击按钮前端生成RequestId请求进入网关。网关层检查用户令牌桶、RequestId去重、黑名单判断通过后放行否则直接返回失败。请求进入秒杀应用服务先校验活动时间窗口不在有效期内直接拒绝。应用服务执行用户维度的限购检查同一个用户同一个活动只能有一个有效订单防止重复抢购。执行Redis Lua脚本原子性完成“检查库存扣减库存记录用户已抢标记”。如果脚本执行成功将一条秒杀成功消息写入MQ返回“抢购中”提示给前端。订单消费者拉取消息开启数据库事务创建订单、扣减库存、记录明细提交事务。如果第7步成功更新Redis中的标记状态为“已下单”通知前端展示成功结果。如果第7步失败回补Redis库存清除用户标记通知前端展示失败原因。第4步和第5步之间要特别注意一个时序问题先检查限购再扣减库存这两个操作必须在一个Redis事务或者Lua脚本里完成否则高并发下同一个用户可能穿过检查同时发起两个扣减请求导致用户创建了两条订单记录。3.2 库存预减Lua脚本的实现Redis库存预减我用了一段Lua脚本这段脚本是秒杀系统的核心直接决定了超卖问题会不会出现。-- KEYS[1] 库存key例如 seckill:stock:1001 -- KEYS[2] 用户去重key例如 seckill:user:1001 -- ARGV[1] 用户ID -- ARGV[2] 商品ID local stock tonumber(redis.call(get, KEYS[1]) or 0) if stock 0 then return 0 end local isExist redis.call(sismember, KEYS[2], ARGV[1]) if isExist 1 then return -1 end redis.call(decr, KEYS[1]) -- SADD操作比较慢在高并发下可以先放到后面处理但这里简单起见直接执行 redis.call(sadd, KEYS[2], ARGV[1]) return 1这段脚本通过一个原子操作完成了三件事检查库存是否足够、检查用户是否已参与、扣减库存并记录用户标记。脚本中库存为0返回0用户已抢购返回-1成功返回1。这里要提醒一个很有迷惑性的坑用 DECR 命令扣减库存很多人喜欢在脚本执行前先去 GET 一下库存发现大于0再 DECR。这个做法在Lua脚本外部是错误的因为Redis单线程执行是一条命令一条命令地执行不在脚本内的检查与扣减之间存在时间窗口并发场景下会被钻空子。正确的做法是把检查和扣减都放进同一个Lua脚本让Redis保证脚本内部的原子性。Redis初始化的时候需要把活动库存提前写入。SET seckill:stock:1001 100这里100是活动商品的可售库存。我通常会把真正库存放大一点预留缓冲比如可售库存95Redis初始值100留5个作为异常回补的缓冲避免各种异常导致数据库库存扣减和Redis不一致时出现负数。3.3 秒杀消息的生成与消费应用服务在Lua脚本返回成功后立刻把消息写入MQ。这块我用了一套比较轻量的实现核心代码如下。// 秒杀请求处理的Service方法 public SeckillResult handleSeckill(Long userId, Long productId, String requestId) { // 前置校验活动时间、用户状态等 SeckillActivity activity seckillActivityMapper.getById(productId); if (activity.getStartTime().isAfter(LocalDateTime.now())) { return SeckillResult.fail(StatusCode.NOT_START); } // 执行库存预减 Long result redisTemplate.execute(seckillLuaScript, Arrays.asList(seckill:stock: productId, seckill:user: productId), userId.toString(), productId.toString()); int code result.intValue(); if (code -1) { return SeckillResult.fail(StatusCode.REPEAT); } if (code 0) { return SeckillResult.fail(StatusCode.SOLD_OUT); } // 异步发送下单消息 SeckillMessage message new SeckillMessage(userId, productId, requestId); rabbitTemplate.convertAndSend(seckill.order.exchange, seckill.order, message); return SeckillResult.success(null, StatusCode.PROCESSING); }消费者端处理的核心逻辑放在一个事务方法里。这个方法设计上有一个重要原则事务要尽可能短小不要在事务里调用外部HTTP接口不要在事务里执行复杂的计算确保数据库连接持有时间尽可能短。消费者代码简化后大概是下面这个样子。RabbitListener(queues seckill.order.queue) public void onMessage(SeckillMessage message) { try { createOrder(message); } catch (Exception e) { // 记录失败消息并回补库存 log.error(创建订单失败回补库存:, e); revertStock(message); } } Transactional(rollbackFor Exception.class) public void createOrder(SeckillMessage message) { // 幂等检查防止消息重复消费 if (orderMapper.existsByRequestId(message.getRequestId())) { return; } // 创建订单 SeckillOrder order new SeckillOrder(); order.setUserId(message.getUserId()); order.setProductId(message.getProductId()); order.setRequestId(message.getRequestId()); order.setStatus(0); orderMapper.insert(order); // 扣减数据库库存只做更新不做检查 int count seckillStockMapper.cutStock(message.getProductId()); if (count 0) { throw new RuntimeException(数据库库存扣减失败); } // 写用户秒杀明细 SeckillDetail detail new SeckillDetail(); detail.setOrderId(order.getId()); detail.setUserId(message.getUserId()); detail.setProductId(message.getProductId()); seckillDetailMapper.insert(detail); }这里最值得注意的就是幂等检查。MQ的重试机制可能导致同一条消息被消费两次如果没有这个检查用户会收到两条订单。幂等健用 RequestId它在用户点击时生成整个生命周期中唯一正好适合充当这个角色。3.4 订单与库存表的核心设计数据库表设计也要为秒杀场景做专门优化。订单表、库存表、明细表三张表是基础但我在实际项目中额外增加了两个关键字段。订单表的核心字段包括ID、用户ID、商品ID、秒杀场次ID、订单状态、创建时间、RequestId唯一键。RequestId上必须建唯一索引这是幂等判断的兜底方案即使代码逻辑漏了检查数据库唯一索引也能拦住重复插入。库存表的设计比较特殊不推荐使用“剩余库存”字段而是使用“总库存”和“已扣减数量”两个字段。这样每次扣减使用UPDATE seckill_stock SET deducted_count deducted_count 1 WHERE product_id ? AND deducted_count 0的方式避免多线程下读改写导致的丢失更新问题。发现deducted_count超过总库存说明出现了超卖马上报警但数据不会被覆盖成负数排查起来也容易。库存表还应该有一个“冻结库存”字段。当订单创建成功但用户迟迟未支付时这部分库存应该被冻结不能释放给后续用户。等订单超时未支付再通过定时任务解冻这样能防止一人抢多件商品却只支付一件的情况。这个字段在活动高峰期尤其重要可以配合状态机准确追踪每件商品与订单的对应关系。3.5 库存表数据库行锁优化数据库库存扣减是高频更新操作最容易引发行锁竞争和死锁。秒杀活动期间所有扣减操作全部集中到同一个商品的库存行上如果这张表存在大量无关索引、表数据过大锁粒度就会扩大导致数据库性能下降。我的做法是给秒杀库存表做精简只保留必要的字段和索引并调整InnoDB行锁粒度到最小。更重要的优化是拆分库存行。如果单个商品库存是1000件把它拆成10行每行100件在Redis预减时对商品做一次随机分片路由让不同请求命中不同的库存行。这样数据库层的行锁就分散到了10个行上并发能力直接提高一个量级。这个优化对数据一致性没有影响只要汇总所有行的deducted_count之和不超过总库存即可。这个分片思路也可以用到Redis层。如果Redis库存Key集中在单个节点上单个Redis实例的CPU会被打满成为瓶颈。我之后会讲热Key问题这里先提一个解法把库存Key拆成多个分片在应用层随机路由请求均匀打散在多台Redis节点上。3.6 订单超时未支付的自动处理秒杀订单通常有支付时限比如15分钟或30分钟。用户抢到资格后如果超时未支付库存需要释放资格也要重新分配给后续用户。这个流程我用延迟队列实现比定时扫描全表高效得多。方案是订单创建成功后立即投递一条延迟消息到MQ延迟时间为订单有效时长。消费者收到延迟消息后查询订单状态如果还是未支付就把订单状态改为已取消同时恢复数据库库存并把Redis中该用户的抢购标记移除允许用户再次参与后续场次。延迟消息不能依赖简单的定时任务扫表否则订单量大时每一轮扫描都是对数据库的全量压力。我用的方案在测试中基本不会产生无效数据库扫描只有确实超时的订单才会触发消费。4. 实测过程中的常见问题与解决实录4.1 超卖问题是怎么发生的第一次压测的时候我们还没有用Lua脚本而是用了一段不严谨的Java代码先查Redis库存大于0再执行decr。结果压测刚跑到第10秒数据库库存字段就出现了负数。这个问题是典型的并发竞态。两个请求同时查到库存为1都认为还有库存然后都执行decr库存就变成了-1。在单个Redis实例中如果没有把检查与扣减合并成原子操作GET和DECR之间永远存在时间窗口并发越高穿透率越高。解决方案就是在Lua脚本内完成检查与扣减。这里还想多强调一句很多人觉得用分布式锁也能解决超卖把库存扣减包在锁里确实能保证不超卖但分布式锁的加锁和释放锁本身有网络开销锁竞争严重时性能会急剧下降。用Lua脚本是最贴合Redis运行模型的做法原子性由Redis内部保证不需要额外的锁协调。4.2 Redis热Key导致的节点打满秒杀活动的流量天然高度集中如果所有库存都在同一个Key里这个Key对应的Redis节点会承受极高的访问压力。我们的压测环境中单Key的访问量到达每秒20万次时Redis节点CPU直接飙到99%命令响应延迟从0.2毫秒涨到近100毫秒整个Redis集群都开始出现连锁超时问题。解决思路就是拆Key。把原来的seckill:stock:1001拆成100个分片Key每个分片Key只承载总库存的百分之一应用层根据用户ID哈希路由到其中某个分片。这样单个Key的访问量变成原来的百分之一Redis节点的压力自然降下来了。同理用户去重标记seckill:user:1001也会成为热Key因为秒杀用户群非常集中。我把这个Set集合也做了分片按照用户ID取模拆成多个小集合。每次用户请求只需要在对应的分片中查询自己是否已抢购正确性完全不受影响。4.3 消息积压与重复消费处理异步下单模式下最怕的就是消费者处理不过来导致消息越堆越多。我们经历过一次订单服务发布时旧代码有Bug消费者一启动就抛异常消息疯狂重试队列里积压了上百万条消息。等修复完Bug积压消息全部涌到数据库直接打满了连接池。这次事故之后我给消费逻辑加了三重保险。第一消费者失败重试次数限制在3次以内超过3次直接转入死信队列不再阻塞主队列消费。积压不是最可怕的最可怕的是消息卡在队列头反复重试后续所有消息全部被阻塞。第二消费者端开启批量消费模式每次拉取一批消息合成一个批处理事务。批量消费可以大幅提升数据库写入效率从单条插入改为批量插入。批量大小要测试确定太大反而会因为事务过长引发锁竞争。第三单独设置死信队列的消费者处理那些重试耗尽的消息。死信队列的性能要求不高可以慢慢清理但绝不阻塞主流程。这样即使出现异常问题消息也会被隔离起来不会影响正常用户下单。4.4 数据库连接池被打满的症状与解决数据库连接池被打满是压测中最常见的事故表现。典型的症状是应用日志大量出现HikariPool-1 - Connection is not available, request timed out数据库监控显示当前活跃连接数达到上限其他非秒杀业务也一起变慢因为所有业务共用同一个连接池。我做的优化有两步。第一步是分离数据源把秒杀订单服务和常规业务服务各用独立的数据库连接池这样秒杀流量再大也只能影响秒杀业务不会拖垮其他核心接口。第二步是控制消费速率用MQ的消费并发度作为数据库写入的限流阀比如设置消费者线程池核心线程数为20每个线程处理一条消息时只持有一个数据库连接那连接池上限定在50左右就够了绝不会出现几百个线程抢几十个连接的情况。很多团队把连接池调大来解决问题这是一个误区。连接池并不是越大越好每个连接背后都是一条数据库会话连接过多反而会让数据库性能下降锁等待和上下文切换都会增加。正确思路是压小连接池用MQ的消费并发来控制压力。我在生产环境把连接池从初始的200调整到了50数据库反而更稳了性能监控明显好转。4.5 压测应该怎么测才能暴露问题秒杀系统的压测不是简单用一个工具压接口就完事我总结了几条经验。第一必须从入口到出口全链路压测。只压秒杀应用层不压MQ消费者和数据库等于没有压。很多问题只有在数据库写入层才能暴露比如索引问题、锁竞争、事务性能。全链路压测能真实反映整个系统的吞吐能力。第二要模拟“流量尖峰”而不是平均流量。秒杀流量的特点是前几秒瞬间冲高测试脚本可以设计成前10秒每秒10000请求后续10秒降到5000再之后降到2000。这个峰值段最能暴露连接池和线程池的短板。第三压测完成后要复盘限流拦截率。压测报告里不仅要有成功请求数还要有拦截数、限流数、降级数。一般秒杀系统的有效穿透率应该在5%以下如果穿透率太高说明前面的漏斗过滤没有生效需要去查网关配置和Lua脚本逻辑。4.6 缓存不一致问题的处理方案Redis库存与数据库库存的不一致是秒杀系统中一个绕不开的问题。在预减库存阶段Redis扣减的数量代表“锁定资格”的数量数据库扣减的数量代表“实际成交”的数量。正常情况下一段时间后两者应该趋于一致但出现订单创建失败需要回补库存时就可能出现Redis库存恢复后数据库库存仍然没有扣减的情况。我的处理策略是把Redis视为“前置缓存”数据库视为“事实来源”。活动结束后以数据库记录为准做一次对账生成差异报告人工确认后调整Redis。日常运行时通过定时任务监控Redis已扣减数与数据库已扣减数的差差值超过阈值就报警。这套对账机制虽然不能实时保证完全一致但能保证数据只会暂时不一致绝不会永久错误。对账脚本的粒度可以细化除了总量对账还要按用户维度对账。确保每个用户最多只有一个有效订单、每个订单都能找到对应的库存扣减记录、每个被回补库存的用户确实没有成功下单。这些维度都对齐了整套系统的一致性就足够可靠了。5. 上线前必须做的事项5.1 容量评估与资源预估秒杀系统的容量预估有一个简单可用的公式预期峰值QPS 预计参加人数 × 每人平均请求次数 ÷ 活动持续秒数。如果预期参加人数50万每人平均点击10次活动持续30秒那么峰值QPS需要承受约16.6万。这个数值直接决定了你需要多大的Redis集群、多少台应用服务器以及多大的MQ吞吐量。需要说明的是这个公式算出来的是一个保守估算实际峰值往往比平均值高2到3倍因为大部分请求都集中在活动开始后的前5秒。所以在计算资源的最后我习惯再乘一个安全系数1.5留出余量宁可过度准备也不要上线后临时扩容。5.2 降级方案与开关设计即使做了万全准备秒杀活动仍然可能出现意外。我在系统里预设了三个降级开关这些开关平时是关闭的活动期间由运维控制台一键开启。第一个开关是库存预减降级。如果Redis集群异常直接把预减逻辑跳过放行所有请求到MQ让数据库层做最终判断。这个方案并不高效但可以保证活动不中断。第二个开关是异步下单转同步。如果MQ服务不可用秒杀应用直接把请求同步调用订单服务跳过队列缓冲。这套降级方案能保住核心链路但会对数据库产生压力只建议作为最后一招。第三个开关是限流阈值动态调整。如果数据库压力接近阈值可以在线修改网关限流参数比如把用户令牌桶容量从5降到2或者增大黑名单拦截范围快速限制流入的请求量。5.3 活动结束后的数据清理与复盘秒杀活动并不是请求处理完就结束了活动结束后还有一轮收尾工作确认所有订单状态正确、检查库存记录与Redis数据一致、清理用户去重标记、归档秒杀明细数据、为下一场活动释放Redis内存。复盘时我会重点看四类数据各层拦截量用于验证漏斗模型是否生效、Redis预减成功量与数据库成交量的差值评估回补与对账的准确性、订单超时未支付比例检查活动规则是否合理、数据库最大活跃连接数评估连接池配置是否需要调整。这些数据汇总起来才是判断一个秒杀系统是否真正健康的标准也直接决定了下一次活动需要扩容还是可以缩容。经历过那次“线上事故”之后我做秒杀系统的原则变得非常明确能用缓存解决的事绝不打数据库能异步处理的事绝不阻塞请求能提前拦截的流量绝不放进后端。这套方案不一定是最优雅的但它在几次真实活动中都稳住了。如果看完这篇文章你想从零搭一套秒杀系统建议先画清楚自己的漏斗模型明确每一层要挡掉什么请求再动手写代码。不要想着把所有逻辑都堆在一起分层设计会让后续的扩容和维护都省心很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑