1零点秒杀:超卖、打挂库存与监控假 green 的叠加
某零售平台在 20:00 开启限量 500 件的联名款秒杀,活动页已 CDN 预热,网关配置 Sentinel 集群 QPS 上限 3 万(合成示例)。
T+0 按钮可点瞬间,seckill-service 峰值 QPS 达 8.6 万——
约为设计值的 2.8 倍,因客户端未做按钮防抖、CDN 边缘缓存未按 SKU 隔离,部分用户绕过静态页直接打 API。
T+30 s,库存服务 inventory-service 对 MySQL 行 UPDATE stock SET qty=qty-1 WHERE sku_id=? AND qty>0
的并发从压测时的 200/s 升至 1.2 万/s。InnoDB 在热点行上形成串行化队列,
连接池 500 槽位在 90 s 内 Acquire 超时率 38%,P99 RT 从 15 ms 升至 6.2 s。
同时 Redis 侧用 DECR 预扣库存,运维在 T+2 min 手工执行了一次「Redis 库存回填同步」脚本——
脚本未加分布式锁,与在线扣减并发,导致 Redis 显示仍有库存而 DB 已为 0。
T+3 min,订单异步消费者 order-consumer 从 RocketMQ 拉取 4.8 万条「秒杀成功」消息(合成示例),
实际可售仅 500 件。消费者按「Redis 已扣即创建订单」逻辑写库,未做 DB 二次校验,
最终成交订单 612 笔——超卖 112 笔,客服工单在 40 分钟内堆积 3800 条。
T+5 min,架构组查看监控:Redis CPU 62%、MySQL 主库 CPU 89%、网关 429 比例仅 4%, Dashboard 显示「流控正常」。值班同学的三个困惑正是本文要拆解的复合模式: 为何限流了还打满库存行?为何 Redis 扣了还会超卖?为何扩容订单消费者让局面更糟?
上述场景折射出一个在《亿级流量网站架构核心技术》中反复强调的误区:把秒杀等同于「页面静态化 + Redis 减一」。 作者李运华在书中将秒杀定义为在极短时间窗内,用有限库存承接远超常态的写请求, 核心矛盾不是「快」,而是在一致性边界内尽可能多地拒绝无效写。 流量削峰、库存锁、防超卖三者分别解决调用链上不同相位的压力: 削峰减少到达写路径的请求量,库存锁保证扣减操作的原子性与有序性,防超卖用多层校验与对账兜底最终一致窗口内的误差。
版本说明:本文模式层与语言无关,示例基于 Redis 6.x(原子命令与 Lua)、 RocketMQ 5.x(顺序消息与事务消息可选)、MySQL 8.0 InnoDB 行锁。 性能数字若无特别说明均为合成示例,机制描述以 Redis 官方文档、RocketMQ 文档与 《亿级流量网站架构核心技术》秒杀章节为准。
阅读前提:你已理解 HTTP 缓存、消息队列异步化、数据库事务与行锁基础。 本文不再解释「什么是秒杀」,而是聚焦削峰-锁-防超卖在失败窗口如何耦合、如何用架构手段编排。 50% 篇幅用于复合现场与排障顺序,35% 用于体系架构与机制推导,15% 留给 Redis/MQ 落地框架。 目标读者为已在生产环境处理过大促或秒杀事故的 P6-P7+ 工程师。 文中合成数字仅用于说明量级关系,不可直接当作容量规划依据。
Redis 的 DECR、INCR、SETNX 等单 key 操作在服务端单线程模型下原子执行;
跨多个 key 的复合逻辑须用 Lua 脚本或 Redis 6.2+ 的 SET key value NX GET 等扩展命令保证原子性。
「先 GET 再 DECR」的两步客户端逻辑在并发下必然竞态。
来源:Redis 官方文档 · Transactions & Lua;
与《亿级流量网站架构核心技术》「Redis 扣减库存」一节一致。
1.1 告警时间线:四类信号交织
复合秒杀事故的识别特征是入口流量、库存层 RT、消息堆积、成交对账差额四类信号在同一时间窗重叠, 而非单一 CPU 或 QPS 尖刺。下表按演练时间线整理(数字为合成示例):
| 时刻 | 入口层 | 库存层 | 消息层 | 业务侧 |
|---|---|---|---|---|
| T+0~30 s | QPS 超设计,静态页未完全生效 | Redis DECR 成功率高 | MQ 生产速率陡升 | 用户大量「排队中」 |
| T+30 s~2 min | 429 比例仍低,无效请求多 | DB 行锁排队,连接池告警 | 消费 lag 上升 | 部分用户「已成功」 |
| T+2~3 min | 运维手工同步 Redis | Redis/DB 库存不一致 | consumer 扩容 | 超卖订单开始出现 |
| T+3 min+ | 活动紧急下线 | 主库 CPU 100% | 死信与重复消费 | 客服工单爆发 |
1.2 与「单纯流量过大」的边界区分
| 维度 | 单纯流量过大(可扩容缓解) | 秒杀复合事故(需架构介入) |
|---|---|---|
| 库存行 | 无热点单行 | 单行/单 key 成为全局瓶颈 |
| 超卖 | 不出现 | Redis 与 DB 双写窗口内出现 |
| 扩容效果 | QPS 线性提升 | consumer 扩容加速错误落库 |
| 根因 | 容量规划不足 | 削峰缺失 + 锁粒度错误 + 无对账 |
1.3 现场应急:有序止损
复合秒杀事故的恢复顺序应为停写 → 对账 → 隔离 → 恢复: 先关闭活动入口与 MQ 生产(或切换「活动已结束」静态页),再统计 Redis 扣减量、DB 实存量、订单成交量的三角差额; 再评估是否暂停 consumer 扩容;最后才做数据修复与客诉预案。 顺序颠倒——尤其「先扩容 consumer 再对账」——往往把一次可控超卖变成需要批量关单的生产事件。 架构师在 war room 的第一职责是划定冻结变更窗口:禁止手工改 Redis 库存、禁止改扣减脚本、禁止无上限扩容消费者。
第 1 章复合场景还有一个易被忽略的细节:网关 429 比例仅 4% 却已超卖—— 这是异步链路过载型事故的典型特征。入口限流只保护同步路径, 大量请求已在 T+0~30 s 通过 Redis 预扣并写入 MQ,后续 consumer 处理能力才是瓶颈。 正确信号是MQ 消费 lag、DB 库存最终校验失败率、订单成交数与 SKU 可售量差额三者联动,而非单一 429。
1.4 OnCall 协作:三条子工单并行
复合秒杀事故下,中间件、业务、DBA 各持一条线索。架构师按语义拆单: 子工单 A(库存止损)——暂停 Redis 预扣接口、冻结 consumer 并发、审计是否有手工脚本; 子工单 B(入口治理)——CDN 切换静态「已结束」页、网关限流收紧、风控拉黑异常 IP 段; 子工单 C(业务对账)——统计超卖差额、暂停支付回调、客服话术与关单批次。 30 分钟后三线汇于:根因是 DB 同步扣减 + 双写竞态,放大是 consumer 扩容,误伤是 UI 过早承诺成功—— 而非「Redis 不够快」这种笼统结论。
书中案例常强调「减少写请求到达数据库」;现场 war room 却容易陷入「加 Redis 节点、加 DB 只读」的惯性。 只读副本对库存扣减写路径无帮助,反而分散 DBA 注意力。 架构师应坚持写路径单点收敛:所有扣减必须经过 Redis 预扣与 MQ 异步,DB 仅做最终确认。
2秒杀的三条不变量:读多写少、热点单行、一致性边界
《亿级流量网站架构核心技术》将秒杀业务特征概括为:读多写少、瞬时并发高、库存有限、成功比例极低。 李运华在书中强调,架构设计的第一性原理不是「支撑 10 万 QPS」,而是让 99% 以上的请求在到达数据库之前被无害拒绝。 三条不变量贯穿后文所有机制:
- 读多写少:活动页、商品详情、库存查询占流量大头;写路径(扣库存、创订单)必须极窄。
- 热点单行:单个 SKU 的库存在 DB 中通常对应一行或少量行,InnoDB 行锁使并发写退化为串行。
- 一致性边界:Redis 预扣与 DB 落库之间存在时间窗;架构必须声明「强一致点」在哪一层,其余用对账补偿。
若可售库存为 S、峰值并发请求为 Q,则任意「同步 DB 扣减」方案的有效吞吐上界约为单行锁吞吐(通常数百~数千 TPS),
与 Q 无关。因此秒杀架构的本质是漏斗:每层过滤后到达下一层的请求量应满足
Q_out << Q_in,且最后一层写 DB 的速率 ≤ 库存服务设计 TPS。
此推断与书中「层层过滤减少写请求」表述一致,不依赖具体中间件。
2.1 与常规大促下单的差异
| 维度 | 常规大促 | 限量秒杀 |
|---|---|---|
| 库存规模 | 万~百万级 SKU 分散 | 单 SKU 极少(百~千件) |
| 成功率 | 多数请求可成交 | 绝大多数请求应快速失败 |
| 写路径 | 可同步下单 | 必须异步 + 预扣 |
| 热点 | 可水平分片 | 单行/单 key 不可分 |
| 用户体验 | 完整购物车流程 | 「排队/结果页」简化交互 |
2.2 一致性声明:强一致点在哪
架构评审必须书面回答:「用户看到的『秒杀成功』以哪一层为准?」常见三档: Redis 扣减成功(最快,风险最高)、 MQ 持久化成功(至少一次,需幂等)、 DB 订单落库成功(最慢,最安全)。 书中推荐路径是 Redis 预扣 + MQ 异步 + DB 最终校验;用户 UI 应显示「排队中」直至 DB 确认, 而非 Redis 扣减后立即展示「购买成功」——后者是复合场景中客诉放大的主因之一。
与 T10(可靠性设计)衔接:秒杀链路禁止对库存 RPC 做多层无界重试; 与 T11(多级缓存)衔接:活动页静态化是读路径缓存的极端形态,库存读缓存 TTL 须短于活动周期并配合主动失效。
2.3 CAP 在秒杀语境下的务实选择
秒杀不是 CAP 理论课,但架构评审必须回答「活动窗口内允许哪类不一致」。 Redis 预扣相对 DB 是AP 倾向:优先可用与响应,接受毫秒~秒级窗口; DB 订单确认是CP 倾向:宁可关单也不超卖。 书中「最终一致性」方案隐含这一分工:用户感知的「抢到了」与财务认可的「成交了」必须是两个状态。 若业务方要求「点击即成交且零超卖」,只能走同步 DB 强一致路径,并诚实告知 QPS 上界—— 这不是技术不够,而是单行锁物理约束。
另一种常见误判是把「分布式锁」当作万能钥匙:在 Redis 扣减外加 Redisson 锁、在 DB 外加全局 ZK 锁, 锁层次越多,持有时间越长,零点反而更容易打挂。 秒杀场景应减少锁竞争面,而非增加锁:Lua 单 key 原子即足够,DB 侧用条件 UPDATE 代替显式 FOR UPDATE。
3流量削峰:从 CDN 到 MQ 的分层漏斗
书中「流量削峰」章节给出经典分层:浏览器缓存 → CDN → 静态活动页 → 网关限流 → 应用层验证码/风控 → Redis 预扣 → MQ 异步。 每一层的目标不是「处理更多请求」,而是减少到达下一层的有效写请求。 架构师常犯的错误是把所有层都当作「性能优化」,而忽略每层应明确的拒绝语义(403/429/ sold out / 排队页)。
3.1 分层职责表
| 层级 | 机制 | 过滤对象 | 典型误配 |
|---|---|---|---|
| L0 客户端 | 按钮防抖、本地倒计时 | 重复点击 | 仅 disable 按钮,API 仍可刷 |
| L1 CDN/静态 | 活动页 HTML/JS 静态化 | 读流量 | 静态页内嵌动态库存 API |
| L2 网关 | 集群 QPS、用户维度限流 | 恶意/超量 IP | 单实例限流,集群仍过载 |
| L3 应用 | 验证码、风控、登录态 | 脚本/黄牛 | 验证码在 Redis 扣减之后 |
| L4 Redis | 原子预扣、令牌桶 | 无库存请求 | GET+DECR 非原子 |
| L5 MQ | 异步排队 | 同步写压 | 无消费速率上限 |
活动页虽 CDN 缓存,若 JavaScript 在 T+0 仍轮询 /api/stock/{skuId} 或
/api/seckill/check,则读流量在零点瞬间打回源——静态化形同虚设。
正确做法:活动开始前将「按钮不可点」态 baked 进静态页;T+0 仅放开单次提交的 POST,
库存余量不在前端实时展示,或展示「已售罄/排队中」三态而非精确数字。
来源:生产经验;与书中「页面静态化」意图一致。
3.2 网关限流与集群一致性
Sentinel 或 Nginx limit_req 在秒杀场景须集群级生效:Redis 令牌桶、Sentinel 集群流控、或网关统一计数。
单实例 QPS 3000 × 20 实例 ≠ 集群 6 万,因流量不均与缓存击穿会导致热点实例先挂。
书中建议对秒杀 URI 单独配置资源名,与日常下单隔离,避免大促规则误伤普通交易。
限流响应体应返回明确业务码(如 SECKILL_RATE_LIMIT),便于客户端停止重试——
与 T10 所述「限流后禁止客户端重试风暴」同构。
3.3 验证码与风控的位置
验证码、设备指纹、黑名单应位于Redis 预扣之前,否则每次验证通过都触发一次库存相关操作。 灰产脚本可绕过前端按钮,直接 POST 秒杀 API;L3 层需与 WAF、风控平台联动。 活动前 24 h 应完成「脚本压测 + 风控规则」联合演练,而非仅压 QPS。
3.4 CDN 与浏览器缓存的精细控制
活动页 HTML 建议 Cache-Control: public, max-age=300 并在 T-5 min 主动 purge,
确保倒计时脚本与按钮态是最新;秒杀 API 必须 no-store,禁止 CDN 缓存 POST 响应。
书中「页面静态化」常被误解为「整个域名静态」——实际上仅商品展示与规则说明静态,
提交接口永远动态。若使用边缘函数(Edge Worker)做地域级限流,须与中心网关规则对齐,避免边缘放行、中心打满的双标。
读路径还可引入本地内存标记:活动未开始时,应用层直接返回 403,不触达 Redis。 这一层在漏斗中常被遗漏,却在「提前 1 秒刷接口」脚本面前极为有效。 合成压测显示(示例):去掉 L0~L1 后,同库存规模下 Redis 请求量可高出 40% 以上—— 足以把「刚好够用」的架构推到超卖窗口。
4库存锁:Redis 原子预扣与 DB 行锁的边界
「库存锁」在秒杀语境下通常指两层:Redis 层的原子扣减(承担 99% 流量)与
DB 层的行锁/乐观锁(最终一致与对账)。
书中经典方案是 Redis DECR 或 Lua 脚本判断 qty>0 再扣减;
DB 侧仅在异步消费或定时同步时更新,避免零点同步打热点行。
4.1 Redis 扣减:单命令 vs Lua
简单场景可用 DECR:初始库存 N,DECR 后若值 ≥0 则预扣成功,<0 则回滚(INCR)。
但 DECR 与判断分离在客户端时存在竞态;推荐 Lua:
-- 合成示例:单 SKU 库存 key
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
if stock <= 0 then return 0 end
redis.call('DECR', KEYS[1])
return 1
Lua 脚本在 Redis 中单线程执行,等价于事务。 Redis Cluster 下须保证同一 SKU 的库存 key 与令牌 key 在同一 slot(hash tag)。
MySQL InnoDB 对 UPDATE ... WHERE sku_id=? AND qty>0 在命中主键/唯一索引时加行级排他锁;
高并发下同一行 UPDATE 串行执行,吞吐受锁持有时间限制。
若 WHERE 条件无法走索引,会升级为表锁或大量间隙锁,性能急剧下降。
来源:MySQL 8.0 InnoDB Locking。
4.2 DB 锁:何时仍需要
Redis 预扣成功后,DB 不应在同步链路中再次 SELECT FOR UPDATE——
那是复合场景打挂库存行的直接原因。DB 扣减应发生在MQ 消费者内,
且消费速率受控(见第 6 章)。消费者内使用乐观锁 UPDATE ... SET qty=qty-1, version=version+1 WHERE sku_id=? AND version=?
或条件更新 qty>0,失败则回滚 Redis(补偿 INCR)并关闭订单。
| 方案 | 吞吐 | 一致性 | 适用阶段 |
|---|---|---|---|
| Redis Lua 预扣 | 极高 | 预扣态,非最终 | 同步秒杀 API |
| DB 悲观行锁 | 低 | 强 | 不推荐同步链路 |
| DB 乐观锁 | 中 | 强 | 异步 consumer |
| 分段库存(库存分桶) | 高 | 需汇总对账 | 超大流量备选 |
4.3 库存分桶:热点分散的代价
将 500 件库存拆为 10 个 Redis key 各 50 件,可并行 DECR 提升吞吐。 代价是全局余量不可精确展示,且桶间不均衡时需后台调度。 书中提及类似「库存分段」思路;架构师仅在单行锁已成为绝对瓶颈且业务接受「近似售罄」时采用, 并必须配套分桶对账任务。
4.4 Redis 高可用与故障切换窗口
主从切换或 Cluster failover 期间,可能出现短暂双主写或客户端路由滞后, 导致 DECR 重复执行或读到旧库存。活动前须确认客户端使用正确拓扑发现, 并对 failover 窗口做演练:切换 30 s 内是否暂停秒杀入口。 生产常见策略是活动峰值时段禁止 Redis 运维变更,与大促冻结同理。
若使用 Redisson 分布式锁包裹扣减,要警惕锁续期与业务超时错位: 锁未释放但 Lua 已执行完毕,下一请求可能重复扣减或死锁。 书中更推荐 Lua 原子而非外挂锁——锁是另一层竞争面,不是库存一致性的必需品。
5防超卖:令牌、幂等、对账与失败补偿
防超卖不是单一技术点,而是贯穿链路的不变量校验: 任意时刻,「Redis 已扣总量 + DB 未同步窗口」不应长期大于物理库存; 订单成交数不应大于可售 S。 书中强调「Redis 扣减 + 数据库最终一致」须配合超时关单、库存回滚、对账任务。
5.1 秒杀令牌(Token)
活动开始前或用户通过风控后,发放一次性令牌(UUID 写入 Redis,TTL 与活动时长一致)。 秒杀 POST 须携带 token,服务端 Lua 校验「token 存在且未使用 → 标记已用 → 扣库存 → 发 MQ」。 无 token 的请求在 L3 即拒绝,避免裸打扣减 API。 Token 与库存扣减在同一 Lua 中原子完成,防止「扣了库存但未绑用户」的悬挂态。
用户 UI 若在 Redis DECR 成功后立即跳转支付,而 DB 异步落库失败或超卖关单,
将产生大量「已支付无货」资损。正确 UX:展示「排队中/结果查询中」,
轮询订单状态接口;仅当 DB 订单状态为 CONFIRMED 才进入支付。
复合场景中超卖 112 笔的部分原因,是前端把 MQ 生产成功误当作成交。
5.2 幂等与重复提交
客户端重试、MQ 至少一次投递、consumer 重启,均会导致重复消费。
幂等键设计:userId + activityId + skuId 或 token 本身。
消费者写订单前 INSERT IGNORE 或 Redis SETNX idempotent_key;
DB 侧对业务唯一键加 UNIQUE 约束。重复消息应安全返回,而非再次扣 DB 库存。
5.3 对账与补偿闭环
定时任务(每分钟或活动结束后)执行三角对账: 物理库存 S、Redis 剩余 + 已扣、DB 订单成交数。 差额 > 0 触发告警与自动关单;Redis 多扣则 INCR 回补;DB 少扣则人工介入。 手工运维脚本必须走变更流程,禁止 war room 即兴执行——复合场景的 Redis 回填竞态即由此而来。
5.4 支付衔接与关单顺序
防超卖不仅止于库存,还止于支付通道。
若用户在 DB 未确认前完成支付,关单将演变为退款纠纷。
推荐顺序:DB 订单 CONFIRMED → 创建支付单 → 唤起收银台。
支付回调须校验订单状态与库存扣减记录,重复回调幂等。
书中「异步下单」章节隐含这一时序;许多团队事故来自产品要求「先付后占库存」的逆向流程。
Saga 补偿链应文档化:DB 扣减失败 → INCR Redis → 关单 → 若已支付则退款。 每一步失败都应有重试队列与人工兜底表,而非依赖 OnCall 手工 SQL。 补偿 INCR 必须与原始 DECR 使用同一 key 与活动维度,避免回补到错误 SKU。
6异步下单:MQ 削峰、背压与消费速率控制
Redis 预扣成功后,同步 API 应尽快返回(「排队中」), 将创单、扣 DB 库存、写支付单等重操作投递至 MQ。 这是书中「异步下单」的核心:用消息堆积换 DB 写吞吐平滑,但堆积本身须设上限与 dead letter 策略。
6.1 生产与消费速率
若峰值 800 次/s Redis 预扣成功,而 DB consumer 仅 200 TPS,则 lag 线性增长。 复合场景中 4.8 万条堆积即源于无消费速率上限 + 盲目扩容。 正确做法:
- consumer 侧固定并发 + 令牌桶,使 DB 写 TPS ≤ 设计值;
- MQ 堆积超过水位(如 5000)时,暂停 Redis 预扣或返回「系统繁忙」——反向背压;
- 扩容 consumer 前须确认 DB 容量,否则加速错误落库。
秒杀活动进行中禁止上调 consumer 并发或批量扩容 Pod。 曾有多起事故在 lag 高时水平扩容 consumer,DB 写 TPS 瞬间翻倍, 热点行锁等待时间拉长,反而增加「扣减超时 → 重复消费 → 超卖」概率。 堆积应对策略是入口背压 + 延长结果查询,而非无限加速消费。 并发变更仅允许在活动前压测标定,活动中仅允许降并发。
6.2 顺序与分区
同一 SKU 的消息应进入同一队列分区(RocketMQ MessageQueue 或 Kafka partition), 保证同一库存行的 DB 更新串行,减少乐观锁冲突。 不同 SKU 可并行消费。顺序消息会降低吞吐,秒杀单 SKU 场景通常可接受。
6.3 失败处理:关单与库存回滚
consumer 内 DB 扣减失败(qty=0 或超时)时:
① 标记订单 CLOSED;② Redis INCR 回补;③ 若已发支付消息则撤销。
三步须在同一事务或 Saga 中定义补偿顺序,避免「DB 失败但 Redis 未回补」。
延迟关单任务扫描「MQ 已消费但 DB 无订单」的悬挂记录,与对账任务互补。
6.4 事务消息与本地消息表:何时升级
RocketMQ 事务消息或「本地消息表 + 定时投递」可在Redis 扣减与 MQ 发送之间提供更强一致, 代价是延迟与实现复杂度上升。书中标准秒杀链路通常接受「Lua 成功 + 同步发 MQ + 对账兜底」; 当资损案例证明 MQ 发送失败窗口不可接受时,再升级为事务消息。 架构评审应量化窗口:若 MQ 发送失败率 < 0.01% 且对账周期 60 s,多数业务可接受。
本地消息表方案将「待发送消息」与业务操作同事务写入 DB,后台线程扫描投递。 秒杀峰值下该表也会成为写热点——须与订单库分表或独立库,避免与库存行锁叠加。 这是「框架 15%」中的进阶选项,不是零点第一版必选。
7经典方案边界与选型矩阵
单机制文档看多了容易「处处 Redis + MQ」。 架构师需要的是场景 → 链路组合的决策矩阵,而非清单式堆叠。 下表基于《亿级流量网站架构核心技术》常见方案,标注适用边界(非绝对优劣)。
| 场景 | 削峰 | 库存锁 | 防超卖 | 备注 |
|---|---|---|---|---|
| 单 SKU 限量秒杀 | 静态页+网关 | Redis Lua | Token+对账 | 本文主线 |
| 多 SKU 同时开抢 | 分资源限流 | 分 key | 分 SKU 对账 | 避免单队列热点 |
| 库存较多(千级以上) | 可弱化静态 | DB 乐观锁可行 | 标准幂等 | 接近普通大促 |
| 强一致零超卖(票务) | 全漏斗 | DB 唯一约束 | 同步落库 | 牺牲 QPS |
组合得当
- 静态活动页 + 集群网关限流 + Redis Lua 预扣
- Token 与扣减同事务 + MQ 背压
- consumer 速率 ≤ DB 设计 TPS
- 三角对账 + 活动冻结运维变更
组合失当
- 同步 DB 行锁扛零点流量
- Redis GET+DECR 两步扣减
- lag 高时盲目扩容 consumer
- Redis 扣成功即展示购买成功
7.1 决策树:零点告警时先查什么
- 成交订单数是否已 > 可售 S?→ 是:立即停入口 + 对账,禁止改 Redis。
- Redis 剩余是否与 DB 物理库存一致?→ 否:查是否有手工脚本/双写竞态。
- MQ lag 是否上升且 consumer CPU 低?→ 是:瓶颈在 DB 行锁,非 consumer 算力。
- 429 低但 DB 连接满?→ 是:无效流量绕过静态页,收紧 L2/L3。
- 是否可降级为「抽签/预约」?→ 是:改同步抢为异步公布结果,彻底削峰。
7.2 与书中章节的映射
《亿级流量网站架构核心技术》「秒杀架构」「缓存」「消息队列」三章与本篇一一对应: 秒杀架构讲整体漏斗,缓存讲 Redis 预扣,消息队列讲异步下单。 架构师评审时应要求方案说清属于哪一层机制、与相邻层如何边界, 而非笼统「上 Redis」。与 T06(Kafka 万亿链路)衔接:MQ 选型差异不改变「背压 + 幂等」不变量。
7.3 典型踩坑案例速查(合成摘要)
案例 A(同步 DB 扛流量):零点全部请求直打 UPDATE stock,500 库存撑不过 3 s,连接池耗尽全站下单受影响。
规避:Redis 预扣 + 异步 DB。案例 B(双写竞态):运维脚本与在线 DECR 并发,Redis 显示有货 DB 已零。
规避:活动冻结变更 + 对账。案例 C(consumer 扩容):lag 高时 Pod 从 10 扩到 40,超卖订单 20 分钟内翻倍。
规避:背压 + 降并发。三案例规律:写路径越宽,一致性越难;扩容不替代漏斗设计。
案例 D(黄牛占库存):Token 未与用户绑定,脚本批量领取 token 后转卖。 规避:Token 与 userId 绑定 + 设备指纹 + 单用户单 token。 案例 E(缓存击穿活动配置):活动开关存 Redis,key 过期瞬间全员打 DB 读配置。 规避:永不过期 + 逻辑过期或本地缓存活动元数据——与 T11 击穿模型同构。
8Redis + RocketMQ + Sentinel 编排要点
框架层占本文约 15%,仅固定「应配什么、不应配什么」,完整 API 以官方文档为准。 目标是把第 3~6 章机制映射到可评审的配置项与代码边界。
8.1 Redis:Key 设计与预热
- 库存 key:
seckill:stock:{activityId}:{skuId},活动开始前SET为可售量; - 令牌 key:
seckill:token:{tokenId},TTL = 活动时长 + 缓冲; - 幂等 key:
seckill:done:{userId}:{activityId},防止重复 POST; - 预热:避免 T+0 冷 key;大 key 拆分时用 hash tag 保证 Lua 单 slot。
8.2 RocketMQ:Topic 与消费
Topic 按活动或 SKU 隔离,避免与普通订单共 Topic 导致优先级反转。
生产者使用同步发送 + 超时(如 500 ms),失败则 Redis INCR 回补并返回「请重试」。
Consumer 配置 consumeThreadMin/Max 与 DB 容量匹配;
开启消费失败重试次数上限,超过进入 DLQ 人工处理,避免无限重试扣库存。
8.3 Sentinel:秒杀 URI 隔离
为 /api/seckill/submit 单独定义资源,集群 QPS 与热点参数规则分离。
流控触发返回 429 + 业务码,客户端识别后停止重试。
勿与库存服务 RPC 共用同一规则——后者应配并发线程数上限(舱壁),见 T10。
// 合成示例:Lua 内联逻辑伪代码(非生产拷贝)
EVAL "token+stock+mq" 3 tokenKey stockKey userKey
// 1. 校验 token 未使用 2. DECR stock 3. SET 幂等键 4. 返回 OK 供应用发 MQ
实际 MQ 发送不宜放入 Lua(Redis 脚本不宜做网络 I/O); 标准模式是 Lua 完成 token+stock+幂等,应用收到 OK 后再发 MQ,失败则 Lua 回补—— 此窗口须极短,并通过对账兜底。
8.4 配置变更与版本对齐
Lua 脚本、consumer 并发、Sentinel QPS 应纳入配置中心版本管理,prod 变更双人复核。
曾发生 test 环境 100 QPS 规则误推 prod 导致秒杀全拒的事故——与 T10 Nacos 误推案例同构。
Redis 脚本使用 SCRIPT LOAD 固定 SHA,避免每次 EVAL 长脚本增加带宽;
脚本变更须灰度实例验证后再全量,活动中禁止变更 SHA。
Dubbo 或 Spring Cloud 调用链若对秒杀服务做超时重试,须在上游显式关闭对写路径的重试。 框架默认「失败重试 3 次」在秒杀场景是放大器,不是容错。 评审清单应包含:全链路重试地图,标注每一跳最大尝试次数与总超时预算。
9全链路压测:从漏斗核算到复盘字段
秒杀架构未经压测等于未上线。 建议分 L1/L2/L3 三级:L1 Redis 单 key 扣减极限;L2 全链路不含 DB;L3 含 DB consumer 与对账。
| 演练级别 | 范围 | 通过标准 | 负责人 |
|---|---|---|---|
| L1 | Redis Lua 扣减 | 无竞态,qty 不为负 | 开发 |
| L2 | 网关+API+MQ 生产 | 429 比例可控,无 OOM | SRE |
| L3 | 含 DB consumer | 成交数 ≤ S,三角对账=0 | 架构委员会 |
9.1 必看指标(非 CPU)
秒杀大盘应同屏展示:Redis 库存剩余、MQ 生产/消费速率与 lag、 DB 库存行 UPDATE RT 与锁等待、订单成交累计 vs S、 关单/回补次数。 单独 QPS 绿点无法发现「lag 堆积 + 超卖」——与第 1 章复合场景一致。
9.2 Postmortem 必填字段
每次秒杀事故复盘应写入: 超卖差额、Redis/DB 不一致持续时间、 MQ 最大 lag 与 consumer 变更记录、手工运维脚本是否执行。 四字段缺失的复盘无法反馈到架构改进,只会重复「下次加强限流」。
压测环境须与生产同构:相同 Lua 脚本、相同 consumer 并发、相同 DB 索引。 shadow 库若无热点行锁真实行为,L3 结论不可信。 若 shadow 不可用,最低限度做桌面推演:按第 7 章决策树逐步走查,记录每步 RTO。
9.3 混沌注入与 shadow 隔离
建议使用 Chaos Mesh 或 Toxiproxy 对 Redis 注入 200 ms 延迟、对 MQ 注入消费暂停 60 s, 观察:入口是否触发背压、对账是否报警、UI 是否仍承诺成功。 混沌实验必须在 shadow 集群执行,禁止生产直接注入 DB 慢 SQL。 通过标准:超卖差额始终为 0,且 lag 水位触发后新预扣暂停。
压测脚本应模拟真实失败模式:10% 客户端重复 POST、5% MQ 发送失败、consumer 随机 crash—— 而非仅均匀流量。均匀流量压测最容易给出「我们能扛 5 万 QPS」的虚假结论, 零点却是重复点击 + 脚本 + 运维变更的叠加。李运华在书中用「减少写请求」而非「提高写吞吐」措辞, 压测设计应与此一致:验证漏斗各层拒绝率,而非单点极限 QPS。
10秒杀上线检查表、总结与延伸阅读
10.1 活动前检查表
| 检查项 | 标准 | 责任人 |
|---|---|---|
| 静态活动页 | T+0 无动态库存轮询;按钮单次提交 | 前端 |
| 网关集群限流 | 秒杀 URI 独立资源,集群 QPS 已压测 | SRE |
| Redis 扣减 | Lua 原子;key 已预热;无 GET+DECR | 开发 |
| Token/幂等 | 与扣减同脚本或同事务窗口;UNIQUE 约束 | 开发 |
| MQ 背压 | lag 水位 + 暂停预扣策略已演练 | 架构 |
| consumer 速率 | 并发 ≤ DB 设计 TPS;活动中冻结扩容 | SRE |
| 三角对账 | 任务已部署;告警阈值已设 | 数据 |
| UX 一致点 | 仅 DB 确认后可支付;排队态文案 | 产品 |
| 运维冻结 | 活动中禁止手工改 Redis/扩容 consumer | 运维 |
| L3 压测报告 | 成交数 ≤ S;对账差额 = 0 | 架构委员会 |
10.2 合成复盘:三条错误路径对照
用文首复合场景做反事实训练(数字合成):路径 A 是零点发现 Redis 库存异常后立刻扩容 consumer, 结果 backlog 更快灌进 DB,超卖差额从千级放大到万级。路径 B 是只加网关限流、不改扣减原子性, 面板变绿但 GET+DECR 竞态仍在。路径 C 是冻结 consumer → Lua 原子扣减 + Token → 三角对账清零 → 再按压测报告解冻。路径 C 的恢复未必最快,但复发率与资损最低——这才是秒杀架构要优化的目标函数。
组织层约束:活动中禁止「只改容量、不附对账与成交数证明」的紧急变更; 任何临时扩容必须在事后复盘标记为高风险措施,并在下一迭代补齐证据或回滚配置,不得默认长期保留在生产环境。
10.3 总结
秒杀架构的终点不是「扛住 10 万 QPS」,而是在有限库存 S 与无限点击 Q之间 建立可证明的漏斗与一致点:削峰让绝大多数请求止于 Redis 之前, 库存锁让写路径可串行化且可补偿,防超卖让三角对账在窗口关闭后仍成立。 复合场景表明:限流绿点、Redis 低 CPU、consumer 可扩容—— 三者任一单独成立都不能推出「不会超卖」。 架构师的价值是把失败窗口写进设计,而不是在零点第一次发现。
落地时建议把本文检查表嵌入发布平台:活动创建即触发 L3 压测报告上传、对账任务启用确认、 运维冻结窗口自动写入日历。机制不变量比组件品牌更持久—— Redis 与 MQ 的版本会变,「写路径窄、一致点清晰、可对账」不会变。 下一篇 T13 分布式事务将进一步讨论跨服务成交与库存确认的边界,与本章异步下单自然衔接。
10.4 延伸阅读
- BOOK李运华《亿级流量网站架构核心技术》—— 秒杀架构、缓存、消息队列章节
- DOCRedis Documentation · Transactions & Lua Scripting
- DOCApache RocketMQ · Ordered Message
- DOCMySQL 8.0 Reference · InnoDB Locking
- DOCSentinel · 流量控制
- SERIES同系列 T10 可靠性设计、T11 多级缓存、T06 Kafka 削峰(architect-series)