场景一 · 秒杀 / 抢票
高并发场景 · Scenario 01

场景一:秒杀 / 抢票

整点开抢的那一秒,流量瞬间放大几百上千倍。这是高并发领域最"暴力"也最经典的场景:百万人抢一万件货,系统不能崩,货还一件都不能多卖。

瞬时脉冲 强一致扣减 漏斗拦截 Redis 预扣减
1000×瞬时流量放大倍数
<1%真正能成交的请求占比
0 容忍超卖(多卖)容忍度
秒级流量洪峰持续时间
📑 本页目录
01

场景描述与业务特征

双11 零点抢半价 iPhone、周杰伦演唱会开票、12306 春运抢票——它们的共同剧本是:货极少、人极多、时间极短。就像演唱会场馆只有一个小门,开门瞬间上万人一起往里挤。

QPS 时间 → 20:00:00 开抢 → 峰值可达日常 1000 倍 开抢前:安静得可怕 几秒后:库存售罄,流量退潮
脉冲型流量:平时用不上的容量,只为那几秒钟存在——所以"硬扛"是最亏的方案。

⚡ 瞬时脉冲

流量集中在开抢后的几秒到几十秒,之前风平浪静,之后迅速退潮。为峰值买一整年的服务器,纯属浪费。

📉 请求极度"无效"

1 万件库存、100 万人抢,注定 99% 的请求拿不到货。设计核心思路:让无效请求死得越早、越便宜越好。

🔒 强一致扣减

库存是不能吹牛的数字:多卖一件就是资损和客诉。允许少卖(回滚),绝不允许超卖。

🤖 真人混着机器人

黄牛脚本、刷子军团往往占流量大头,风控与验证码是隐形的第一道闸门。

🧊 读写倒挂

开抢前全是读(刷新页面看倒计时),开抢瞬间转为写(下单扣库存),读写洪峰前后脚到达

💥 连坐风险

秒杀流量若与正常业务共用数据库,一旦打挂,整个站点跟着陪葬——所以要隔离部署。

02

整体运作流程

成熟的秒杀架构是一个层层过滤的漏斗:100 万请求进来,每一层都拦掉一大批,真正摸到数据库的只剩几千。就像演唱会安检:先看有没有票(静态页/CDN)、再验身份(网关限流)、再安检(Redis 预扣减)、最后才进场(数据库落单)。

① CDN + 静态化页面(挡掉刷新流量) 按钮置灰倒计时 · 页面全静态 · 100万请求 → 只有点击"抢"的往下走 ② 网关层:限流 + 风控 + 验证码(拦掉 80%) 令牌桶限流 · 黑名单 · 答题/滑块劝退脚本 → ~20万 ③ Redis 预扣减库存(内存里定生死) Lua 脚本原子扣减 · 扣到=资格 · 扣不到=秒回"已抢完" → ~1万 ④ 消息队列削峰(排队进场) 拿到资格的请求进 MQ · 下游按自己的节奏消费 ⑤ 订单服务 + 数据库落单 真正写 DB 的只剩几千 QPS · 从容处理 100万 20万 1万 1万 99% 请求 止步于此 核心思想:无效请求死得越早,成本越低 —— "漏斗式层层拦截"
🧠
为什么 Redis 能定生死?Redis 单机内存操作可达 10 万+ QPS,是 MySQL 行锁扣减的几十倍。让 Redis 当"资格发放处",数据库只处理拿到资格的少数人——这就是"预扣减"的精髓。
03

核心瓶颈与数据一致性挑战

🔥 单行热点:所有人挤同一行库存

百万请求最终都要更新 同一行库存记录。MySQL 行锁让请求排成串行长队,锁等待雪崩,连接池瞬间打满——这是秒杀的"物理极限"所在。

💸 超卖:最经典的一致性事故

朴素写法"先查库存再扣减",两个请求同时查到"还剩 1 件",双双下单成功——卖出 2 件。检查与扣减不是原子操作,就是事故的温床。

👻 少卖:预扣减的副作用

Redis 扣了库存,用户却没付款/下单失败,如果不把额度还回去,商品明明有货却显示售罄——需要超时回补机制

🎭 缓存与数据库的双账本

库存同时存在于 Redis 和 MySQL 两本账上,宕机、消息丢失都会导致两本账对不上——必须以谁为准、如何对账,要提前想清楚

💥
事故现场还原:某电商大促,开发同学用 UPDATE stock SET n = n - 1 WHERE id = 1 但忘了加 AND n > 0,库存被扣成 -4327,多卖四千余单,只能一单一单打电话道歉赔券。一致性挑战不是玄学,是真金白银。
04

优化手段、获益与代价

① 页面静态化 + CDN 拦截层

活动页做成纯静态推到 CDN 边缘节点,开抢前按钮置灰,倒计时由前端本地计算。

✅ 获益:90% 以上的"刷新围观"流量根本不碰源站,最便宜的一道防线。
⚠️ 代价:页面时效性变弱;改价格/改时间需要刷新 CDN 缓存,操作失误就是事故。

