Skip to content

高并发写架构 — 秒杀 / 抢单 / 发帖怎么扛

系统设计 ⭐⭐⭐ 进阶 🔥🔥🔥 必考

💡 核心要点

高并发写的核心心智模型是「用前面的层挡掉无效请求 + 用 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 次/秒防点击狂魔
单 IP10 次/秒防机房代理刷
单接口总 QPS 限到 10w兜底保护
黑名单历史黄牛 / 异常 IP风控前置

面试加分点:黑名单不光是限流,风控系统会做"账号特征评分"——新注册账号、设备指纹异常、有刷单历史 → 直接拒绝或限流到 0.1 次/秒。

③ 应用层 — Redis 预扣库存(最关键

核心思路:库存数据先放 Redis,每次下单 DECR stock:item_999Redis 返回 < 0 直接拒绝,根本不到 DB。

java
// 伪代码
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 选型

场景推荐
秒杀 / 单 regionRocketMQ(事务消息成熟,金融场景多)
日志 / 全球高吞吐Kafka
简单业务Redis Stream

必须考虑的 3 个细节

  1. 消息不丢:MQ 持久化 + 消费 ACK + 失败重试
  2. 幂等消费:网络重试可能消费两次,用 Idempotency-Key 去重(详见 幂等性
  3. 死信队列:连续失败的消息进 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 次同一行)。

解法:库存分桶

sql
-- 原表
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 落单前的窗口,并发请求都看到"还有库存"。

对策

  1. Redis DECR + INCR 兜底(已在上面代码体现)
  2. DB 乐观锁兜底UPDATE stock SET count = count - 1 WHERE item_id = ? AND count >= 1
  3. 库存预热:秒杀前提前把库存从 DB 同步到 Redis

陷阱 2:少卖

原因:用户支付超时取消、消息消费失败但没回滚库存。

对策

  1. 延迟队列:订单 15 分钟未支付 → 自动取消 + 归还库存(RocketMQ 延迟消息 / Redis ZSet)
  2. 对账 Job:定时扫描"Redis 预扣 > 实际订单数"差异,回滚

陷阱 3:黄牛刷单

对策

  1. 风控前置:账号年龄 / 设备指纹 / 行为评分
  2. 验证码 / 答题:CDN 层降速
  3. 限购规则:单账号 / 单 IP / 单设备每场限购数量
  4. 延迟揭晓:预约抽签(小米早期)—— 把"先到先得"改成"先约后抽",本质是把瞬时洪峰摊平到几小时

陷阱 4:DB 写放大(社交场景)

微博发帖:你发 1 条 → 推给 1000 follower 的 timeline → DB 要写 1000 次

对策:详见 设计 Twitter

  • 推模型:写时扇出(适合普通用户)
  • 拉模型:读时聚合(适合大 V)
  • 推拉结合:Twitter / 微博生产方案

流量估算速查表

维度单组件能力
Redis DECR8-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 秒能说完):

  1. :CDN 验证码 + 网关多维度限流 → 挡 90%
  2. :Redis DECR 原子预扣 → 挡 99%(库存不到的直接返回售罄)
  3. :MQ 削峰,应用层立即返回 pending
  4. :消费者按 DB 节奏慢慢落单(200-500 QPS)
  5. :延迟队列处理未支付订单 + 对账 Job 修正差异

"Redis 预扣库存为什么不超卖"

DECR 是 Redis 单线程原子操作。100w 并发 DECR 串行执行,前 1000 个返回 ≥ 0、第 1001 个开始返回负数。Redis 的单线程在秒杀里反而是核心优势

"MQ 挂了怎么办"

3 道防线:

  1. MQ 集群高可用:Kafka 3 副本、RocketMQ 主从
  2. 降级到 DB 直写:MQ 不可用时短期直接写 DB(限流到 DB 极限)
  3. 预订单存 Redis:MQ 没送到时数据不丢,从 Redis 重发

"怎么防止超卖"

3 层防线:

  1. Redis DECR 兜大头(原子)
  2. DB 乐观锁 兜底:UPDATE ... WHERE count >= 1
  3. 对账 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 推荐)有不同的架构思路,见 高并发读架构