sequenceDiagram
    participant User as 用户
    participant Frontend as 前端/App
    participant Redis as Redis缓存
    participant MQ as 消息队列
    participant OrderService as 订单服务
    participant DB as 数据库

    User->>Frontend: 点击"立即抢购"
    Frontend->>Frontend: 校验(未开始/验证码)
    Frontend->>Redis: 请求扣减库存 (Lua脚本)
    
    alt 库存不足 或 用户已买
        Redis-->>Frontend: 返回失败 (秒杀结束)
        Frontend-->>User: 提示"手慢了"
    else 库存充足
        Redis->>Redis: 预扣库存 (decr)
        Redis-->>Frontend: 返回成功 (排队中)
        Frontend->>MQ: 发送"创建订单"消息
        Frontend-->>User: 提示"正在为您排队..."
        
        loop 轮询结果
            Frontend->>Backend: 查询订单结果
        end
    end

    MQ->>OrderService: 消费消息
    OrderService->>DB: 1. 插入订单 (唯一索引防重)
    OrderService->>DB: 2. 扣减真实库存
    
    alt 数据库事务成功
        OrderService-->>Redis: 标记订单创建成功
    else 异常/重复
        OrderService-->>Redis: 回滚Redis库存
    end

秒杀系统的核心挑战

秒杀系统的特点是时间短、并发高、资源少,核心要解决的问题是:

  1. 高并发流量:防止流量直接击垮数据库
  2. 超卖/少卖:保证库存的严格一致
  3. 防刷/安全:防止机器人等恶意脚本抢单

前端进行削峰

前端是流量的第一大关口,可以把绝大部分无效请求拦截在最外层,让少量有效请求进入后端。

  • 页面静态化(CDN): 静态资源部署到CDN,减少业务服务器的压力
  • 按钮控制:未开始的时候置灰、点击后立即禁用
  • 答题/验证码:点击抢购前弹出验证码或简单数学题
  • 接口动态校验:先请求一个获取Token的接口,拿到随机的path_id,再拼装成/seckill/{path_id}/buy,后台校验该 URL 的合法性。

redis锁库存设计

  • 库存预热:秒杀开始前,把库存加载到redis中,比如 set stock:1001 100
  • 原子扣减
    • 不要使用简单的getset,因为在并发环境下二者操作的时间差会导致超卖
    • 不要单纯依赖 decr+代码判断ret>0,因为要防止减成负数
      • 因为如果用户退款,需要回滚库存,如果扣成负数,则乱套了
    • 必须做成Lua脚本,将 判断库存>0库存扣减 做成原子操作
      1
      2
      3
      4
      5
      6
      7
      8
      
      local key = KEYS[1]
      local stock = tonumber(redis.call('get', key))
      if stock <= 0 then
          return -1
      else
          redis.call('decr', key)
          return 1
      end
      
  • 一人一单:SETNX、布隆过滤器。这一步需要放到上述Lua里面

MQ削峰填谷

redis扣减库存成功后,不是直接去写库。而是把这一条“创建订单”的消息发送到MQ,完成这个步骤后就可以给前端返回“抢购成功”

下游服务按照自己的处理能力慢慢排队去执行下单逻辑

限流

用限流算法,将过量请求直接拦截掉。

todo::限流的策略

因为目的是促成订单交易成功,把库存的东西卖出去,而不是追求大而无用的请求

数据库设计

数据库是最后一道防线,主要用于数据持久化和最终一致性保证。

  • 表和字段设计
    • 订单表内,uid和商品id要创建唯一索引,这样即使上层出错,也会利用数据库的 Duplicate Key Error 保证只有一个订单生成
  • 扣库存SQL: 建议落库的时候再做一次乐观锁校验
    1
    
        UPDATE seckill_goods SET stock_count = stock_count - 1 WHERE goods_id = ? AND stock_count > 0
    

付款超时处理

下单后加入到延时队列,到时间内没付款则关闭订单、回滚库存