高层架构图

  • 主要角色:
    • seckill-api:预扣库存,写入outbox;
    • Redis:库存视图 + Stream outbox;
    • relay-service:从Stream推到RabbitMQ;
    • order-service:幂等消费,写MySQL;
    • reconcile Job / DLQ 工具:事后审计、补偿
  • 整体顺序图(主成功路径):P1-S1 READY → P2-R0 LUA_OK → P3-N2 SENT_OK → P4-C0 DB_OK → P5-RC0 MATCH
    sequenceDiagram
      participant C as Client
      participant G as Gateway
      participant A as seckill-api
      participant R as Redis
    Lua+Stream participant L as relay-service participant MQ as RabbitMQ participant O as order-service participant DB as MySQL C->>G: HTTP /seckill G->>A: forward + JWT (P1) A->>R: GET totalKey (P1-S1 READY) A->>R: EVALSHA seckill.lua(...) (P2-R0 LUA_OK) R-->>A: 0 (pre-deduct + XADD outbox) loop P3 relay L->>R: XREADGROUP (P3-N0 -> N1) L->>MQ: publish message MQ-->>L: publisher confirm ack=true (P3-N2 SENT_OK) L->>R: XACK + XDEL end MQ->>O: deliver message O->>DB: UPDATE stock-1 WHERE stock>0 O->>DB: INSERT order(request_id) (P4-C0 DB_OK) O-->>MQ: ack A-->>C: "秒杀成功,订单处理中"

阶段 + 状态建模

Phase 1:入口&缓存准备(P1)

  • 主体:seckill-api -> Redis(Cache+Redisson 锁)
  • 状态:
    • P1-S1 READY:缓存已就绪,total > 0;
    • P1-S2 SOLD_OUT:总库存 sentinel ≤ 0;
    • P1-S3 NOT_FOUND:L3 回源无商品;
    • P1-S4 LOADING:没抢到锁,别人正在回源。

phase 2:Lua扣库存 + outbox(P2)

  • 主体 Redis Lua
  • 状态:
    • P2-R0 LUA_OK:预扣成功 + SADD user + XADD outbox
    • P2-R1 DUPLICATE:重复购买;
    • P2-R2 SOLD_OUT:总库存真实为 0;
    • P2-R3 BUCKET_EMPTY:当前桶耗尽,代码内会重试;
    • P2-R4 BUCKET_ALL_EMPTY:多桶尝试后依然失败,最终返回“桶空”。
      sequenceDiagram
      participant C as Client
      participant A as seckill-api
      participant R as Redis(Lua)
      
      C->>A: submitSeckillOrder()
      
      A->>R: GET totalKey
      alt P1-S1 READY
          A->>R: EVALSHA seckill.lua(...)
          alt P2-R0 LUA_OK
              R-->>A: 0 (预扣成功 + XADD)
          else P2-R1 DUPLICATE
              R-->>A: 1 (重复购买)
          else P2-R2 SOLD_OUT
              R-->>A: 2 (总库存耗尽)
          else P2-R3 BUCKET_EMPTY
              R-->>A: 3 (换桶重试, 最终可能 LUA_OK 或 BUCKET_ALL_EMPTY)
          end
      else P1-S2 SOLD_OUT
          A-->>C: "已售罄"
      else P1-S3 NOT_FOUND
          A-->>C: "商品不存在"
      else P1-S4 LOADING
          A-->>C: "系统繁忙,请稍后再试"
      end

phase 3:relay & MQ(P3)

  • 主体:Redis Stream outbox → relay-service → RabbitMQ
  • 针对一条outbox entry的状态:
    • P3-N0 NEW:刚 XADD,还没人读;
    • P3-N1 PENDING:被 XREADGROUP 读出,挂到 pending;
    • P3-N2 SENT_OK:发送 MQ + confirm ack=true → XACK+XDEL;
    • P3-N3 RETRYING:确认失败/超时,attempts < max,保留在 pending;
    • P3-N4 RELAY_DLQ:重试多次仍失败 → 写 DLQ,XACK+XDEL 原 entry。
      sequenceDiagram
      participant R as Redis Stream(outbox)
      participant L as relay-service
      participant MQ as RabbitMQ
      
      L->>R: XREADGROUP (NEW -> PENDING)
      
      alt P3-N2 SENT_OK
          L->>MQ: publish
          MQ-->>L: confirm ack=true
          L->>R: XACK + XDEL
      else P3-N3 RETRYING
          L->>MQ: publish
          MQ-->>L: confirm ack=false / timeout
          L->>R: HINCR attempts
          note right of L: attempts < maxAttempts,保留在 pending 等重试
      else P3-N4 RELAY_DLQ
          L->>MQ: publish
          MQ-->>L: confirm timeout 多次
          L->>R: move to relay DLQ
      XACK + XDEL 原 entry end note over R,L: XPENDING + XCLAIM 定时捞长时间 idle 的 PENDING
      避免 entry 永久卡住

