系统设计:秒杀系统
秒杀系统是典型的读多写少、高并发、强一致性的场景。核心挑战:在几万 QPS 的抢购流量下,保证不超卖、不宕机。
1. 需求分析
场景
- 商品库存:100 件
- 并发用户:10 万人同时抢购
- 时间窗口:几秒到几十秒
核心问题
- 超卖:库存扣减的原子性问题
- 性能:大量请求冲击数据库
- 公平性:防刷单、防机器人
- 用户体验:快速反馈是否抢到
2. 系统架构
用户 → CDN → API Gateway
↓
┌───┴───┐
↓ ↓
Nginx 限流(令牌桶)
↓
负载均衡
↓
┌───┴───┬───────┐
↓ ↓ ↓
Redis 秒杀服务 消息队列
库存预减 订单创建 (削峰)
↓ ↓ ↓
数据库(异步落库)
3. 关键技术
3.1 Redis 原子扣减库存
使用 Lua 脚本保证原子性:
-- 扣减库存
local stock = redis.call('get', KEYS[1])
if not stock or tonumber(stock) <= 0 then
return 0 -- 库存不足
end
redis.call('decr', KEYS[1])
return 1 -- 扣减成功
import redis
r = redis.Redis()
def deduct_stock(product_id):
lua_script = """
local stock = redis.call('get', KEYS[1])
if not stock or tonumber(stock) <= 0 then
return 0
end
redis.call('decr', KEYS[1])
return 1
"""
result = r.eval(lua_script, 1, f"stock:{product_id}")
return result == 1
为什么用 Lua?
- 避免先读再写的竞态条件
- Redis 单线程执行 Lua,天然原子
3.2 令牌桶限流
import time
from collections import deque
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate # 令牌产生速率(个/秒)
self.capacity = capacity
self.tokens = capacity
self.last_time = time.time()
def allow(self, token=1):
now = time.time()
elapsed = now - self.last_time
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last_time = now
if self.tokens >= token:
self.tokens -= token
return True
return False
Nginx 限流配置:
limit_req_zone $binary_remote_addr zone=seckill:10m rate=10r/s;
server {
location /seckill {
limit_req zone=seckill burst=20 nodelay;
proxy_pass http://backend;
}
}
3.3 消息队列削峰
成功抢到资格后,放入消息队列异步处理:
import kafka
producer = kafka.KafkaProducer(bootstrap_servers='localhost:9092')
def create_order(user_id, product_id):
# 1. Redis 扣减库存
if not deduct_stock(product_id):
return {"success": False, "message": "库存不足"}
# 2. 放入消息队列(秒级返回用户)
order_msg = {
"user_id": user_id,
"product_id": product_id,
"timestamp": time.time()
}
producer.send('order-topic', order_msg)
return {"success": True, "message": "抢购成功,正在处理订单"}
消费者异步处理:
def process_order(order_msg):
# 幂等处理:检查是否已创建
if order_exists(order_msg):
return
# 创建订单
create_order_db(order_msg)
# 扣减数据库库存(最终一致性校验)
update_db_stock(order_msg.product_id)
3.4 分布式锁(防重复提交)
import uuid
def acquire_lock(lock_key, expire=10):
token = str(uuid.uuid4())
acquired = r.set(lock_key, token, nx=True, ex=expire)
if acquired:
return token
return None
def release_lock(lock_key, token):
# 使用 Lua 保证原子性
lua_script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
r.eval(lua_script, 1, lock_key, token)
3.5 URL 动态化 + 验证码
- 秒杀 URL 在活动开始前保密
- 增加验证码 / 答题环节,过滤机器人
4. 数据一致性
最终一致性方案
- Redis 扣减 → 用户看到「抢购成功」
- 消息队列 → 异步创建订单
- 数据库 → 最终落库
- 对账 → 定时任务校验 Redis 与数据库一致性
兜底方案
- 数据库设置库存字段为
UNSIGNED INT,物理防超卖 - 定时对账发现不一致时,人工介入处理
5. 面试常见问题
Q: 为什么不用数据库悲观锁?
悲观锁 SELECT ... FOR UPDATE 性能太差,高并发下大量请求阻塞,数据库压力巨大。
Q: Redis 挂了怎么办?
- 主从 + Sentinel 高可用
- 降级方案:直接读数据库 + 限流
- 预热:提前将库存加载到 Redis
Q: 如何应对刷单?
- 用户限流:每用户每秒限制请求次数
- 设备指纹:检测异常设备
- 验证码:图形验证码、行为验证码
- 黑名单:对异常 IP/账号封禁
Q: 如何实现「已售罄」的快速提示?
- 布隆过滤器:标记已售罄商品
- 本地缓存:Nginx / 应用层缓存库存状态
- 库存为 0 时直接返回,不走到 Redis
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。