高并发写架构 — 秒杀 / 抢单 / 发帖怎么扛
系统设计 ⭐⭐⭐ 进阶 🔥🔥🔥 必考
💡 核心要点
高并发写的核心心智模型是「用前面的层挡掉无效请求 + 用 MQ 把同步变异步 + 用 Redis 做扣减预处理」。写流量比读流量难一个数量级,因为写没法"缓存复用",每个请求都要落库。秒杀题不是问"怎么写得快",是问"怎么挡掉 99% 的请求只让 1% 进数据库"。
这页解决什么问题
面试题:
- "设计秒杀系统" / "双 11 库存扣减" / "春运抢票下单"
- "设计微博发帖" / "朋友圈发布"
- "设计支付下单" / "外卖下单 + 派单"
- "点赞 / 关注 / 收藏这种轻写量怎么扛"
写流量的共同特征:
- 必须落库(不能像读那样靠缓存命中跳过 DB)
- 强一致要求高(订单不能丢、库存不能超卖)
- 写放大效应(一条微博推给 1000 follower → 1 写 = 1000 推送)
与读架构的关键差异:
| 维度 | 读架构 | 写架构 |
|---|---|---|
| 缓存策略 | 多级缓存吃 99% 流量 | 缓存只能做"扣减预处理",写还得落库 |
| 一致性 | 接受秒级延迟 | 必须强一致(订单 / 库存) |
| 削峰方式 | 缓存命中削峰 | MQ 削峰 + 限流前置拦截 |
| 数据库压力 | 读副本扩展 | 主库瓶颈 → 分库分表 |
| 异步化 | 不需要 | 必须(用户秒响应、后台慢慢写) |
全链路架构总览(秒杀场景)
100w 用户点击 "立即购买"
│
▼
┌──────────────────────────┐
① CDN │ CDN: 秒杀按钮静态化 │ 挡 0%(CDN 不挡写请求)
│ + 验证码/答题前置降速 │ → 100w QPS
└────────┬─────────────────┘
▼
┌──────────────────────────┐
② GW │ 网关: 限流 + 黑名单 │ 挡 90% → 剩 10w
│ - 单用户 1 次/秒 │
│ - 黄牛 IP 黑名单 │
│ - 验证码失败拦截 │
└────────┬─────────────────┘
▼
┌──────────────────────────┐
③ APP │ 应用层: Redis 预扣库存 │ 挡 99% → 剩 1000
│ DECR stock:item_999 │
│ 失败的直接返回 "已售罄" │
└────────┬─────────────────┘
│ 1000 真正下单
▼
┌──────────────────────────┐
④ MQ │ 消息队列削峰 │ 写入 Kafka/RocketMQ
│ 返回用户 "排队中..." │
└────────┬─────────────────┘
▼ 后台消费者按 DB 承载力慢慢写
┌──────────────────────────┐
⑤ DB │ 数据库: 落单 │ 实际写入 1000 条订单
│ 分库分表(按 userId) │
└──────────────────────────┘金句:「100w 流量进来,只让 1000 个到 MQ,最后 DB 收到 1000 条订单。99.9% 流量被 Redis 挡掉」。
每层方案与细节
① CDN — 把"秒杀按钮"挡在外面
写请求 CDN 没法直接缓存,但能做两件事:
1. 秒杀页面静态化:商品图、规则文案、按钮的 JS / CSS 全部静态化推 CDN,减少回源压力。
2. 前置验证码 / 答题(关键降速):
- 用户点"立即购买" → 弹出验证码 / 数学题
- 答完才允许真正发起下单请求
- 削峰 + 防机器人 一举两得
12306 春运的"答题验证"就是这个机制——把瞬时 100w QPS 摊到 30 秒,每秒只有 3w 答完题的"真用户"进来。
② API 网关 — 多维度限流挡 90%
写场景的网关比读场景更狠:
| 限流维度 | 阈值 | 目的 |
|---|---|---|
| 单用户 | 1 次/秒 | 防点击狂魔 |
| 单 IP | 10 次/秒 | 防机房代理刷 |
| 单接口 | 总 QPS 限到 10w | 兜底保护 |
| 黑名单 | 历史黄牛 / 异常 IP | 风控前置 |
面试加分点:黑名单不光是限流,风控系统会做"账号特征评分"——新注册账号、设备指纹异常、有刷单历史 → 直接拒绝或限流到 0.1 次/秒。
③ 应用层 — Redis 预扣库存(最关键)
核心思路:库存数据先放 Redis,每次下单 DECR stock:item_999,Redis 返回 < 0 直接拒绝,根本不到 DB。
// 伪代码
public OrderResult tryPurchase(long userId, long itemId) {
// 1. 用户级限流(防同账号多次下单)
if (!checkUserLimit(userId, itemId)) {
return OrderResult.fail("请勿重复提交");
}
// 2. Redis 预扣(原子操作)
Long remaining = redis.decr("stock:" + itemId);
if (remaining < 0) {
redis.incr("stock:" + itemId); // ★ 立即回滚(不超卖)
return OrderResult.fail("已售罄");
}
// 3. 创建预订单(带去重 Idempotency-Key)
String orderId = UUID.randomUUID().toString();
redis.setex("preorder:" + orderId, 300, JSON.toJSONString(new Order(...)));
// 4. 写 MQ 异步落库
mq.send("order_queue", orderId);
// 5. 立刻返回(用户看到"排队中")
return OrderResult.pending(orderId);
}关键设计:
- DECR 是原子操作:Redis 单线程保证不超卖(这是 Redis 在秒杀里的核心价值)
- 失败回滚 INCR:异常时立即归还库存
- 预订单进 Redis:防止 MQ 消费失败时订单丢失
- 同步返回 pending:用户体验好,后台慢慢写
④ MQ 削峰 — 同步转异步
核心目的:让数据库按自己节奏慢慢消费,不被瞬时流量打垮。
1000 QPS 预扣成功
↓
Kafka/RocketMQ 缓冲
↓
消费者按 200 QPS 写 DB(DB 极限的 50%,留 buffer)
↓
完成订单 → 状态通知用户(轮询 / WebSocket)MQ 选型:
| 场景 | 推荐 |
|---|---|
| 秒杀 / 单 region | RocketMQ(事务消息成熟,金融场景多) |
| 日志 / 全球高吞吐 | Kafka |
| 简单业务 | Redis Stream |
必须考虑的 3 个细节:
- 消息不丢:MQ 持久化 + 消费 ACK + 失败重试
- 幂等消费:网络重试可能消费两次,用
Idempotency-Key去重(详见 幂等性) - 死信队列:连续失败的消息进 DLQ,人工排查
⑤ 数据库 — 分库分表防瓶颈
单库 MySQL 极限:写约 3000-5000 QPS。
1000 QPS 写 → 单库够用(消费者按 DB 节奏控速)。但当业务起飞,长期高并发写必须分库:
按 userId 取模分 16 库 × 16 表 = 256 物理表
单表 ~ 500 万行,单库 QPS 3000-5000
总容量:256 × 500w = 12.8 亿行
总写入:16 × 5000 = 8w QPS分库分表中间件:ShardingSphere、TiDB、PolarDB-X(详见 存储选型)。
热点行问题:秒杀时所有人都在改一条库存记录(哪怕 Redis 挡掉了,DB 还是要写 1000 次同一行)。
解法:库存分桶
-- 原表
stock(item_id, count) -- 单行被 1000 个事务串行
-- 拆桶
stock_bucket(item_id, bucket_id, count)
-- 1000 个事务分到 10 个 bucket → 单 bucket 100 个串行
-- 库存查询时 SUM(count) GROUP BY item_id关键陷阱与对策
陷阱 1:超卖(最经典)
原因:Redis 预扣后、DB 落单前的窗口,并发请求都看到"还有库存"。
对策:
- Redis DECR + INCR 兜底(已在上面代码体现)
- DB 乐观锁兜底:
UPDATE stock SET count = count - 1 WHERE item_id = ? AND count >= 1 - 库存预热:秒杀前提前把库存从 DB 同步到 Redis
陷阱 2:少卖
原因:用户支付超时取消、消息消费失败但没回滚库存。
对策:
- 延迟队列:订单 15 分钟未支付 → 自动取消 + 归还库存(RocketMQ 延迟消息 / Redis ZSet)
- 对账 Job:定时扫描"Redis 预扣 > 实际订单数"差异,回滚
陷阱 3:黄牛刷单
对策:
- 风控前置:账号年龄 / 设备指纹 / 行为评分
- 验证码 / 答题:CDN 层降速
- 限购规则:单账号 / 单 IP / 单设备每场限购数量
- 延迟揭晓:预约抽签(小米早期)—— 把"先到先得"改成"先约后抽",本质是把瞬时洪峰摊平到几小时
陷阱 4:DB 写放大(社交场景)
微博发帖:你发 1 条 → 推给 1000 follower 的 timeline → DB 要写 1000 次
对策:详见 设计 Twitter
- 推模型:写时扇出(适合普通用户)
- 拉模型:读时聚合(适合大 V)
- 推拉结合:Twitter / 微博生产方案
流量估算速查表
| 维度 | 单组件能力 |
|---|---|
| Redis DECR | 8-10w QPS / 单实例 |
| MySQL 写入 | 3000-5000 QPS / 单库 |
| Kafka 写入 | 100w+ QPS / 单 broker |
| RocketMQ 写入 | 10w+ QPS / 单 broker |
| HBase 写入 | 5w+ QPS / 单 region server |
| ES 写入 | 1-5w QPS / 单节点 |
典型案例剖析
案例 1:双 11 秒杀(淘宝 / 京东)
目标:100w 用户抢 1000 件商品。
架构:
1. 提前 1 小时预热:商品页静态化推 CDN、库存写入 Redis
2. 0 时刻流量到达:CDN 验证码降速 → 网关限流挡 90% → 应用 Redis DECR 挡 99.9%
3. 1000 条订单进 MQ → 消费者按 200 QPS 写 DB
4. 15 分钟未支付订单延迟队列取消 + 归还 Redis 库存关键指标:
- 用户感知延迟:< 1s 看到结果(pending / 售罄)
- DB 实际 QPS:200(不到极限的 10%)
- 资损率:0(不超卖、不少卖)
案例 2:12306 抢票
特点:用户量大、车票库存按车次 × 区段切分、强一致要求高(不能超卖)。
关键设计:
- 答题验证削峰:100w QPS 摊到 30 秒变 3w QPS
- 余票分车次缓存:Redis 按车次分片,热门车次单独再拆桶
- 下单串行化:同一个车次的同一区段的票,DB 行锁串行扣减
- 支付 30 分钟超时:延迟队列回退库存
案例 3:微信红包
特点:单个红包总额固定、N 个人抢、不能超发。
核心算法:
- 预拆分:发红包瞬间就拆好 N 份金额(不是抢时实时算)
- Redis List PUSH/POP:N 份金额放 Redis List,抢一份 LPOP 一份
- 抢完即结束:List 为空时直接返回"晚了一步",根本不到 DB
详见 微信红包系统。
跟其他章节的关联
| 子主题 | 详细页面 |
|---|---|
| 限流算法手撕 | 令牌桶/漏桶手撕、限流与熔断 |
| 热点 Key 拆分 / 库存分桶 | 幂等性与热点 Key |
| MQ 选型(Kafka / RocketMQ) | 消息队列 |
| 分库分表 / ShardingSphere | 存储选型、MySQL 分库分表 |
| 分布式事务(最终一致) | 分布式事务 |
| 幂等性设计 5 种方案 | 幂等性与热点 Key |
| Feed 流写放大 | 设计 Twitter |
| 微信红包详解 | 微信红包系统 |
面试常问 & 怎么答
"秒杀系统怎么设计"
5 步答题模板(30 秒能说完):
- 挡:CDN 验证码 + 网关多维度限流 → 挡 90%
- 扣:Redis DECR 原子预扣 → 挡 99%(库存不到的直接返回售罄)
- 缓:MQ 削峰,应用层立即返回 pending
- 写:消费者按 DB 节奏慢慢落单(200-500 QPS)
- 兜:延迟队列处理未支付订单 + 对账 Job 修正差异
"Redis 预扣库存为什么不超卖"
DECR 是 Redis 单线程原子操作。100w 并发 DECR 串行执行,前 1000 个返回 ≥ 0、第 1001 个开始返回负数。Redis 的单线程在秒杀里反而是核心优势。
"MQ 挂了怎么办"
3 道防线:
- MQ 集群高可用:Kafka 3 副本、RocketMQ 主从
- 降级到 DB 直写:MQ 不可用时短期直接写 DB(限流到 DB 极限)
- 预订单存 Redis:MQ 没送到时数据不丢,从 Redis 重发
"怎么防止超卖"
3 层防线:
- Redis DECR 兜大头(原子)
- DB 乐观锁 兜底:
UPDATE ... WHERE count >= 1 - 对账 Job 定时修正
"为什么不直接 DB 行锁串行"
单行写 MySQL ~ 1000-3000 QPS,100w QPS 直接打爆。Redis 单实例 8-10w QPS、且能挡掉 99% "明知会失败"的请求——根本不到 DB。
"推 vs 拉 vs 推拉结合,发微博怎么选"
详见 设计 Twitter:
- 普通用户(< 1w follower)→ 推模型,写入 follower inbox
- 大 V(> 1w follower)→ 拉模型,follower 读时实时聚合
- Twitter / 微博生产用推拉结合
看到什么就先想到这类
- "秒杀 / 抢购 / 双 11" → CDN 答题 + 网关限流 + Redis DECR + MQ 削峰
- "下单 / 支付" → 同上,加幂等 + 延迟队列
- "发微博 / 朋友圈" → Feed 流推拉结合
- "点赞 / 关注" → 简单写,写 Redis + 异步同步 DB
- "12306 抢票" → 余票 Redis 分桶 + 答题降速 + 下单串行
- "微信红包" → 预拆分 + Redis List
读流量(百万 QPS 详情页 / 热搜 / Feed 推荐)有不同的架构思路,见 高并发读架构。