phase 4:consumer & DB(P4)

  • 主体:RabbitMQ -> order-service -> MySQL
  • 状态:
    • P4-C0 DB_OK:扣减成功 + 插入订单;
    • P4-C1 IDEMPOTENT:已存在 requestId 或 (userId, productId),幂等返回;
    • P4-C2 BIZ_ERROR_ACK:业务异常(库存不足、非法状态)被捕获并 ack,不再重试;
    • P4-C3 SYS_ERROR_RETRY:DB down/超时等系统异常 → 抛 RuntimeException,让 MQ 重试;
    • P4-C4 MQ_DLQ:重试次数超限,消息被 MQ 放入 DLQ。
      sequenceDiagram
      participant MQ as RabbitMQ
      participant O as order-service
      participant DB as MySQL
      
      MQ->>O: deliver message
      
      alt P4-C0 DB_OK
          O->>DB: UPDATE stock-1 WHERE stock>0
          O->>DB: INSERT order(request_id)
          O-->>MQ: ack
      else P4-C1 IDEMPOTENT
          O->>DB: SELECT/INSERT 冲突 -> 已存在 request_id
          O-->>MQ: ack (不再重试)
      else P4-C2 BIZ_ERROR_ACK
          O->>DB: 业务校验失败
          O-->>MQ: ack (视为业务失败,不重试)
      else P4-C3 SYS_ERROR_RETRY
          O->>DB: DB down / timeout
          O-->>MQ: nack / 不ack
          note right of MQ: 多次重试后 -> P4-C4 MQ_DLQ
      end

phase 5:对账 & 补偿(P5)

  • 主体:Reconcile Job / DLQ
  • 对某个requestId的审计结果:
    • P5-RC0 MATCH:Redis/outbox/DLQ 与 DB 一致;
    • P5-RC1 LEAK:Redis/Stream/MQ 有记录,DB 无订单 → 漏单候选;
    • P5-RC2 OVER_SELL:DB 订单数 > initial_stock;
  • 对某个补偿动作的结果:
    • P5-COMP0 COMPENSATED:补单成功;
    • P5-COMP1 FAILED_FINAL:补偿失败(例如真没库存),记录为永久失败。
      sequenceDiagram
      participant J as ReconcileJob
      participant R as Redis(outbox/DLQ)
      participant MQ as MQ DLQ
      participant DB as MySQL
      
      J->>R: scan outbox / relay DLQ (old entries)
      J->>DB: SELECT order by requestId
      alt P5-RC0 MATCH
          J-->>J: 标记为已处理
      else P5-RC1 LEAK
          J-->>J: 生成补单任务
      end
      
      J->>MQ: scan MQ DLQ
      J->>DB: SELECT order by requestId
      alt 可补偿
          J->>DB: 尝试幂等补单 (P5-COMP0)
      else 不可补偿
          J-->>J: 标记 P5-COMP1 FAILED_FINAL
      end

测试方案

验收目标

不超卖
  • 任意时刻DB中订单数 <= initial_stock;
  • DB中stock >= 0
不重复单
  • 对某个requestId最多只有一条订单
不默默消失
  • 对所有在P2-R0(Lua_OK)状态的请求
    • 最终在DB要有订单
    • 在relay DLQ/MQ DLQ/failed_request中被记录为失败。
  • 不允许存在Redis预扣,但是DB没订单,DLQ也没记录的情况

测试设计

总览
  1. Group A:正常 + 入口逻辑(P1+P2,无故障)
    • 单用户成功、重复请求被拦截、并发下不超卖
  2. Group B:relay故障路径(P3)
    • MQ短暂不可用,relay重试可成功
    • MQ长时间不可用,relay重试次数attempts达到上限,该payload进入relay DLQ
    • relay崩溃在PENDING状态,有消息在等待ack和attempts达到上限的中间状态,此时relay崩溃,靠XPENDING+XCLAIM恢复(有多个relay服务的情况,该relay挂掉,可以推给别的relay服务)
  3. Group C:consumer & DB 故障路径(P4)
    • 重复投递 + 幂等
    • 业务异常不应被无限重试
    • 系统异常,MQ重试,达到次数进入MQ DLQ
    • DB崩掉后,Resilience4j发挥作用,熔断打开,不影响消息的投递行为
  4. Group D:reconcile / DLQ / 补偿(P5)
    • 漏单检测、超卖检测
    • 从DLQ中回放,补单
    • 永久失败的记录与策略
  5. Group E:压测 + 统计验证
    • JMeter高并发压测下的“不超卖+不重复”
    • 压测后,Redis和DB的最终一致性检测
Group A 无故障 + 入口逻辑
Group B relay 故障路径
Group C consumer & DB
Group D reconcile / DLQ /对账
Group E 压测

本站由 Xylumina 使用 Stellar 1.30.0 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本"页面"访问 次 | 总访问 次 | 总访客