返回架构笔记
Arch Day 111

Arch Day 111: 系统设计面试(2) — 设计秒杀系统

Arch Day 111: 系统设计面试(2) — 设计秒杀系统

2026-03-29
第四阶段 - 高阶融合
系统设计秒杀高并发库存一致性限流缓存

日期: 2026-03-29 (Day 111) 阶段: 第四阶段 - 高阶融合 标签: #系统设计 #秒杀 #高并发 #库存一致性 #限流 #缓存


面试模拟 — 45分钟

面试场景

面试官: "请设计一个电商秒杀系统。用户在特定时间(如晚上8点)抢购限量商品(如1000件iPhone)。预计100万用户同时参与。"


第一步:需求澄清(5分钟)

我的提问

功能需求确认:
Q1: 秒杀商品是独立的活动还是嵌入在主商城中?
A: 独立活动页面,可从主商城跳转。

Q2: 秒杀持续多长时间?
A: 通常商品卖完即止,最长30分钟。

Q3: 一个用户最多买几件?
A: 每人每商品限购1件。

Q4: 支付有时间限制吗?
A: 抢到后15分钟内完成支付,超时释放库存。

Q5: 需要排队机制吗?
A: 需要,抢购成功后进入支付队列。

非功能需求确认:
Q6: 并发量?
A: 100万用户同时刷新/点击,峰值QPS预计50万。

Q7: 延迟要求?
A: 秒杀结果返回<1秒。

Q8: 商品库存?
A: 1000件(典型),最少10件,最多10000件。

Q9: 公平性要求?
A: 先到先得,不能出现"黄牛"和机器人大面积抢购。

需求总结

核心挑战:
├── 极高并发: 50万QPS → 普通系统的500倍
├── 极小库存: 1000件 → 99.9%的请求注定失败
├── 极短时间: 秒级决战 → 所有请求集中在同一秒
├── 强一致性: 不能超卖(库存不能为负)
└── 公平性: 真人优先,防止机器人

核心思路:
"挡"(尽可能拦截无效请求) → "削"(削峰填谷) → "快"(核心路径极致优化)

第二步:高层设计(10分钟)

2.1 全链路架构

秒杀系统全链路架构:

100万用户
    │
    ▼
┌──────────────────────────────────────────────────────────┐
│  Layer 1: CDN + 前端                                      │
│  ├── 静态页面CDN缓存(商品图/描述)                         │
│  ├── 前端倒计时(本地时钟+服务器校准)                       │
│  ├── 按钮置灰防抖(点击后5秒冷却)                          │
│  └── 前端随机丢弃(只允许10%的请求发出)                    │
│  → 拦截率: 90% (10万请求到达后端)                        │
└────────────────────┬─────────────────────────────────────┘
                     │ 10万QPS
                     ▼
┌──────────────────────────────────────────────────────────┐
│  Layer 2: 接入层(API Gateway + 限流)                      │
│  ├── 用户身份验证(JWT快速校验)                            │
│  ├── 全局限流(令牌桶: 5万QPS)                            │
│  ├── 用户级限流(单用户1次/5秒)                           │
│  ├── IP级限流(单IP 10次/分钟)                            │
│  ├── 验证码/人机验证(防机器人)                            │
│  └── 黑名单过滤(已知机器人/黄牛IP)                       │
│  → 拦截率: 50% (5万请求进入服务层)                       │
└────────────────────┬─────────────────────────────────────┘
                     │ 5万QPS
                     ▼
┌──────────────────────────────────────────────────────────┐
│  Layer 3: 秒杀服务(独立部署,与主商城物理隔离)             │
│  ├── 活动校验(是否开始/是否结束)                          │
│  ├── 资格校验(是否已购/是否有资格)                        │
│  ├── 库存预扣(Redis原子操作)                              │
│  │   └── DECR seckill:stock:{item_id}                    │
│  │       → >0: 抢购成功,发送到MQ                        │
│  │       → ≤0: 库存不足,直接返回失败                    │
│  └── 本地内存标记(库存=0后本地缓存,不再请求Redis)        │
│  → 通过率: 2% (1000个请求进入队列)                       │
└────────────────────┬─────────────────────────────────────┘
                     │ 1000 请求
                     ▼
┌──────────────────────────────────────────────────────────┐
│  Layer 4: 消息队列(Kafka/RocketMQ)                       │
│  ├── 削峰填谷: 1000个请求排队处理                        │
│  ├── 异步下单: 消费者逐个创建订单                        │
│  └── 保证顺序: 同一用户消息在同一分区                    │
└────────────────────┬─────────────────────────────────────┘
                     │
                     ▼