② 网关限流 + 验证码/答题 拦截层

令牌桶/漏桶限流按容量放行;滑块、答题打乱脚本节奏,把机器人挡在门外。

✅ 获益:后端只承接"设计容量内"的流量;黄牛脚本成本大增。
⚠️ 代价:真实用户体验变差(多点一步);限流阈值拍脑袋定,太紧误伤、太松失守。

③ Redis + Lua 原子预扣减 核心

库存提前加载进 Redis,用 Lua 脚本把"判断+扣减"合成一个原子操作,扣到即获得下单资格。

✅ 获益:扛住 10 万级 QPS 扣减;数据库压力降低两个数量级;天然防超卖。
⚠️ 代价:引入两本账(Redis/DB)的对账复杂度;Redis 宕机需预案;未支付订单要做额度回补。

④ 消息队列异步下单 削峰

拿到资格后请求进 MQ(RocketMQ/Kafka),订单服务按自身消费能力匀速处理,前端轮询/推送结果。

✅ 获益:把"秒级洪峰"抹平成"分钟级细流",下游永远不会被打垮。
⚠️ 代价:下单从同步变异步,用户要等结果("排队中…");引入消息丢失/重复消费问题,消费端必须幂等。

⑤ 库存分片(热点打散) 进阶

把 1 万件库存拆成 100 个桶(每桶 100 件)分散到多个 Redis 分片/多行记录,请求随机路由到桶。

✅ 获益:单点热 Key 变成 100 个温 Key,突破单分片吞吐上限。
⚠️ 代价:桶间余量不均("A桶售罄B桶有货")需要二次路由或桶间调度,复杂度明显上升。
🛡️
降级策略(保命三板斧):① 流量超过预案上限 → 随机丢弃部分请求,返回"太火爆请重试"(牺牲部分可用性保全局);② Redis 故障 → 降级为数据库扣减 + 更狠的限流(容量骤降但不至于超卖);③ 全链路告急 → 熔断秒杀入口,只保主站交易不受连累。秒杀是典型的"性能+一致性优先、牺牲可用性"的三角站位。
05

多种解决方案对比与选型建议

方案防超卖能力吞吐上限实现复杂度适用规模
数据库悲观锁
SELECT ... FOR UPDATE
低 · 数百QPS极简内部系统、小型活动
数据库乐观锁
UPDATE ... WHERE stock>0
中 · 数千QPS简单万级并发的中小促销
Redis+Lua 预扣减 + MQ
本页主推架构
强(需对账兜底)高 · 10万+QPS中等大型电商秒杀主流方案
本地缓存分桶+分片
JVM内存扣减+异步汇总
最终一致极高 · 百万QPS复杂12306/头部大厂级别
答题/抽签制
先收集意向再摇号
不限(无瞬时峰)简单公平性优先(摇号购车/购房)
🎯
选型口诀:千级并发用乐观锁就够;万到十万级上 Redis 预扣减 + MQ;再往上要么分桶分片堆工程,要么干脆改产品规则(抽签制把脉冲拍平——最优雅的高并发方案是让高并发不发生)。切忌小活动上重型架构:复杂度本身也是故障源。
06

推演检验:换个条件还成立吗?

背下架构图不算学会,能判断"条件变了方案还成不成立"才算。先自己推,再展开参考。

推演 1:如果库存从 1 万件变成 1000 万件(基本人人有份),这套漏斗架构还需要吗?

💡 参考推演

拆成两个维度看:流量脉冲仍在(开售瞬间大家还是一起点),所以 CDN 静态化、网关限流、MQ 削峰这些"为流量服务"的层仍然需要;但稀缺性消失了,防超卖压力骤降——Redis 预扣减可以退化成 DB 乐观锁甚至不预扣。结论:拦截层为流量服务,一致性层为稀缺服务,两者要分开评估,不是打包取舍。

推演 2:如果开抢进行到一半 Redis 集群整体宕机,降级成数据库悲观锁硬顶,会发生什么?

💡 参考推演

行锁把并发捣成串行,吞吐从 10 万级跌到几百 QPS,连接池瞬间占满,若与正常业务共库还会拖垮全站——这就是"连坐"。正确姿势不是硬顶,而是同步把限流阀值降到 DB 容量以内(容量骤降但不超卖),或干脆熔断秒杀入口保主站。降级从来不是"换个组件继续扯",而是"换个组件 + 同比例收缩流量"。

推演 3:如果业务方新增硬性要求"绝不能少卖"(预扣额度不回补就是亏损),架构要加什么?

💡 参考推演

回补机制从"可选"变"必修":订单超时未支付 → 取消订单 → 额度原子归还 Redis,再加定时对账(Redis 余额 + 已成交订单 应等于 总库存)兑底修正。代价:复杂度上升,而且会出现"显示售罄又复活"的体验问题——技术指标(不少卖)和体验指标(不诈尸)本身也在博弈,需要产品一起拍板。