系统设计:秒杀系统

从零设计一个支持百万并发的秒杀系统,详解库存扣减策略、Redis原子操作、消息队列削峰、限流算法与防超卖机制,包含架构图与核心代码实现。

系统设计:秒杀系统

秒杀系统是典型的读多写少、高并发、强一致性的场景。核心挑战:在几万 QPS 的抢购流量下,保证不超卖、不宕机。

1. 需求分析

场景

  • 商品库存:100 件
  • 并发用户:10 万人同时抢购
  • 时间窗口:几秒到几十秒

核心问题

  1. 超卖:库存扣减的原子性问题
  2. 性能:大量请求冲击数据库
  3. 公平性:防刷单、防机器人
  4. 用户体验:快速反馈是否抢到

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. 数据一致性

最终一致性方案

  1. Redis 扣减 → 用户看到「抢购成功」
  2. 消息队列 → 异步创建订单
  3. 数据库 → 最终落库
  4. 对账 → 定时任务校验 Redis 与数据库一致性

兜底方案

  • 数据库设置库存字段为 UNSIGNED INT,物理防超卖
  • 定时对账发现不一致时,人工介入处理

5. 面试常见问题

Q: 为什么不用数据库悲观锁?
悲观锁 SELECT ... FOR UPDATE 性能太差,高并发下大量请求阻塞,数据库压力巨大。

Q: Redis 挂了怎么办?

  • 主从 + Sentinel 高可用
  • 降级方案:直接读数据库 + 限流
  • 预热:提前将库存加载到 Redis

Q: 如何应对刷单?

  • 用户限流:每用户每秒限制请求次数
  • 设备指纹:检测异常设备
  • 验证码:图形验证码、行为验证码
  • 黑名单:对异常 IP/账号封禁

Q: 如何实现「已售罄」的快速提示?

  • 布隆过滤器:标记已售罄商品
  • 本地缓存:Nginx / 应用层缓存库存状态
  • 库存为 0 时直接返回,不走到 Redis

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页