┌──────────────────────────────────────────────────────────┐
│  Layer 5: 订单服务(正常业务流程)                          │
│  ├── 创建订单(写MySQL)                                   │
│  ├── 真实扣减库存(MySQL事务)                             │
│  ├── 设置支付超时(15分钟)                                │
│  └── 通知用户(WebSocket/Push)                            │
└──────────────────────────────────────────────────────────┘

2.2 核心数据流

时间线视角:

T-10min: 活动预热
├── CDN预加载静态资源
├── Redis预热库存数据
├── 服务预热(JIT编译/连接池预建)
└── 前端展示倒计时

T=0: 秒杀开始
├── 100万用户同时点击
├── CDN层拦截90% → 10万请求到Gateway
├── Gateway限流50% → 5万请求到服务层
├── Redis库存预扣 → 1000个成功
├── MQ排队 → 异步创建订单
└── ~500ms内返回结果(成功/失败)

T+1s ~ T+3s:
├── 库存归零 → 本地标记 → 后续请求直接返回"已售罄"
├── 秒杀服务QPS骤降到几百(查询状态为主)
└── 订单服务正常处理1000笔订单

T+15min:
├── 未支付订单超时 → 释放库存
├── 释放的库存可能触发第二轮抢购(可选)
└── 活动数据统计

第三步:核心组件设计(15分钟)

3.1 库存管理(Redis原子操作) ★ 核心

Redis库存预扣方案:

预加载:
  SET seckill:stock:{item_id} 1000
  SET seckill:status:{item_id} "ACTIVE"

扣减(Lua脚本保证原子性):
-- seckill_deduct.lua
local stock_key = KEYS[1]
local user_key = KEYS[2]      -- seckill:user:{item_id}:{user_id}
local user_id = ARGV[1]

-- 检查是否已购买
if redis.call('SISMEMBER', user_key .. ':bought', user_id) == 1 then
    return -2  -- 已购买
end

-- 检查库存
local stock = tonumber(redis.call('GET', stock_key))
if stock <= 0 then
    return -1  -- 库存不足
end

-- 扣减库存 + 记录用户
redis.call('DECR', stock_key)
redis.call('SADD', user_key .. ':bought', user_id)
return stock - 1  -- 返回剩余库存

结果处理:
├── 返回 ≥ 0: 抢购成功 → 发送MQ消息
├── 返回 -1: 库存不足 → 返回"已售罄"
└── 返回 -2: 已购买 → 返回"您已购买"

关键优化:
├── Lua脚本: 保证"检查+扣减+记录"三个操作的原子性
├── 热点Key分桶: stock分成10个子Key → 分散到不同Redis节点
│   seckill:stock:{item_id}:0 = 100
│   seckill:stock:{item_id}:1 = 100
│   ...
│   seckill:stock:{item_id}:9 = 100
│   → 用户ID % 10 决定访问哪个子Key
│   → 单Redis节点压力降低10倍
├── 本地内存标记: 库存=0后设置JVM本地标志
│   → 后续请求不再访问Redis
│   → 万级QPS降到0
└── 库存释放: 支付超时后INCR归还库存

3.2 防超卖保证

防超卖三道防线:

第一道: Redis原子扣减
├── Lua脚本保证原子性
├── DECR操作天然防并发
└── 但Redis和MySQL可能不一致(Redis宕机)

第二道: MySQL乐观锁
├── UPDATE inventory SET stock = stock - 1
│   WHERE item_id = ? AND stock > 0
├── 影响行数=0 → 库存不足(补偿:返回MQ重试/退款)
└── 这是最终一致性保证

第三道: 库存校验任务
├── 定时任务: 每分钟比对Redis库存和MySQL库存
├── Redis > MySQL → 以MySQL为准(修正Redis)
├── Redis < MySQL → 告警(可能超卖)
└── 最终通过订单数据 + 库存数据对账确认

超卖处理(万一发生):
├── 立即告警(P0)
├── 联系多出的订单用户
│   ├── 方案A: 补货
│   └── 方案B: 退款+优惠券补偿
└── 复盘根因(Redis/MySQL不一致?并发Bug?)

3.3 数据模型

