系统设计——如何设计一个每秒 10 万订单的秒杀系统 / Designing a High-Throughput Flash-Sale System
📅 创建时间:2026-05-08 🏷️ 标签:#系统设计 #秒杀 #容量规划 #一致性哈希 #高并发 📚 前置知识:[[00-backend-overview]](全部章节内容) 📚 相关知识:[[01-redis-deep]](Redis 缓存) [[02-mq-kafka]](削峰) [[04-distributed-system]](分布式锁)
场景:面试官说,设计一个秒杀系统
┌─────────────────────────────────────────────────────────────┐
│ │
│ 面试官:如何设计一个双十一秒杀系统? │
│ │
│ 你(错误示范): │
│ "用 Redis 做缓存,Kafka 做削峰,MySQL 做持久化..." │
│ 面试官:好的,下一题。 │
│ │
│ 你(正确示范): │
│ "先确认需求——QPS多少?库存多少?需不需要地域限制?" │
│ 面试官:...说说你的思路。 │
│ │
└─────────────────────────────────────────────────────────────┘系统设计不是背答案,而是展示你思考问题的方式。
第1节:需求分析——先问清楚问题
需求澄清 checklist
┌─────────────────────────────────────────────────────────────┐
│ 系统设计第一步:澄清需求 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 功能需求: │
│ • 秒杀商品展示(提前多久?静态化还是动态?) │
│ • 下单流程(限单?一人一单还是一人多单?) │
│ • 支付流程(超时多久?库存锁定多久?) │
│ • 库存回滚(未支付自动取消) │
│ │
│ 非功能需求: │
│ • QPS:峰值多少?持续多久? │
│ • 库存:总量多少?SKU 多少? │
│ • 可用性:99.9% 还是 99.99%? │
│ • 数据一致性:允许超卖多少?0? │
│ │
│ 约束条件: │
│ • 预算(影响架构复杂度) │
│ • 团队技术栈(会用什么语言/框架) │
│ • 时间(多久要上线) │
│ │
└─────────────────────────────────────────────────────────────┘典型秒杀系统的量级
┌─────────────────────────────────────────────────────────────┐
│ 秒杀系统典型量级 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 估算基础(以双十一为参考): │
│ • 日活用户:5 亿 │
│ • 参与秒杀用户:1 亿 │
│ • 秒杀商品:100 个 SKU,每个 1000 件 │
│ • 峰值 QPS:10 万 - 100 万 │
│ • 持续时间:2 小时 │
│ │
│ 拆解: │
│ • 10 万 QPS / 100 个商品 = 每商品 1000 QPS │
│ • 1000 QPS / 10 秒 = 每秒 100 人抢 1 件商品 │
│ │
│ 结论: │
│ 99% 的请求会在 Redis 层面被拦截 │
│ 只有 1% 的请求需要打到数据库 │
│ │
└─────────────────────────────────────────────────────────────┘第2节:架构设计——分层防护
从流量入口到数据层
┌─────────────────────────────────────────────────────────────┐
│ 秒杀系统分层架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户请求 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CDN(静态资源 + 商品页面缓存) │ │
│ │ → 秒杀开始前 1 分钟,商品页面全部缓存到 CDN │ │
│ │ → 用户浏览器直接加载,不经过服务器 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ API 网关层(限流 + 鉴权 + 路由) │ │
│ │ → 令牌桶限流(总 QPS 限制) │ │
│ │ → 黑名单过滤(黑产 IP/账号) │ │
│ │ → 路由到秒杀服务集群 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 秒杀服务层(Redis + Lua) │ │
│ │ → 活动开始前预热:库存加载到 Redis │ │
│ │ → Lua 脚本:原子性扣库存 + 判断是否还有库存 │ │
│ │ → 库存为 0?直接返回"已售罄"(不查数据库) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 消息队列层(Kafka) │ │
│ │ → 秒杀成功后,下单请求进入 Kafka │ │
│ │ → 异步处理:创建订单、扣减数据库库存、发送通知 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 订单服务层(MySQL) │ │
│ │ → 消费者处理 Kafka 消息 │ │
│ │ → 乐观锁扣减数据库库存 │ │
│ │ → 创建订单记录 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘核心代码:Redis Lua 原子扣库存
python
# 秒杀的核心:Redis Lua 脚本,保证原子性
SECKILL_SCRIPT = """
local stock_key = KEYS[1]
local user_key = KEYS[2]
local limit = tonumber(ARGV[1])
local user_id = ARGV[2]
local expire = tonumber(ARGV[3])
-- 1. 检查用户是否已经购买(一人一单)
local bought = redis.call('SISMEMBER', user_key, user_id)
if bought == 1 then
return -2 -- 已购买
end
-- 2. 检查库存
local stock = tonumber(redis.call('GET', stock_key))
if not stock then
return -3 -- 活动不存在
end
if stock <= 0 then
return -1 -- 已售罄
end
-- 3. 扣库存 + 标记用户已购买(原子操作)
redis.call('DECR', stock_key)
redis.call('SADD', user_key, user_id)
redis.call('EXPIRE', user_key, expire)
return stock - 1 -- 返回剩余库存
"""
def seckill(product_id, user_id, activity_id):
stock_key = f"seckill:stock:{activity_id}:{product_id}"
user_key = f"seckill:user:{activity_id}:{product_id}"
result = redis.eval(
SECKILL_SCRIPT,
2, stock_key, user_key,
1, user_id, 86400 # 每人限购 1 件,24 小时内有效
)
if result == -2:
return {"code": "ALREADY_BOUGHT", "msg": "您已购买过此商品"}
elif result == -1:
return {"code": "SOLD_OUT", "msg": "已售罄"}
elif result == -3:
return {"code": "ACTIVITY_NOT_FOUND", "msg": "活动不存在"}
else:
# 秒杀成功,发送消息到 Kafka 创建订单
kafka.send("order-topic", {
"user_id": user_id,
"product_id": product_id,
"activity_id": activity_id,
"remaining_stock": result
})
return {"code": "SUCCESS", "remaining_stock": result}第3节:容量规划——你的系统能撑多少
估算方法:自顶向下
┌─────────────────────────────────────────────────────────────┐
│ 容量规划计算步骤 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1:估算总 QPS │
│ 日活 1 亿,假设 10% 用户参与秒杀 │
│ 1 亿 × 10% = 1000 万 UV │
│ 秒杀持续 2 小时,假设 80% 流量集中在前 10 分钟 │
│ → 1000 万 × 80% / (10 × 60) = 1.3 万 QPS │
│ │
│ Step 2:拆分到各层 │
│ CDN 层:拦截静态请求,QPS 可忽略 │
│ API 网关:1.3 万 QPS │
│ Redis 秒杀服务:只有 5% 用户能抢到 → 650 QPS │
│ MySQL 订单服务:异步消息队列 → 峰值 100 QPS │
│ │
│ Step 3:计算存储 │
│ 库存:100 SKU × 1000 件 = 10 万条订单 │
│ 订单表:10 万条 × 1KB = 100 MB │
│ 用户购买记录:10 万条 × 100B = 10 MB │
│ Redis 缓存:10 万 × 200B = 20 MB │
│ │
│ Step 4:计算带宽 │
│ 平均响应 200B,1.3 万 QPS │
│ → 1.3 万 × 200B × 8 = 20 Mbps │
│ → 留 3 倍余量:60 Mbps │
│ │
└─────────────────────────────────────────────────────────────┘性能预估表
┌─────────────────────────────────────────────────────────────┐
│ 各层性能预估 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 层级 │ 单机能力 │ 需要机器数 │ 说明 │
│ ├───────────────┼─────────────┼───────────┼─────────────┤
│ │ CDN │ 10 万 QPS │ 1-2 台 │ 静态资源 │
│ ├───────────────┼─────────────┼───────────┼─────────────┤
│ │ API 网关 │ 1 万 QPS │ 3-5 台 │ 限流鉴权 │
│ ├───────────────┼─────────────┼───────────┼─────────────┤
│ │ Redis │ 10 万 QPS │ 3-6 台 │ 哨兵/集群 │
│ ├───────────────┼─────────────┼───────────┼─────────────┤
│ │ Kafka │ 50 万 QPS │ 3 台 │ 削峰缓冲 │
│ ├───────────────┼─────────────┼───────────┼─────────────┤
│ │ 订单服务 │ 1000 QPS │ 5-10 台 │ 异步处理 │
│ ├───────────────┼─────────────┼───────────┼─────────────┤
│ │ MySQL │ 5000 QPS │ 2 台 │ 主从备份 │
│ │
└─────────────────────────────────────────────────────────────┘第4节:常见系统设计题
设计一:设计一个短链系统
┌─────────────────────────────────────────────────────────────┐
│ 设计短链系统 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 需求:把长 URL 转成短 URL │
│ 例:https://example.com/product/123456 → https://t.cn/abc │
│ │
│ 核心问题: │
│ 1. 短码如何生成?(不能重复,不能被猜测) │
│ 2. 如何存储?(KV 存储,短码→长 URL) │
│ 3. 如何跳转?(302 临时跳转还是 301 永久跳转) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 方案 A:分布式 ID 转 Base62 │ │
│ │ 雪花 ID = 1234567890123456 │ │
│ │ Base62 = 2LKx2M → https://t.cn/2LKx2M │ │
│ │ │ │
│ │ 方案 B:哈希 + 冲突检测 │ │
│ │ hash = MD5(long_url)[:6] │ │
│ │ 冲突?→ 加盐再哈希 │ │
│ │ │ │
│ │ 方案 C:发号器(推荐) │ │
│ │ 原子计数器:1 → 2 → 3 → ... │ │
│ │ 转 Base62 = "abc" │ │
│ │ 碰撞率:0(严格递增,不会重复) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘设计二:设计一个 Feed 流系统
┌─────────────────────────────────────────────────────────────┐
│ 设计 Feed 流系统 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 需求:微博/小红书/抖音的关注页 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Push 模式(写扩散): │ │
│ │ 发微博 → 写入所有粉丝的 Feed 表 │ │
│ │ 优点:读取快 │ │
│ │ 缺点:大 V 发微博要写几亿条 │ │
│ │ 适用:小红书(粉丝少,互动高) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Pull 模式(读扩散): │ │
│ │ 查看 Feed → 查询所有关注用户的最新 N 条微博 │ │
│ │ 优点:发微博快 │ │
│ │ 缺点:读取慢,需要合并排序 │ │
│ │ 适用:Twitter(粉丝多,内容多) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 混合模式(推荐): │ │
│ │ 普通用户:Push │ │
│ │ 大 V 用户:Pull(用户读时拉取大 V 的最新微博) │ │
│ │ 热门微博:Push 到热点池 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘设计三:设计一个延迟任务系统
┌─────────────────────────────────────────────────────────────┐
│ 设计延迟任务系统 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 需求:订单 30 分钟未支付,自动取消 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 方案 1:定时轮询(简单,但浪费资源) │ │
│ │ 每分钟扫描:WHERE status='pending' AND create_at < NOW()-30min│
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 方案 2:Redis ZSet(精准延时) │ │
│ │ ZADD order:cancel task_id score=过期时间戳 │ │
│ │ ZRANGEBYSCORE order:cancel 0 过期时间戳 │ │
│ │ 定时扫描,精度高,但需要轮询 Redis │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 方案 3:RabbitMQ 延迟队列(推荐) │ │
│ │ 发送消息时设置 x-delay = 30分钟 │ │
│ │ 30 分钟后消息投递到消费者 │ │
│ │ MQ 保证不丢消息 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 方案 4:时间轮算法(高性能延时调度) │ │
│ │ 分针刻度 = 60 格,每格 = 1 分钟 │ │
│ │ 每个格子 = 任务链表 │ │
│ │ 时钟每走一格,处理对应格子的任务 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第5节:一致性哈希——数据分片的艺术
问题:如何把数据均匀分布到 N 台机器
┌─────────────────────────────────────────────────────────────┐
│ 普通哈希的问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ hash(key) % N = 机器编号 │
│ │
│ 机器从 3 台扩到 4 台: │
│ → hash(key) % 4 ≠ hash(key) % 3 │
│ → 几乎所有数据都需要迁移! │
│ │
│ N=3 时:1, 2, 3 → 全部重新映射 → 迁移量 = 100% │
│ │
└─────────────────────────────────────────────────────────────┘一致性哈希
┌─────────────────────────────────────────────────────────────┐
│ 一致性哈希原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 哈希空间:[0, 2^32),组成一个环 │
│ │
│ 0 │
│ \ │
│ \ │
│ \ │
│ ●──────────────●───────────● │
│ 节点 A 节点 B 节点 C │
│ hash(A) hash(B) hash(C) │
│ │
│ 数据定位: │
│ hash(key) → 顺时针找到第一个节点 │
│ │
│ 扩缩容: │
│ 新增节点 D:只需迁移 D 相邻区间的数据 │
│ 其他节点的数据不受影响 │
│ │
│ 虚拟节点: │
│ 每台物理节点 = 100 个虚拟节点 │
│ → 使数据分布更均匀 │
│ │
└─────────────────────────────────────────────────────────────┘python
import hashlib
class ConsistentHash:
def __init__(self, replicas=100):
self.replicas = replicas
self.ring = {}
self.sorted_keys = []
def _hash(self, key):
return int(hashlib.md5(str(key).encode()).hexdigest(), 16)
def add_node(self, node):
for i in range(self.replicas):
key = self._hash(f"{node}:{i}")
self.ring[key] = node
self.sorted_keys.append(key)
self.sorted_keys.sort()
def get_node(self, key):
if not self.ring:
return None
hash_key = self._hash(key)
for node_key in self.sorted_keys:
if hash_key <= node_key:
return self.ring[node_key]
return self.ring[self.sorted_keys[0]] # 环绕到第一个
# 使用
ch = ConsistentHash()
ch.add_node("192.168.1.1")
ch.add_node("192.168.1.2")
ch.add_node("192.168.1.3")
# 数据定位
node = ch.get_node("user:12345") # 永远路由到同一台机器升华:系统设计的思维框架
┌─────────────────────────────────────────────────────────────┐
│ 系统设计四步法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1:澄清需求(2-3 分钟) │
│ → 功能需求 / 非功能需求 / 约束条件 │
│ → 不要急着画图,先问清楚问题 │
│ │
│ Step 2:高层设计(5-8 分钟) │
│ → 数据流 / 服务划分 / 关键组件 │
│ → 先给宏观架构图 │
│ │
│ Step 3:核心问题深入(10-15 分钟) │
│ → 数据存储 / 可用性 / 一致性 / 扩展性 │
│ → 选择 + 权衡(Trade-off) │
│ │
│ Step 4:总结优化(2-3 分钟) │
│ → 监控 / 告警 / 容灾 / 成本 │
│ → 展示工程思维 │
│ │
│ 一句话: │
│ 系统设计考的是权衡能力,不是背答案。 │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ 一致性哈希的具体代码实现
✅ 雪花算法的具体代码
✅ 容量规划的计算公式
✅ 各开源组件的选型参数
必须理解:
🔴 系统设计的四步法(澄清需求→高层设计→深入→总结)
🔴 秒杀系统的分层防护架构
🔴 Redis Lua 脚本保证原子性的原理
🔴 Push / Pull / 混合 Feed 流的选择
🔴 一致性哈希 vs 普通哈希的 trade-off学习状态:🟡 开始学习