Redis秒杀高并发优化实战:库存扣减、防重限流与热点Key拆分 秒杀这个场景做过电商类系统的朋友应该都不陌生。我第一次接触秒杀需求的时候想法很朴素不就是把商品库存放到 Redis 里下单前减一下减成功了就生成订单减失败了就提示售罄。真跑起来才发现事情远没有这么简单——超卖、重复下单、库存扣了订单没生成、数据库被打穿、热点 Key 把单个节点压垮这些问题会一个接一个冒出来。Redis 在秒杀业务里扮演的角色绝不只是一个存库存的缓存它是整个流量漏斗里最关键的那道闸门闸门设计得好不好直接决定了系统在大促那一刻是稳如老狗还是全线崩盘。这篇内容我想聊的是 Redis 在秒杀业务优化中的完整落地思路从流量分层、库存预热、原子扣减到防重、限流、异步落单、热点拆分再到压测验证和线上排障。适合已经写过基础 CRUD、用过 Redis 常见命令但没真正扛过一次高并发活动的后端同学也适合做技术方案评审时想把细节讲清楚的人。下面这些方案和参数一部分是我自己项目里跑过的一部分是基于常见实践做的合理推演涉及具体数字的地方你可以按自己的机器规格和业务量级等比调整别照抄。1. 秒杀业务的整体设计与流量分层思路1.1 先想清楚秒杀到底在难什么很多人把秒杀当成高并发扣库存来做方向从第一步就偏了。秒杀的本质矛盾不是扣库存难而是瞬时流量和系统承载能力之间的巨大落差。一个平时 QPS 几百的系统活动开始那一秒可能涌进来几万甚至几十万请求而这些请求里 99% 以上最终都是失败的——商品就那么多绝大多数人注定抢不到。既然绝大多数请求注定失败那优化的核心目标就变成了用尽可能低的成本把注定失败的请求挡在系统最外层让真正能成功的那一小部分请求走到后面的下单链路。这个认知一旦建立整套方案的设计逻辑就顺了。Redis 之所以成为秒杀的核心组件是因为它能在内存里用极低的耗时完成库存判断这个动作一次 DECR 或 Lua 脚本执行通常在亚毫秒级,比查一次数据库快两到三个数量级。把库存判断前移到 Redis等于在数据库前面竖了一道极快的筛子把无效流量拦下来数据库只需要处理真正扣减成功的少量请求。这里有个容易被忽略的点Redis 挡住的不仅是数据库压力,还挡住了应用服务器的线程压力。如果不做前置拦截几万请求全部打进 Tomcat 线程池线程上下文切换、GC 压力、连接池耗尽会连锁反应最后连健康检查都通不过。所以流量分层的意义是全链路的不只是数据库。我习惯把秒杀链路画成一个漏斗从上到下依次是静态资源与客户端限频、网关层限流、应用层前置校验、Redis 库存原子扣减、消息队列削峰、数据库最终落单。每一层都在做过滤层数越多越靠后的组件压力越小。下面这张表是我自己项目里常用的分层职责划分你可以对照着看自己的系统缺哪一层。层级主要职责典型手段拦下的流量占比经验值客户端按钮防抖、验证码、答题前端置灰、随机延迟减少无效点击 30%~60%网关层全局限流、黑名单令牌桶、IP 维度限流拦掉超量请求应用层活动状态、参数校验、用户资格本地缓存、布隆过滤拦掉 80% 以上Redis 层库存原子扣减、防重Lua 脚本、SETNX拦掉剩余绝大部分消息队列削峰、异步落单分区顺序、重试平滑数据库写入数据库最终一致性落单唯一索引、乐观锁只处理成功请求1.2 为什么库存要放 Redis 而不是直接怼数据库有人会问数据库加个乐观锁update stock set num num - 1 where id ? and num 0不也能防超卖吗能但性能不行。这条 SQL 在行锁竞争激烈的时候会引发大量线程阻塞几万请求排队等同一行锁数据库连接池瞬间打满响应时间从毫秒级飙到秒级接着触发上游超时重试流量翻倍彻底雪崩。Redis 是单线程处理命令的指命令执行主线程天然串行扣减库存这种操作不需要加锁就具备原子性——前提是你用对了命令。用DECR或者 Lua 脚本一条命令下去判断和扣减在同一个原子操作里完成不存在并发窗口。这就是它比数据库乐观锁更适合秒杀的根本原因不是数据库不行是数据库的锁机制在这个场景下成本太高。不过要提醒一句Redis 的单线程原子性只保证单机内的命令原子集群模式下如果库存 Key 分散在不同节点跨 Key 的操作就没法保证原子了。所以库存 Key 通常要打上 hash tag 让它落在同一个 slot或者干脆用单 Key 方案。这个细节后面会展开。1.3 方案选型几种常见库存扣减模型的取舍实际项目里我见过三种库存扣减模型各有适用边界选错了会给自己挖坑。第一种是纯 Redis 扣减 异步落库。Redis 里维护可售库存Lua 脚本原子扣减成功就发消息给队列消费者异步写订单和扣数据库库存。优点是性能极致缺点是 Redis 和数据库之间要靠消息保证最终一致消息丢了就出现Redis 扣了、订单没生成的少卖问题。适合对少卖容忍度稍高、追求极致性能的场景。第二种是 Redis 预扣 数据库强一致。Redis 只做前置拦截比如放行量是库存的 1.2 倍真正扣减在数据库事务里用乐观锁完成失败了再回滚 Redis。优点是数据准确缺点是数据库还是要扛一部分并发且有回滚逻辑要处理。适合库存少、对超卖零容忍的场景。第三种是分段库存 多 Key 分散。把总库存拆成 N 份放到 N 个 Key请求按用户 ID 哈希到某个 Key 扣减扣完再从别的段借。这一套主要是为了解决单 Key 热点问题后面第 4 章会详细讲。我的建议是中小规模活动库存几百到几千、峰值几万 QPS用第一种加补偿即可金融属性强、绝对不能超卖的场景用第二种单 Key 已经出现明显热点的用第三种。别一上来就上最复杂的方案先跑通再优化。1.4 库存预热活动开始前你必须做完的事秒杀活动开始前库存必须已经在 Redis 里准备好不能等第一个请求进来才去数据库加载——那一瞬间的缓存击穿会把数据库打死。预热的做法很简单活动配置审核通过后由一个定时任务或管理后台触发的接口把活动库存、商品信息、限购规则写入 Redis并设置合理的过期时间通常比活动结束时间晚一到两个小时留出对账和补偿的窗口。预热时有两个细节值得说。一是写入要用SET key value EX seconds NX这种带过期和不存在才写的语义避免重复触发预热把已经扣减过的库存覆盖回初始值——这个坑我踩过手动重跑预热脚本把活动进行中的库存重置了直接导致超卖。二是预热完要做一次校验读回来比对一下写入值和数据库配置值是否一致避免序列化错误或者类型问题比如把整数写成了字符串Lua 里比较时类型不匹配。注意预热接口一定要做幂等和开关控制最好加一个人工确认步骤或者用活动状态机约束——只有待开始状态才允许预热进行中的活动直接拒绝。1.5 活动状态机把时间维度的判断也前置秒杀有明确的时间窗口活动没开始和已结束的请求根本不该走到库存扣减。这些判断放在应用层用本地缓存做最划算活动配置在启动时或定时刷新到本地 Map请求进来先看当前时间是否在窗口内、活动是否被手动下架不符合直接返回。本地缓存的好处是零网络开销几万 QPS 打过来也只是内存比较。状态机我一般设计成待审核 → 待开始 → 进行中 → 已结束 → 已结算。只有进行中允许下单待开始允许预热已结束允许对账。状态流转由后台操作触发同时广播到各应用实例刷新本地缓存。这样即使 Redis 里还有残留库存活动结束后也不会被继续扣。2. 核心细节解析原子扣减、防重与限流2.1 Lua 脚本实现原子的判断 扣减 记录单纯用DECR有个问题它只减不判断减到负数了你还得再补一次INCR回滚中间这一小段就是超卖窗口。正确做法是用 Lua 脚本把读库存、判断是否大于 0、扣减、记录购买用户打包成一个原子操作。Redis 执行 Lua 是单线程的脚本内部不会被其他命令打断天然满足原子性。下面这个脚本是我项目里用的简化版包含库存判断、用户防重、扣减三个动作-- KEYS[1] 库存 key, KEYS[2] 已购用户集合 key -- ARGV[1] 用户 id, ARGV[2] 本次购买数量 local stock tonumber(redis.call(GET, KEYS[1])) if stock nil then return -1 -- 库存 key 不存在未预热 end if stock tonumber(ARGV[2]) then return 0 -- 库存不足 end if redis.call(SISMEMBER, KEYS[2], ARGV[1]) 1 then return -2 -- 该用户已购买过 end redis.call(DECRBY, KEYS[1], ARGV[2]) redis.call(SADD, KEYS[2], ARGV[1]) return 1 -- 扣减成功返回码设计很重要-1 表示未预热、0 表示售罄、-2 表示重复购买、1 表示成功调用方根据返回码给不同提示也方便打点统计各分支的命中量。实测下来这个脚本在单机上执行耗时在 0.1~0.3 毫秒一次活动峰值几万 QPS靠它拦掉 99% 的请求毫无压力。几个必须注意的点脚本里所有的 Key 都要通过 KEYS 数组传入不要在脚本里拼接 Key 名否则集群模式下会报错脚本要尽量短因为 Redis 执行 Lua 期间会阻塞其他命令脚本太长会影响整体吞吐tonumber转换不能省Redis 里 GET 出来是字符串直接和数字比较在某些版本下行为不一致。2.2 防重的三个层次别只靠一层重复下单是秒杀里投诉率最高的问题之一。用户在客户端疯狂点或者网络重试导致同一个请求发了两次如果不做防重一个用户可能扣两次库存。我一般做三层防护。第一层是客户端按钮点击后立即置灰加一个短时间的防抖比如 500 毫秒同时前端生成一个一次性的请求令牌token随请求带上。这一层能挡掉大部分误触但不能信任因为客户端可以绕过。第二层是 Redis 层的用户维度去重就是上面 Lua 脚本里的SISMEMBER判断。这里有个选型问题用 Set 还是用 String 的 SETNXSet 的好处是能顺便统计已购人数坏处是活动大了以后这个 Set 会很大几百万用户就是几百万成员内存占用可观。我的做法是活动结束后清理或者按用户 ID 取模分片成多个 Set。如果只需要防重不需要统计用SET key:userId 1 NX EX 3600更省内存每个用户一个短过期的小 Key。第三层是数据库唯一索引兜底订单表上建(activity_id, user_id)的唯一索引即使前面两层都漏了数据库插入时也会报唯一键冲突捕获后返回已参与。这是最后一道保险绝对不能省——我见过 Redis 里的防重 Key 因为过期时间设太短设了 5 分钟但消息消费延迟了 10 分钟导致防重失效最后靠数据库唯一索引拦住的案例。注意三层防重的 Key 过期时间要大于整个链路的处理时长宁可设长一点事后清理也不要设短了中途失效。经验值是活动时长加上 2 小时。2.3 限流应该放在哪一层参数怎么定限流的目的不是精确控制而是在流量超过预期时保住系统不崩。我习惯在网关层做全局令牌桶限流桶容量按压测得出的系统最大承载的 70%~80% 设定比如压测出单机最大处理 5000 QPS三台机器就是 15000那限流阈值设 11000~12000留出突发余量。限流的粒度也值得琢磨。按 IP 限流能挡掉脚本刷单但容易误伤 NAT 出口后的正常用户按用户 ID 限流更精准但需要先解析出用户身份按活动维度限流是必须的防止单个活动打垮全局。我一般组合使用活动维度总量限流兜底用户维度做频率限制比如每个用户每秒最多 5 次IP 维度只对异常 IP 做黑名单。限流被触发时返回什么也有讲究。直接返回 429 或者错误提示会让用户以为系统挂了体验很差。我一般返回一个当前参与人数过多请稍后再试的友好提示同时在响应头里带上Retry-After前端据此做倒计时。别小看这些细节活动当天客服压力能小一半。2.4 库存 Key 的过期与清理策略库存 Key 不能永久存在否则内存会被历史活动堆满。过期时间我一般设成活动结束时间加 2 到 4 小时这个窗口要覆盖对账和补偿的时间。但对账要用的数据比如最终售出数量、已购用户集合不能只靠 Redis必须在数据库里有落地否则 Key 一过期数据就没了。清理策略上用户防重的 Set 是内存大户活动结束后应该主动清理而不是等过期。我一般在对账任务跑完后触发清理或者干脆给每个活动的 Key 加上统一前缀用 SCAN 批量删注意不要用 KEYS 命令会阻塞。这里提醒一句SCAN 删除大量 Key 也会占用 CPU建议分批、限速放在低峰期做。3. 完整实操流程从预热到落单的实现细节3.1 环境与数据结构的准备先把数据结构定清楚后面所有代码都依赖它。我项目里的 Key 规划如下表前缀统一用业务名加活动 ID方便排查和批量操作。Key 模式类型内容过期时间sk:stock:{actId}String剩余可售库存活动结束 2hsk:users:{actId}Set已购用户 ID活动结束 2hsk:order:{actId}:{userId}String下单令牌/订单号活动结束 2hsk:limit:{actId}:{userId}String用户请求频率计数10ssk:goods:{actId}Hash商品快照信息活动结束 2h注意sk:stock:{actId}这种带大括号的写法是 Redis Cluster 的 hash tag 语法保证同一个活动的库存和用户集合落在同一个 slotLua 脚本操作两个 Key 时才不会跨 slot 报错。这一点在单机版测试时不会暴露一上集群就炸必须提前注意。数据来源是数据库里的活动配置表和商品表预热时组装好写入。商品快照用 Hash 存字段包括价格、名称、限购数量等下单时从 Redis 读快照而不是查数据库进一步减少数据库访问。3.2 预热脚本的实现与校验预热我一般写成一个独立的定时任务活动开始前 10 分钟触发逻辑是查数据库拿到活动配置和库存 → 组装数据 → 用SET ... NX写入 → 读回校验 → 记录日志和监控打点。# 简化的预热命令示例实际用代码执行 SET sk:stock:{act1001} 500 EX 10800 NX HSET sk:goods:{act1001} name 测试商品 price 99 limit 1校验逻辑不能省写入后GET回来和数据库的值比对不一致就告警并中止活动上线。我遇到过序列化框架把Long写成带引号的字符串导致 Lua 里tonumber拿到 nil 的情况如果没有校验这个错误会一直潜伏到活动开始才爆发。预热完成后还要做一次冒烟验证用一个测试账号走一遍完整下单流程确认库存能正常扣减、订单能正常生成、防重能正常生效。这个验证建议在预发环境做别在生产环境用真实库存试。3.3 下单接口的完整执行链路用户点击抢购后请求依次经过这些环节我在每个环节标注了失败时的处理和耗时预期网关层限流校验超限返回友好提示。耗时预期小于 1 毫秒。应用层活动校验从本地缓存读活动状态非进行中直接拒绝校验用户登录态和参数合法性。耗时小于 1 毫秒。频率限制INCR sk:limit:{actId}:{userId}配合EXPIRE设 10 秒过期超过阈值拒绝。这里要注意 INCR 和 EXPIRE 不是原子的最好也用 Lua 包一下或者接受这个小瑕疵最坏情况是计数 Key 没有过期时间可接受因为量不大。耗时 1 毫秒左右。Lua 原子扣减执行前面那个脚本根据返回码分支。耗时 0.1~0.3 毫秒。生成订单号并入队扣减成功后生成全局唯一订单号雪花算法或 Redis INCR把订单消息发到消息队列立即返回抢购成功正在处理。耗时 1~2 毫秒。异步消费者从队列取消息写订单表、扣数据库库存、更新用户订单列表。这一环在后台跑不影响用户响应。整条同步链路的总耗时控制在 5 毫秒以内这是我认为比较舒服的体验阈值。超过 10 毫秒用户会有明显卡顿感也会拉长线程占用时间降低整体吞吐。这里有个关键决策扣减成功但入队失败怎么办我的处理是入队失败时立即回滚 Redis 库存用INCRBY加回去并SREM移除用户然后返回失败让用户重试。回滚虽然不优雅但这个窗口极小入队失败率通常很低比让用户以为抢到了实际没订单要好。更严谨的做法是把扣减 入队做成一个本地事务消息或者用可靠消息表但复杂度上升明显中小项目用回滚兜底就够了。3.4 消息队列削峰与落单消费者的设计队列这一层的作用是把瞬时的高并发写请求拉平成长时间的匀速写入。峰值几万 QPS 的扣减成功请求可能只有几百到几千取决于库存量如果库存是 1000那最多 1000 条消息消费者慢慢处理就行数据库压力完全可控。消费者的实现要注意几点。一是幂等消息可能重复投递消费者要能识别并跳过已处理的订单用订单号做唯一键或者查库判断是否已存在。二是失败重试写库失败要重试但要设最大重试次数超过后进死信队列并告警不能无限重试堵住队列。三是顺序性如果同一个用户的多次请求可能乱序到达需要保证先扣减先成单通常按用户 ID 分区发送就能解决。数据库落单这一步扣库存要用带条件的更新update stock set num num - ? where id ? and num ?影响行数为 0 说明数据库库存不够正常情况下不该发生发生了说明 Redis 和数据库不一致要告警排查。订单插入依赖唯一索引防重。3.5 库存回补与对账机制活动结束后必须对账核对三个数字Redis 初始库存、Redis 实际扣减量、数据库实际成单量。正常情况下三者应该满足初始 剩余 扣减、扣减 ≈ 成单量。不一致的地方就是需要人工介入的异常常见的有消息丢失导致的少卖、消费者重复处理导致的超卖。对账我一般做成一个离线任务活动结束后跑一次输出差异明细。差异处理原则是少卖的库存可以回补给数据库让用户能买到超卖的订单不能简单取消用户会投诉要人工评估。所以前面强调的数据库唯一索引和条件更新非常关键它们是防止超卖的最后防线。4. 常见问题排查与避坑实录4.1 超卖了第一时间查什么超卖排查我有一套固定顺序最快能定位到根因。先查是不是 Lua 脚本没生效或者被绕过——比如有老代码路径直接调DECR或者某个入口没走脚本。再查集群模式下库存 Key 和用户 Key 是否落在不同 slot跨 slot 的脚本执行会报错如果代码里捕获了异常又继续走了后面的流程就会出现脚本没扣但订单生成了的假象。然后查回滚逻辑扣减成功、入队失败、回滚时只加了库存但没移除用户或者加库存的量超过了实际扣减量比如重试导致回滚两次都会造成库存虚高。这种情况要靠日志里的订单号和扣减记录对账。最后查预热有没有重复预热把进行中的库存重置了这个我前面提过是真实发生过的坑。现象可能原因排查手段处理方式售出量大于库存库存被重置/回滚超量查预热日志、回滚日志立即下架活动人工核账同一用户多单防重 Key 过期/未生效查 Set 内容和过期时间取消多余订单补 Key 过期时间库存扣了没订单消息丢失/消费失败查队列堆积和死信从死信重投或按扣减记录补单库存没扣但订单有脚本执行失败被吞异常查脚本错误日志补扣库存或作废订单活动结束仍能下单本地缓存未刷新/Key 未过期查实例缓存时间和 Key TTL强制刷新缓存缩短 TTL4.2 库存明明有却提示售罄这类问题多半和类型或者数据格式有关。Redis 里存的库存如果是字符串500Lua 里没做tonumber直接比较在某些边界值上会出问题。还有一种情况是预热写入了但写到了错误的 Key活动 ID 拼错、前缀不一致导致脚本读的 Key 根本不存在返回 -1 被当成售罄处理。另一种隐蔽情况是负数库存回滚逻辑加多了或者某次DECRBY减的数量比实际库存大库存变成负数后续正常请求看到负库存直接拒绝。我的经验是在脚本里加一个库存不能减到负数的硬判断同时监控库存为负的告警。4.3 大促当天 Redis 变慢甚至超时Redis 变慢通常有三个来源。一是大 Key比如把已购用户集合做成一个几百万成员的 SetSISMEMBER虽然快但整个 Key 的迁移、过期删除、持久化都会拖慢实例。解决办法是分片按用户 ID 取模拆成多个小 Set。二是慢查询比如误用了KEYS、SMEMBERS这种 O(N) 命令或者 Lua 脚本里有循环。上生产前一定要开慢查询日志slowlog-log-slower-than设成 10000 微秒也就是 10 毫秒定时检查。三是网络和连接数应用侧连接池没配好每次请求新建连接光是握手就吃掉大量时间。连接池大小按平均耗时 × QPS估算比如单次操作 1 毫秒、峰值 5000 QPS理论上需要 5 个连接但考虑突发和阻塞我一般配 50 到 100 个加上超时和熔断。4.4 消息队列堆积了怎么办堆积说明消费速度跟不上生产速度。先看消费者是不是卡在某个少数派消息上比如某条消息一直重试失败阻塞了分区再看单条消费的耗时是不是太长比如消费者里查了数据库还做了远程调用。临时扩容消费者实例是最直接的手段但前提是分区数够分区数不够要先扩分区。预防性措施是消费者逻辑尽量轻重活异步化监控队列的堆积深度和生产消费速率差设合理的批处理大小批量拉取减少网络往返。4.5 我踩过的几个具体坑说几个印象深刻的。第一个是防重 Set 没设过期活动跑了三个月那个 Set 涨到几百万成员一次过期删除直接把实例 CPU 打满。教训是所有临时 Key 必须有 TTL且大 Key 要分片。第二个是回滚逻辑写成扣减失败也回滚把没扣成功的请求又加了一次库存导致活动刚开始库存就变多了。教训是回滚只在扣减成功但后续失败的分支执行且要严格依赖扣减返回值。第三个是本地缓存的活动状态没刷新活动已经开始五分钟了部分实例还认为是待开始用户看到的是活动未开始。教训是状态变更要有广播机制且本地缓存设一个较短的 TTL比如 30 秒做兜底。这些坑的共同点是都不是 Redis 本身的问题而是使用方式的问题。Redis 的语义很明确出问题的永远是我们对它的假设。5. 热点 Key 拆分与性能压测验证5.1 单 Key 热点的成因与拆分方案秒杀场景下所有请求都打同一个库存 Key这个 Key 所在的那个 Redis 节点会承受全部压力其他节点闲着。单节点能力再强也有上限单 Key 的 QPS 瓶颈通常在几万到十几万之间取决于命令复杂度和机器规格超过就会成为瓶颈。拆分方案是分段库存把总库存 1000 拆成 10 段每段 100请求进来根据用户 ID 哈希到某一段扣减这一段扣完了再尝试其他段。好处是压力分散到多个 Key理论上可以线性扩展坏处是会出现某段空了但其他段还有的情况需要借库存或者让用户重试别的段。实现上我一般这样做分段 Key 用sk:stock:{actId}:{segNo}段数按预估峰值除以单 Key 承载能力来定比如预估 10 万 QPS、单 Key 稳跑 2 万那就分 5 到 8 段留余量。Lua 脚本里先试本段本段不足则遍历其他段段数少时遍历成本可接受。借库存要有原子性避免多段同时借超。代价是复杂度上升库存统计变麻烦要汇总所有段对账逻辑也要相应调整。所以我的建议是单 Key 能扛住就别拆先压测出真实瓶颈再决定。5.2 压测怎么做才有参考价值压测最容易犯的错是压测环境和生产差太远或者压测数据不真实。我的做法是压测环境尽量和生产同规格至少同量级压测前把 Redis 预热到和真实活动一样的库存量压测脚本模拟真实用户行为不是同一个用户 ID 猛刷要模拟多用户、带随机间隔、带完整的登录态。压测指标要看全QPS、P99 延迟、错误率、Redis 的 CPU 和内存、应用侧的线程池活跃数、数据库连接池使用率、队列堆积深度。只看 QPS 没意义P99 延迟才是用户真实感受。压测分两轮第一轮阶梯加压找拐点从 1000 QPS 开始每轮加 1000观察哪个点开始延迟陡增或者错误率上升那就是系统的真实承载上限第二轮按预估峰值的 1.5 倍做稳定性压测跑 10 到 30 分钟看有没有内存泄漏、连接泄漏、慢慢变慢的问题。压测中常见的假象也要提防施压机自身成为瓶颈CPU 打满、端口耗尽导致压测结果偏低网络带宽打满压测没有走完整链路比如绕过了网关结果虚高。压测时同步监控施压机的资源别只盯着被测系统。5.3 监控指标与告警配置线上要有几个必看的指标我按重要性排Redis 库存 Key 的剩余量实时曲线能看出扣减速率是否符合预期、Lua 脚本各返回码的计数售罄率、重复率突增要告警、队列堆积深度、消费者处理耗时、数据库订单写入成功率和耗时、Redis 实例的 CPU 和慢查询数。告警阈值我一般这样设库存低于 10% 提醒运营准备补货或者下架脚本返回 -1未预热出现任何一次立即告警队列堆积超过 1 万条告警消费者 P99 超过 1 秒告警。阈值要结合业务量级调整别照抄。光有监控还不够要定期做故障演练手动杀掉一个 Redis 节点看主从切换期间业务表现手动让消费者停止看队列堆积告警是否触发这些演练能在真实故障时救你一命。6. 几个容易被忽略的设计权衡6.1 少卖和超卖你更怕哪个这是个业务决策问题不是技术问题。超卖意味着承诺了没有的货要赔付或者道歉用户体验伤害大少卖意味着有货没卖出去损失的是收入。大多数电商场景宁可少卖不可超卖所以方案偏向严格。但有些清仓场景对超卖容忍度高可以放宽换取更高的吞吐和更简单的链路。这个权衡要在方案设计阶段和业务方确认清楚因为它决定了你选哪种扣减模型、要不要做数据库强一致、回滚逻辑怎么写。6.2 Redis 挂了怎么办秒杀这种强依赖 Redis 的场景必须在设计时想清楚 Redis 不可用时的降级策略。常见做法是Redis 不可用时直接快速失败返回系统繁忙而不是降级去查数据库——因为数据库根本扛不住这个流量降级只会让它死得更彻底。快速失败保住系统等 Redis 恢复再继续。同时要有主从和哨兵或者集群模式保证高可用主从切换期间会有短暂的写入失败客户端要能重试或者优雅降级。切换时间通常在几秒到几十秒这个窗口内活动可以考虑暂停入口避免大量失败请求。6.3 数据一致性的最后一道防线不管 Redis 层做得多严密最后一道防线永远是数据库唯一索引防重复订单条件更新防超卖。任何绕过数据库约束的优化都是危险的。我在方案评审时有个习惯先问如果 Redis 全错了数据库能不能兜住能兜住再谈性能优化兜不住先补数据库约束。这套东西说到底Redis 是矛帮你把性能做到极致数据库约束是盾保证最坏情况下数据不出错。两者缺一不可只谈性能不谈兜底早晚要还债。我个人在实际操作中的体会是秒杀优化最难的从来不是写那段 Lua 脚本而是把所有边界情况想全预热会不会重、消息会不会丢、回滚会不会多、缓存会不会旧、集群会不会跨 slot。这些细节没有一个是 Redis 命令层面的问题全是工程层面的严谨性。每次活动前把清单过一遍比事后救火轻松得多。