-- 秒杀活动表
CREATE TABLE seckill_activity (
    id              BIGINT PRIMARY KEY,
    item_id         BIGINT NOT NULL,
    item_name       VARCHAR(200),
    seckill_price   BIGINT NOT NULL,           -- 秒杀价(分)
    original_price  BIGINT NOT NULL,           -- 原价
    total_stock     INT NOT NULL,              -- 总库存
    available_stock INT NOT NULL,              -- 可用库存
    start_time      TIMESTAMP NOT NULL,
    end_time        TIMESTAMP NOT NULL,
    status          VARCHAR(20) DEFAULT 'PENDING',  -- PENDING/ACTIVE/FINISHED
    limit_per_user  INT DEFAULT 1,             -- 每人限购
    created_at      TIMESTAMP DEFAULT NOW()
);

-- 秒杀订单表(独立于主订单系统)
CREATE TABLE seckill_order (
    id              BIGINT PRIMARY KEY,
    order_no        VARCHAR(64) UNIQUE NOT NULL,
    activity_id     BIGINT NOT NULL,
    user_id         BIGINT NOT NULL,
    item_id         BIGINT NOT NULL,
    seckill_price   BIGINT NOT NULL,
    status          VARCHAR(20) NOT NULL,      -- CREATED/PAID/TIMEOUT/CANCELLED
    pay_deadline    TIMESTAMP NOT NULL,        -- 支付截止时间
    paid_time       TIMESTAMP,
    created_at      TIMESTAMP DEFAULT NOW(),
    UNIQUE KEY uk_activity_user (activity_id, user_id)  -- 防止同一用户重复下单
);

-- 库存变更流水(审计用)
CREATE TABLE stock_change_log (
    id              BIGINT PRIMARY KEY,
    activity_id     BIGINT NOT NULL,
    change_type     VARCHAR(20) NOT NULL,      -- DEDUCT/RELEASE/ADJUST
    change_amount   INT NOT NULL,
    stock_after     INT NOT NULL,
    order_no        VARCHAR(64),
    reason          VARCHAR(200),
    created_at      TIMESTAMP DEFAULT NOW()
);

3.4 防机器人/公平性设计

防机器人多层策略:

Layer 1: 前端防御
├── 滑块验证码(活动开始前弹出)
├── 按钮点击防抖(5秒冷却)
├── 前端加密参数(动态Token)
│   └── Token = HMAC(user_id + timestamp + 随机盐)
│       每次请求必须携带,服务端验证
└── 设备指纹采集

Layer 2: 接入层防御
├── IP频率限制(单IP 10次/分钟)
├── 用户频率限制(单用户 1次/5秒)
├── Token验证(必须通过前端验证码获取)
├── User-Agent检查(过滤明显的Bot)
└── 黑名单(已知机器人IP/设备)

Layer 3: 业务层防御
├── 实名认证要求(绑定手机号+实名)
├── 账号注册时间限制(注册超过7天才能参与)
├── 历史行为评分(正常购物用户优先)
└── 同一设备/手机号限制

公平性保证:
├── 先到先得(Redis DECR的顺序性)
├── 抢到不代表购买成功(需要15分钟内支付)
├── 超时未支付释放库存 → 给其他用户机会
└── 可选: 抽签模式(5分钟报名→随机抽取1000人)

第四步:深入讨论(10分钟)

4.1 容量规划与性能计算

容量规划:

前提假设:
├── 100万用户同时参与
├── 每人平均刷新/点击5次
├── 峰值集中在10秒内
└── 目标: 总请求500万次, 峰值QPS 50万

各层容量计算:

CDN层:
├── 静态资源QPS: 50万(全由CDN承担)
├── CDN节点: 全球20+节点
├── 带宽: ~10Gbps(主要是图片)
└── CDN不处理动态请求

Gateway层:
├── 动态请求QPS(限流后): 5万
├── 单Nginx处理能力: ~5万QPS
├── 需要: 2-4台Nginx(冗余)
├── 带宽: ~1Gbps
└── 限流策略: 令牌桶 5万token/s

秒杀服务:
├── QPS(限流后): 5万
├── 单实例处理能力: ~5000 QPS(主要是Redis操作)
├── 需要: 12-16个实例(含冗余)
├── CPU: 每实例4核
├── 内存: 每实例4GB
└── 主要瓶颈: Redis网络RTT

