基于Redis SETNX的秒杀系统演进:从单机锁到集群分布式锁

基于Redis SETNX的秒杀系统演进:从单机锁到集群分布式锁 秒杀系统做了几年库存超卖、重复下单、服务雪崩这些坑我基本都踩遍了。这篇直接把我在高并发场景下基于 Redis SETNX 做集群秒杀系统的完整迭代思路写出来从最初最简单的加锁扣减到后面集群环境下分布式锁的演进每一步为什么这么做、中间踩过哪些坑全给你交代清楚。特别是针对 SETNX 这个命令它怎么从一个简单的“占位”命令一步步成长为秒杀场景下库存扣减的核心武器这里面有不少细节文档里通常不会写。整个方案适合正在做秒杀、抢购、限量活动等场景的后端开发同学参考无论是你刚接触 Redis还是已经在生产环境跑着秒杀服务这篇的演进思路应该都能给你一些启发。1. 内容整体设计与思路拆解1.1 秒杀系统的本质不是一个“查询”问题而是一个“并发写”问题很多刚接触秒杀的同学第一反应是“把库存放到 Redis 里用高并发读扛住流量”这个思路其实只对了一半。秒杀的核心矛盾点在于无论有多少请求打进来最终能下单成功的只有 N 个人而这个“只有 N 个人能成功”的限制条件在并发环境下会变成一个非常棘手的写冲突问题。举个例子商品库存只有 10 件同时来了 1 万个请求。如果你只是把库存存在 Redis 里每个请求来了都执行一个 read → check → decrement 的逻辑在高并发下几乎必然出现多个线程同时读到库存为 1、同时判定“还有货”、同时执行扣减的情况最终结果就是库存变成负数也就是经典的“超卖”问题。超卖在真实业务里非常致命它不仅意味着系统逻辑有缺陷更意味着你要面对大量用户投诉和售后问题。所以秒杀系统的整体设计第一步就是要建立一个思维模型这是一个分布式并发写问题。所有的技术选型包括 Redis 的 SETNX、Lua 脚本、分布式锁、消息队列本质上都是在围绕“如何在并发写的情况下保证数据一致性”做文章。1.2 为什么选 Redis 而不是数据库锁传统的做法是直接在数据库层面做乐观锁或者悲观锁。比如 UPDATE stock SET num num - 1 WHERE goods_id ? AND num 0这种 SQL 确实能在数据库层面避免超卖但问题是数据库的并发处理能力有上限。在秒杀这种瞬时流量峰值的场景下数据库的连接池很快会被打满紧接着就是大量请求堆积、超时最后服务雪崩。Redis 是单线程模型所有的命令在 Redis 服务端都是串行执行的这就天然避免了多线程并发修改的问题。同时 Redis 基于内存操作单机 QPS 可以到 10 万甚至更高这个能力远超数据库。所以在秒杀场景下把库存扣减这个高频写操作下沉到 Redis 层是最合理的架构选择。但这里有一个容易忽略的细节Redis 的“单线程”只是说命令在服务端执行是串行的客户端到 Redis 之间的网络请求、应用层代码逻辑仍然是多线程并发调用的。假如你写的是这样的 Java 代码int stock redisTemplate.opsForValue().get(stock:1001).intValue(); if (stock 0) { redisTemplate.opsForValue().decrement(stock:1001); // 生成订单 }这段代码在高并发下一定会出问题。原因在于 get 操作和 decrement 操作是两个独立的命令线程 A get 到库存是 1还没来得及 decrement线程 B 也 get 到库存是 1两个线程都认为有货然后都执行 decrement库存就变成 -1 了。这就是网上说的“竞态条件”。我在早期做秒杀系统时就在这里踩过坑。当时想的是“反正 Redis 是单线程的应该没问题”结果压测一跑超卖数据直接打脸。所以核心结论第一条要保证 Redis 操作之间的原子性不能靠“Redis 单线程”这个特性来偷懒必须使用带有原子性保证的命令。SETNX 就是这个方向上一个非常关键的解决方案。2. 核心细节解析SETNX 原理与秒杀场景适配2.1 SETNX 到底是什么它解决了什么问题SETNX 的全称是 SET if Not eXists翻译过来就是“如果不存在才设置”。它的用法是SETNX key value当 key 不存在的时候设置 key 的值为 value返回 1表示设置成功当 key 已经存在的时候不做任何操作返回 0表示设置失败。这个命令看起来非常简单但它在秒杀场景里的价值非常大。它可以很自然地用来做“占位”——比如在用户点击秒杀按钮的那一刻我们先尝试用 SETNX 去占一个“锁”占到了就说明这个用户抢到了秒杀资格没占到就说明资格已经没了。命令的返回值天然可以作为判断条件原子性有保证不需要额外的事务控制。顺便说一句新版 Redis 更推荐直接用带参数的 SET 命令SET key value NX EX 10意思是“如果 key 不存在则设置同时设置 10 秒过期时间”。这个命令可以在一行里同时完成“加锁”和“设置过期时间”两个动作比分开执行 SETNX 和 EXPIRE 更安全。因为在老版本的 SETNX 加 EXPIRE 方案里如果 SETNX 执行成功后、EXPIRE 没来得及执行服务就挂掉了这个锁就会永远存在后续所有请求都进不来这就是典型的死锁问题。在我做的秒杀集群迭代中从一开始就在这个细节上做了规避。2.2 秒杀场景下的库存扣减为什么依赖 SETNX你可能要问了库存扣减用 DECR 命令不是更直接吗确实纯库存扣减场景下DECR stock:1001这个命令本身是原子性的在高并发下执行一万次每次都能保证减一不会出现超卖。但问题在于秒杀流程不是只有“扣减库存”这一个动作它后面还跟着创建订单、锁定优惠券、通知支付等一系列流程。如果说 DECR 解决的是“库存数对不对”问题SETNX 解决的就是“谁有资格去扣库存”的问题。用 SETNX 来做资格控制的思路是这样的SETNX seckill:order:1001:userId123 1如果返回 1说明这个用户成功抢占了秒杀资格接下来可以继续执行库存扣减和下单逻辑如果返回 0说明这个用户已经抢过了直接返回“重复抢购”避免同一用户重复下单。这种设计有两个明显优势。一是它天然做了一层“接口幂等”同一个用户无论并发点了多少次秒杀按钮只有第一次能拿到资格后面的请求全部被挡在门外数据库层面不会因为这个用户产生多笔重复订单。二是它把高并发的写冲突从数据库转移到了 Redis 层通过 SETNX 这个原子命令快速过滤掉绝大多数无效请求真正走到扣库存这一步的请求量已经非常小了。这里我自己的经验是在秒杀场景里不要指望一个 Redis 命令解决所有问题而是要想清楚每个命令各自负责哪一环。DECR 负责库存数据的严格扣减SETNX 负责资格控制分布式锁负责保护更复杂的业务流程。各司其职系统才能既保证正确性又保证高性能。3. 实操过程从单机锁到集群分布式锁的迭代演进3.1 第一版单机 SETNX 加锁为什么压测一上就崩最早做秒杀功能的时候由于业务量不大我直接用了一个非常简单的方案用户点击秒杀按钮后先执行 SETNX 获取锁拿到锁之后执行库存查询、库存扣减、订单创建逻辑最后释放锁。// 第一版伪代码 boolean locked redisTemplate.opsForValue().setIfAbsent(lock:seckill:1001, 1); if (!locked) { return 秒杀失败请重试; } try { // 查询库存 int stock getStock(1001); if (stock 0) { return 已售罄; } // 扣减库存 deductStock(1001); // 创建订单 createOrder(userId, 1001); } finally { redisTemplate.delete(lock:seckill:1001); }功能验证阶段一切正常单线程串行调用没问题二三十个并发也能跑通。但等压测工具一上500 个并发同时来立刻就暴露了两个严重的问题。第一个问题是有些请求明明没有拿到锁却也执行了后续的秒杀逻辑。排查后发现问题出在 Redis 客户端连接池。在高并发下连接池里的连接被大量创建和销毁偶发性的连接异常导致 setIfAbsent 这条命令执行超时但是超时并不代表命令没在 Redis 端执行。也就是说 Redis 端实际已经设置了锁但客户端收到了超时异常代码走入了 catch 分支或者直接重试导致同一个用户的两条请求都进入了后续逻辑。第二个问题是锁的粒度太粗了。我用的是商品级别的全局锁也就是说同一时间只有一个请求能进入后续逻辑完全串行化。这样一来Redis 单线程 应用层串行锁的双重限制下系统的吞吐量极其低下压测结果大概只有 200 TPS 左右这显然不是秒杀场景该有的水平。这个第一版方案让我意识到加锁这件事关键是锁的粒度、锁的可靠性、锁的释放时机而不是简单地在方法前后套一个 SETNX。于是我开始正式规划第二版。3.2 第二版SETNX Lua 脚本把“检查库存 扣减库存”做成原子操作第二版的改动核心是不再用“Java 代码判断库存 Java 代码扣减库存”这种分步方式而是把所有判断逻辑写进 Lua 脚本一次性交给 Redis 执行。Lua 脚本在 Redis 中的执行也是原子的——Redis 会保证一个 Lua 脚本在执行过程中不会有其他命令插入。这样“检查库存是否大于 0如果大于 0 就扣减并返回成功否则返回失败”这个复合判断就变成了一个原子操作。-- 秒杀库存扣减脚本 local stockKey KEYS[1] local userKey KEYS[2] local userId ARGV[1] -- 判断用户是否已秒杀成功 if redis.call(exists, userKey) 1 then return 0 -- 重复秒杀 end -- 判断库存是否充足 local stock tonumber(redis.call(get, stockKey)) if not stock or stock 0 then return -1 -- 已售罄 end -- 扣减库存 redis.call(decr, stockKey) -- 标记用户已秒杀 redis.call(set, userKey, 1) return 1 -- 秒杀成功这个脚本同时解决了三个问题库存判断和扣减的原子性、同一用户重复下单的幂等性问题、以及业务逻辑与 Redis 交互的网络开销问题。以前需要三到四个网络 RTT 才能完成的事情现在一个脚本执行就行性能提升非常明显。不过第二版上线后我又遇到了新的状况。有一次大促前压测出现了“库存扣减成功但大量用户同时秒杀成功”的现象。排查后发现问题在于 Lua 脚本在 Redis Cluster 模式下有 slot 限制——同一个 Lua 脚本涉及的多个 key 必须落在同一个 hash slot 上否则 Redis 集群无法保证脚本执行的原子性。我当时的库存 key 和用户 key 是分开的比如 stock:1001 和 user:1001:12345这两个 key 的 hash tag 不同在集群模式下会被分配到不同的节点上。脚本在 Redis 集群中执行时直接报错拒绝执行。解决办法就是给 key 加上同一种 hash tagstock:{1001} user:{1001}:12345花括号里的内容会被 Redis 用来计算 hash slot所以这两个 key 一定会落在同一个节点上。这是一个非常经典的集群环境下的 Redis 使用技巧我在这里浪费了大半天时间写出来给大家避坑。3.3 第三版集群环境下的分布式锁演进RedLock 到底要不要用Lua 脚本解决了库存扣减的原子性问题但秒杀流程中还有一个环节需要保护创建订单。订单创建涉及数据库插入、缓存更新、库存流水记录等操作这些操作的耗时远高于 Redis 操作如果完全不加锁数据库依然可能在扣库存完成后瞬间被订单写入打爆。这个环节需要的是分布式锁。分布式锁的本质是让多个应用实例之间互相协调保证同一个时刻只有一个实例能执行某个临界区逻辑。在集群部署环境下业务服务通常是多实例部署的JVM 内置的 synchronized 锁在多实例环境下完全无效必须引入一个跨进程可见的锁协调者——Redis 就是最常用的选择。第三版的分布式锁实现我在第二版的基础上加入了一个非常关键的细节锁的 value 必须是唯一的随机字符串。为什么看下面这个经典的误删锁场景线程 A 拿到锁执行订单创建逻辑因为某种原因执行时间过长锁自动过期了线程 B 在这个时间点尝试加锁成功了开始执行自己的业务逻辑这时候线程 A 终于执行完了在 finally 里执行 delete 释放锁——结果把线程 B 的锁删掉了。之后线程 C 又进来抢到锁导致锁完全失效多个线程同时进入临界区。如果锁的 value 是线程唯一标识释放锁前先对比 value 是否一致只有一致才删除就能避免这个误删问题。这个对比 删除的动作同样要用 Lua 脚本保证原子性if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) end return 0这版方案在生产环境跑了很久相对稳定。但严格来说它依然有一个理论上的短板如果 Redis 主节点发生故障锁数据还没来得及同步到从节点主节点就宕机了从节点被提升为新的主节点此时新的主节点上没有这个锁的记录其他线程就能加锁成功导致锁失效。要解决这个问题Redis 官方给出了 RedLock 算法向集群中多个独立的 Redis 节点同时加锁只有超过半数的节点加锁成功才认为锁获取成功。RedLock 实现比较复杂而且对于大多数秒杀业务来说库存数据本身就在 Redis 上做了主从同步如果连主节点都挂了整个服务应该触发降级或者熔断策略而不是继续追求“绝对正确”的分布式锁。我的实际建议是如果你的秒杀系统对数据一致性极其敏感例如涉及真实资金交易可以研究 RedLock但对于大多数商品秒杀活动在 Redis 主从 哨兵或 Redis Cluster 架构下用“SET NX EX 唯一 value Lua 释放”这个方案已经足够。毕竟秒杀场景更看重的是可用性和吞吐量一旦 Redis 主节点故障你的核心诉求是快速恢复服务而不是在故障窗口期继续追求极致的锁正确性。3.4 集群部署拓扑Redis Cluster 还是主从哨兵秒杀系统的 Redis 集群部署我通常会根据业务体量做两种选型。如果是中小型秒杀比如单场活动几万到几十万人参与主从 哨兵架构完全够用。一个主节点负责写入两三个从节点负责读流量哨兵监控主节点的健康状态出现问题自动故障转移。这种架构简单可靠运维成本低。如果是大型秒杀比如平台级别的活动几百万人参与就需要上 Redis Cluster。Cluster 模式将数据自动分片到多个主节点上每个主节点又可以有从节点既解决了单节点容量上限问题又提升了读写吞吐量。但我必须提醒一个运维经验Redis Cluster 在秒杀场景下有个容易犯的错就是 not all slots covered 问题。集群中的每个主节点负责一部分 hash slot如果有节点宕机且没有从节点可以顶上集群就会停止服务。我曾经在一次大促前巡检时发现集群里有节点内存使用率接近 100%于是执行了扩容但因为扩容过程中没有正确设置从节点数量导致部分 slot 没有被覆盖整个集群处于不可用状态。这个教训告诉我在大促前集群巡检时一定要检查每个分片的 slot 覆盖情况、主从节点的对应关系、以及内存使用率不要等到流量高峰才发现问题。4. 秒杀全流程实操从用户点击到订单落库的完整链路4.1 前端限流 后端限流先挡住一部分流量很多人以为秒杀系统的第一道防线是 Redis其实不是。真正的第一道防线是前端限流和后端网关限流。在前端秒杀按钮点击后应该置灰禁止用户重复提交同时配合一个简单的 JS 倒计时逻辑让用户在活动开始后才能点击。这个小功能看起来不起眼但实测能减少至少 30% 到 50% 的无效请求。在后端网关层我习惯用 Nginx 的 limit_req 模块做 IP 维度限流再在应用层用 Guava RateLimiter 或者 Redis 计数器做用户维度限流。用户维度的限流特别适合用 Redis 的 INCR EXPIRE 实现例如限制单个用户每秒钟最多只能请求 3 次秒杀接口超过就直接拒绝返回“操作太频繁”。这样可以有效过滤掉脚本刷单和机器人流量。4.2 基于 SETNX 的秒杀主流程文字流程图对于标题中提到的“详细流程图”我用文字把整体逻辑梳理出来。实际作图时你可以按照下面这个流程来画主流程就是一条直线加几个分支判断。用户点击秒杀按钮 ↓ 前端按钮置灰 格式化请求信息 ↓ 网关层 IP 限流 用户维度限流 ↓ 秒杀接口接收请求 ↓ (前置判断) 活动是否在有效期内 ↓ 否 → 返回活动未开始/已结束 ↓ 是 基于 SETNX 做用户维度“资格占位” ↓ 失败 → 返回您已参与过秒杀 ↓ 成功 Lua 脚本原子检查并扣减 Redis 库存 ↓ 库存不足 → 返回已售罄 ↓ 扣减成功 发送消息到 MQ异步创建订单 ↓ Redis 设置下单成功标记TTL 15 分钟 ↓ 返回“秒杀成功等待支付” ↓ 异步订单流程消费 MQ校验 Redis 扣减流水 创建数据库订单、锁定优惠券、生成支付链接 ↓ 用户支付支付回调更新订单状态右边还可以补充一列“异常分支”比如 MQ 消费失败、订单创建超时、数据库锁表等对应的兜底逻辑可以单独画成泳道图。建议用 Visio 或者 draw.io 画图把 Redis 集群和业务集群的边界画清楚面试或者团队分享时都很有用。4.3 为什么一定要引入 MQ 异步创建订单这里有一个很多初学者不理解的点既然库存都扣减了为什么不直接在秒杀接口里同步创建订单原因是秒杀接口的处理时间直接决定了系统的吞吐量。Redis 操作是微秒级的但数据库订单插入是毫秒级的两者的性能差距在一到两个数量级。如果你在接口里同步创建订单数据库会成为系统的瓶颈同时接口耗时也会长很多。我的做法是秒杀接口只负责“确认资格 扣减库存 发送 MQ 消息”然后立即返回给用户整个接口的 RT响应时间通常能控制在 5ms 到 20ms 之间。真正创建订单的操作放在 MQ 的异步消费者里执行。但异步化会带来一个新的问题如果用户秒杀成功但订单异步创建失败了怎么办比如数据库临时不可用或者 MQ 消息丢失。针对这个问题我做了一个补偿机制秒杀接口返回成功后Redis 中会保存一个“下单成功”的标记并设置 15 分钟的过期时间。用户可以在这个时间窗口内查询订单状态如果发现订单没有生成前端会提供“重新下单”的按钮通过这个标记确保同一个用户不会重复秒杀同时能够在超时场景下主动触发补单。4.4 订单过期未支付怎么办很多秒杀系统做的是限时抢购比如用户抢到资格后必须在 15 分钟内支付超时未支付订单就自动取消库存回滚。这就是热词里提到的“订单过期了怎么办”问题。最简单的方案是使用 Redis 的过期监听keyspace notifications在用户秒杀成功时设置一个过期时间为 15 分钟的 key比如 order:timeout:1001:userId123当 key 过期时触发监听事件去关闭超时订单。不过这个方案有个问题Redis 的过期事件不是实时触发的而是 key 被访问或者过期扫描轮询时才触发默认情况下延迟可能在几十秒甚至更长。而且 keyspace notifications 在集群模式下需要额外配置事件消息可能丢失。更可靠的做法是采用定时任务扫描定义一个 Job每 30 秒扫描一次“已创建但未支付”的订单扫描条件设置为 create_time 小于当前时间减去 15 分钟。扫描到之后先检查 Redis 中是否有支付成功标记没有的话就把订单置为“已过期”同时把 Redis 中的库存重新加回。两种方案也可以结合用 Redis 过期事件做“软提醒”触发时优先检查订单是否已支付未支付才走关闭流程。用定时任务作为兜底保证即使过期事件丢了也能通过扫描补偿。5. 常见问题与线上排查实录5.1 缓存与数据库一致性问题库存显示有货下单却说已售罄这是秒杀系统中最常见的线上问题。用户在商品详情页看到库存还有 5 件点进秒杀页面却提示已售罄。这个现象通常是因为商品详情页的库存数据是从缓存读取的而缓存数据和 Redis 秒杀库存之间有一个短暂的时间差。比如商品详情页每 10 秒刷新一次库存而 Redis 中的秒杀库存可能在前一秒就被清零了。解决方法有两种。一种是实时性要求高的场景下用 Redis 的发布订阅机制在秒杀库存变化时主动通知详情页刷新缓存。另一种是弱化详情页库存数据的准确性详情页只显示“有货/无货/紧张”几个档位等用户进入秒杀页再实时从 Redis 获取精确库存。第二种方案更简单大多数业务场景都适用。5.2 Redis 主从切换导致的一小段时间锁失效如何应对前面提到过主从切换期间分布式锁可能失效的问题这里给出更务实的应对方案。首先Redis 主从切换时通常会有几十毫秒到几秒的不可用时间这个时间窗口内秒杀系统的正确性不能完全依赖锁。我的做法是在秒杀入口加一个本地开关一旦监控到 Redis 连接异常或者主从切换事件立刻触发熔断不再接受新的秒杀请求已经进入的请求通过“快速失败”方式返回“系统繁忙”。等 Redis 恢复后再自动放量。其次在库存扣减这个核心动作上永远以 Lua 脚本的原子操作为准。因为 Lua 脚本本身不具备“加锁 — 执行 — 释放”这种长链路逻辑即使锁短暂失效库存扣减的严格原子性仍然有保障最多影响的是业务链路的并发保护。只要订单创建环节有数据库层兜底问题就处于可控范围。5.3 压测时 Redis 连接池被打满一个容易被忽视的配置有一次压测时我遇到一个很奇怪的现象Redis 的 CPU 占用率不到 20%但接口的 P99 延迟却高达 5 秒。排查后发现是应用层的 Redis 连接池配置不合理最大连接数默认设置的是 50而压测时并发达到了 500 以上大量线程在等待获取连接形成排队阻塞。解决方法很简单把最大连接数调整到 200 到 500同时设置合理的最大等待时间和连接超时时间。更关键的是在使用 Lua 脚本和 SETNX 之后Redis 操作的耗时本身非常短单个连接的处理速度足够快真正限制吞吐量的往往是连接池配置和网络延迟。连接池不是越大越好——太大了反而会增加系统资源开销建议用下面的方式压测调整先估算单台业务机的目标 QPS假设是 2000Redis 单操平均延迟 1ms那么单连接每秒最多处理约 1000 个请求计算需要的连接数约等于 QPS 除以单连接吞吐也就是 2 到 5 个连接再加上网络抖动的缓冲和慢查询场景给到 50 到 100 的合理区间。我一般会把最大连接数设置在 100 左右最大等待时间不超过 200ms连接超时 1000ms过大的连接池反而会带来不必要的线程上下文切换开销。5.4 死锁问题加锁有风险释放锁必须谨慎热词里有一个问题非常经典“什么情况下会产生死锁如何避免”。放在 Redis 分布式锁语境下死锁产生的三种典型场景是第一获取锁后业务逻辑异常catch 块没有释放锁锁变成了永久锁。避免方法非常简单把删除锁的动作放在 finally 块里确保任何情况下都会执行释放。第二加锁之后 Redis 服务不可用删除锁失败其他线程永远拿不到锁。避免方法是给锁设置一个合理的过期时间比如 10 秒这样即使释放失败锁也会自动过期。第三锁被误删除也就是前面提到的持有锁的线程执行时间超过锁过期时间锁自动释放后被其他线程获取原线程执行完后删除了其他线程的锁。避免方法是给锁的 value 设置唯一标识释放前先比较再删除用 Lua 脚本实现。在秒杀场景下还有一类分布式锁死锁问题值得注意多个锁的获取顺序不一致导致互相等待。比如线程 A 持有锁 1 去获取锁 2线程 B 持有锁 2 去获取锁 1两者就会死锁。我的习惯是在写多把锁的逻辑时严格按照同一个顺序获取所有锁比如先锁用户维度再锁商品维度从源头上规避循环等待。6. 集群环境下其他配套环节服务注册发现与服务熔断降级6.1 SpringBoot 微服务如何做注册与发现秒杀系统在集群环境部署时通常不是一个单体应用而是拆成了用户服务、商品服务、订单服务、秒杀服务等多个微服务。微服务之间的调用需要知道对方的地址这就涉及到服务注册与发现。在 Spring Cloud 体系中最常用的方案是 Nacos 或 Eureka。服务提供方在启动时把自己的 IP 和端口注册到注册中心服务消费方从注册中心获取可用服务列表并通过负载均衡策略选择一个发起调用。SpringBoot 集成比较简单引入依赖后加上注解就行SpringBootApplication EnableDiscoveryClient public class SeckillApplication { public static void main(String[] args) { SpringApplication.run(SeckillApplication.class, args); } }服务注册发现的核心价值在于当某台秒杀服务实例宕机时注册中心能自动摘除这台实例把流量切换到其他健康实例上。这是集群高可用的基础能力在生产环境几乎是标配。6.2 秒杀服务的熔断、降级与限流秒杀场景下Redis 和数据库是最容易出问题的环节。一旦 Redis 集群出现异常如果所有请求都继续打到 Redis 上只会加重故障最终导致整个服务雪崩。所以必须在核心依赖出现故障时快速触发熔断降级。我常用的熔断方案是 Sentinel 或 Resilience4j。熔断的核心逻辑是设置一个失败比例阈值比如 1 秒内 Redis 操作失败比例超过 50%熔断器打开后续所有请求直接返回“系统繁忙请稍后重试”的兜底结果不再真正访问 Redis。熔断器打开后会进入半开启状态定期尝试放行少量请求探测 Redis 是否恢复恢复成功则关闭熔断恢复正常流量。降级则需要有预案。秒杀系统的降级预案一般包括关闭商品评论、推荐等非核心接口把资源都留给秒杀链路秒杀入口改为排队模式用户点击秒杀后先进入等待队列而不是立即处理数据库写操作批量合并降低数据库压力。这套组合方案不是秒杀独有的但在秒杀系统里效果特别明显。因为你面对的流量峰值是平时的几十倍甚至上百倍任何一个依赖点的脆弱都可能成为瓶颈。提前规划好熔断降级策略遇到故障时才能做到“先保命再修复”。7. 秒杀系统后续扩展方向我个人在实际迭代过程中觉得这套基于 Redis SETNX 的秒杀系统解决了核心的扣库存问题但远远不是终点。如果你想在这个基础上继续做扩展有两条比较成熟的方向值得投入。一条是引入消息队列做削峰填谷比如 Kafka 或 RocketMQ。在秒杀接口确认资格后把请求数据写入 MQ下游订单服务从 MQ 拉取消息异步处理。这种方式能把瞬时峰值打散到一段时间内有效保护数据库和下游服务。之前我在秒杀集群中做过对比引入 Kafka 后订单服务在高峰期的负载波动从峰值 100% 降到了平稳的 50% 左右效果非常显著。另一条是做分布式链路追踪和监控告警。秒杀系统的调用链比较长网关 → 秒杀服务 → Redis → MQ → 订单服务 → 数据库。任何一个环节出问题都需要快速定位否则故障排查成本非常高。接入 SkyWalking 或类似链路追踪工具配合 Prometheus 监控秒杀接口的 QPS、RT、成功率、Redis 缓存命中率等指标再配置好告警规则能大幅提升线上问题的定位效率。最后再分享一个小技巧任何时候在秒杀场景引入一个新的 Redis 命令或新方案先在小流量上灰度再逐步放量。不要因为压测通过了就直接全量上线。压测环境像不像生产环境、集群规模和网络延迟差异、真实用户的请求模式都可能导致线上表现和预期完全不一样。秒杀系统本质上是高并发、高可用、强一致性三者之间的权衡游戏每做一次改动都要同时对这三个维度做一次评估缺一不可。