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级+用户级
└── 黑名单: 已知机器人