Redis:
├── 操作QPS: 5万(Lua脚本)
├── 单Redis处理能力: ~10万 ops/s
├── 热点分桶后: 5000 ops/s/桶 × 10桶
├── 需要: Redis Cluster 6节点(3主3从)
├── 内存: 单节点1GB足够(数据量很小)
└── 瓶颈: 热点Key → 用分桶解决

Kafka:
├── 写入QPS: ~1000(只有抢购成功的)
├── 单分区: ~10000 msg/s
├── 需要: 3分区(冗余)
└── 这里不是瓶颈

MySQL:
├── 写入QPS: ~100(异步消费MQ)
├── 单MySQL: 轻松应对
├── 主从: 1主2从
└── 这里完全不是瓶颈(MQ已经削峰)

4.2 故障处理

各种故障场景处理:

故障1: Redis宕机
├── 影响: 无法扣减库存
├── 预案: Redis Sentinel自动主从切换(秒级)
├── 切换期间: 返回"系统繁忙"
└── 数据恢复: 从MySQL重新加载库存到Redis

故障2: 秒杀服务宕机
├── 影响: 部分请求失败
├── 预案: K8s自动重启(秒级) + 多实例冗余
├── 用户体验: 短暂不可用→刷新重试
└── 库存安全: Redis库存未受影响

故障3: Kafka宕机
├── 影响: 抢购成功但订单创建延迟
├── 预案: Kafka多副本(3副本,2个确认)
├── 兜底: 本地消息表 → Kafka恢复后重新发送
└── 用户体验: 告知"排队中,稍后通知"

故障4: MySQL宕机
├── 影响: 订单无法写入
├── 预案: MQ消息保留 → MySQL恢复后重新消费
├── 数据安全: MQ消息持久化
└── 用户体验: 延迟通知(分钟级)

故障5: 恶意攻击(DDoS)
├── 检测: 异常流量监控(突然10倍于预期)
├── 预案: CDN层WAF → IP封禁 → 接入清洗服务
├── 降级: 直接返回静态"活动结束"页面
└── 通知: 自动告警 → 运维介入

4.3 监控与告警

秒杀系统关键监控指标:

实时监控:
├── 在线用户数(WebSocket连接数)
├── QPS(各层: CDN/Gateway/Service/Redis)
├── 库存剩余(Redis实时查询)
├── 下单成功率
├── 支付成功率
├── 各层延迟(P50/P95/P99)
└── 错误率(各层)

告警阈值:
├── P99延迟>500ms → 告警
├── 错误率>1% → 告警
├── Redis CPU>70% → 告警
├── 库存<10% → 通知(准备结束活动)
├── 超卖检测(Redis库存<0) → P0告警
└── 支付超时率>20% → 告警

仪表板:
├── 活动实时大屏(投屏到作战室)
│   ├── 实时QPS
│   ├── 实时库存
│   ├── 订单数/支付数
│   └── 错误率
└── 事后分析报告(活动结束后自动生成)
    ├── 总参与人数
    ├── 抢购成功人数
    ├── 支付完成人数
    ├── 各层拦截率
    └── 性能数据汇总

第五步:扩展性(5分钟)

5.1 从1000件到100万件

库存量级扩展:

1000件(小型秒杀):
├── 单Redis实例足够
├── 10秒内结束
└── 基础架构即可

10000件(中型秒杀):
├── Redis热点分桶(10桶)
├── 20-30秒内结束
└── Kafka分区增加到10

100000件(大型秒杀):
├── Redis Cluster(多节点)
├── 热点分桶(100桶)
├── 多个秒杀服务集群
└── MQ消费者并行(50个)

1000000件(超大型活动/双11):
├── 分时段放量(每小时放10万件)
├── 多品类分散流量
├── 全链路压测(提前1个月)
├── 多Region部署
└── 预案: 10+种降级策略

5.2 与主系统的隔离

隔离设计(核心原则:秒杀不能影响主商城):

┌─────────────────────────────────────────────┐
│                主商城系统                     │
│  ┌────────┐ ┌────────┐ ┌────────┐          │
│  │商品服务│ │订单服务│ │支付服务│          │
│  │(主库)  │ │(主库)  │ │(主库)  │          │
│  └────────┘ └────────┘ └────────┘          │
└─────────────────────────────────────────────┘
         (完全隔离,不共享任何资源)
┌─────────────────────────────────────────────┐
│                秒杀系统                      │
│  ┌────────┐ ┌────────┐ ┌────────┐          │
│  │秒杀服务│ │秒杀订单│ │秒杀库存│          │
│  │(独立)  │ │(独立DB)│ │(Redis) │          │
│  └────────┘ └────────┘ └────────┘          │
│                                              │
│  隔离点:                                     │
│  ├── 独立域名: seckill.example.com          │
│  ├── 独立服务集群: 不与主商城共用容器        │
│  ├── 独立数据库: 不与主商城共用DB           │
│  ├── 独立缓存: 不与主商城共用Redis          │
│  ├── 独立MQ: 不与主商城共用Kafka Topic      │
│  └── 独立监控: 独立告警渠道                  │
│                                              │
│  唯一交互点:                                  │
│  └── 秒杀订单支付成功 → 异步MQ → 主订单系统  │
│      (异步消息,不影响主系统的实时性能)        │
└─────────────────────────────────────────────┘

面试官追问及应对

追问1: "如果Redis热点Key导致单节点瓶颈怎么办?"

解决方案: 热点Key分桶

将 seckill:stock:{item_id} 拆分为:
  seckill:stock:{item_id}:0 = 100
  seckill:stock:{item_id}:1 = 100
  ...
  seckill:stock:{item_id}:9 = 100

路由策略: user_id % 10 → 决定访问哪个桶

好处:
├── 10个桶分散到不同Redis Slot → 不同节点处理
├── 单节点QPS从5万降到5000
└── 可以根据需要增减桶数

挑战:
├── 某个桶库存扣完但总库存还有 → 需要"借调"机制
│   桶0库存=0 → 从桶5"借"50个 → 桶0=50, 桶5=50
├── 借调操作用Lua脚本保证原子性
└── 库存总量校验: 所有桶的SUM应该=总可用库存

追问2: "如何保证用户体验公平?"

公平性方案:

方案A: 纯先到先得(默认)
├── 网络快的用户有优势
├── 但这是最简单且用户最能理解的
└── 配合人机验证防止机器人

方案B: 排队抽签
├── 5分钟报名窗口(降低网络优势)
├── 报名截止后随机抽取N人
├── 中签者15分钟内支付
├── 更公平但延迟体验差(要等开奖)
└── 适合超高价值商品(如茅台)

方案C: 分批放量
├── 不是一次性放出全部库存
├── 每隔5分钟放出20%库存
├── 第一轮没抢到的还有机会
├── 降低瞬间并发压力
└── 用户体验更好(不是"一秒决定胜负")

推荐:
├── 常规秒杀 → 方案A(先到先得 + 人机验证)
├── 高价值商品 → 方案B(抽签)
└── 大型活动(双11) → 方案C(分批放量)

追问3: "秒杀结束后,这些基础设施怎么复用?"

基础设施复用:

秒杀是"脉冲式"负载 → 大部分时间空闲 → 必须复用

方案1: K8s弹性伸缩
├── 平时: 秒杀服务缩到最小(2个Pod)
├── 活动前30分钟: 自动扩容到50个Pod
├── 活动后30分钟: 自动缩容
└── CronHPA(定时弹性伸缩)

方案2: Serverless
├── 秒杀核心逻辑用云函数实现
├── 按请求量计费(不活动时0成本)
├── 挑战: 冷启动延迟
└── 解决: 预热(活动前10分钟预启动)

方案3: 资源池共享
├── 秒杀集群和压力测试共享基础设施
├── 非秒杀时间: 用于全链路压测
├── 秒杀时间: 切换为秒杀服务
└── 通过K8s命名空间隔离

完整答案总结

秒杀系统设计核心:

1. "挡": 尽可能在前面拦截
   ├── CDN拦截90%静态请求
   ├── Gateway限流拦截50%
   └── 99.9%的请求在到达DB之前已被拦截

2. "削": 削峰填谷
   ├── MQ异步化下单
   ├── 只有抢购成功的1000笔进入MQ
   └── MySQL看到的是稳定的低流量

3. "快": 核心路径极致优化
   ├── Redis Lua脚本: 原子扣减+限购检查
   ├── 本地内存标记: 售罄后0 Redis访问
   └── 热点分桶: 10x降低单节点压力

4. "稳": 不能影响主系统
   ├── 物理隔离: 独立域名/服务/DB/缓存
   ├── 降级兜底: 10+种降级策略
   └── 容灾: 任何单点故障不会超卖

5. "防": 公平性保证
   ├── 人机验证: 滑块验证码
   ├── 频率限制: IP级+用户级
   └── 黑名单: 已知机器人

参考资源