Skip to content

电商系统中,用户下单后如何保证订单、库存、支付三个服务的数据一致性?请设计一个分布式事务方案 ​

  • 标签:后端、系统设计、场景题、电商

🏛 业务架构师 ​

订单、库存、支付三服务的数据一致性——这是分布式事务的教科书场景,我的答案立场鲜明:拒绝强一致(XA),用"本地消息表 + 状态机 + 对账兜底"的最终一致方案,并把"不一致时间窗"作为可管理的业务指标。

整体方案:订单状态机编排 + 异步确认

1. 下单主流程(同步,保用户体验)

  • 订单服务创建订单(状态"待支付")+ 库存预占(同步调用,UPDATE stock SET locked=locked+1 WHERE sku=? AND available>0)——预占失败下单失败,这两步用TCC 风格的预留资源保证原子性
  • 用户侧 30 分钟支付窗口,超时释放预占(延迟消息)

2. 支付回调(异步,驱动状态机)

  • 支付成功回调 → 订单状态机推进"已支付" → 扣减库存(预占转实扣)→ 触发履约
  • 每步用本地消息表:状态变更与"下一步动作消息"同事务落库,异步执行,失败重试
  • 全链路幂等(支付流水唯一索引、状态机条件更新),重复消息无副作用

3. 逆向链路(一致性闭环)

  • 取消/超时/退款:按状态机逆向补偿(释放库存、原路退款),补偿顺序固定(先资金后库存),每步幂等可重试

4. 对账兜底(资损防线,最终一致性的"最终"保证)

  • 实时对账:支付流水 vs 订单状态,分钟级发现"付了钱订单没推进",自动冲正(补单或退款)
  • 日终对账:库存预占余额 vs 订单在途量的全量核对

关键业务观点(差异点)

  • 时间窗是设计指标不是缺陷:三系统不一致窗口控制分钟级,用户无感;追求秒级强一致的代价(XA/锁)会杀死吞吐
  • 库存策略是业务决策:预占 vs 付款减——取决于"超卖"和"少卖"哪个更伤(秒杀付款减,普通预售预占)
  • 状态机 + 幂等 + 对账是三件套,缺一个都会有资损事故

🏗 系统架构师 ​

订单、库存、支付三个服务的一致性是分布式事务的经典场景。我先说结论:不用 2PC/XA,用"本地事务 + 事务消息/事件驱动 + 最终一致 + 对账兜底"的组合,这是电商主流架构。

1. 为什么不用强一致方案

  • XA/Seata-AT:同步阻塞、协调者单点、吞吐低(百级 TPS),大促场景直接出局
  • 跨服务的业务本质允许短暂不一致(订单"处理中"状态对用户可解释),要的是最终一致与状态机正确,不是实时一致

2. 我的方案:订单状态机 + 事务消息驱动的 Saga

text
下单链路:
1) 订单服务本地事务:创建订单(status=待支付) + 冻结库存预占消息(本地消息表)
2) 事务消息投递 → 库存服务消费:预扣库存(带过期时间)
3) 用户支付 → 支付服务本地事务:记录支付流水(status=成功)
4) 支付成功事件(MQ) → 订单服务:订单状态机 待支付→已支付
                      → 库存服务:预扣转实扣
超时未支付:延迟消息 → 订单关闭 + 库存释放预占

3. 关键设计点:

  1. 每个服务只有本地事务,跨服务靠消息(RocketMQ 事务消息保证"本地事务与消息发送"原子:half 消息 → 本地事务 → commit/rollback + 回查)
  2. 状态机 + 幂等:消费方按状态机校验(已支付订单收到关闭消息 → 忽略),条件更新 WHERE status=待支付 保证并发正确;消息带唯一业务号去重
  3. 逆向补偿自动编排:任何一环失败走补偿——支付成功但订单服务处理失败,死信重试 + 告警;补偿也有失败就进人工对账队列
  4. 库存不超卖:预扣用 Redis 原子扣减或 DB 乐观锁 UPDATE stock SET n=n-1 WHERE sku=? AND n>=?,失败即回滚订单
  5. 对账兜底:T+1 三方对账(订单流水 vs 支付流水 vs 库存流水),单边账自动修复任务 + 人工兜底

4. 边界说明:如果业务是资金划转类(不允许任何中间态可见),那用 TCC(Try 冻结/Confirm/Cancel);订单库存支付场景,最终一致 + 秒级收敛 + 对账兜底就是架构最优解。

💻 全栈开发专家 ​

电商三件套一致性,我的结论:不用强一致分布式事务(2PC/Seata AT),用"本地消息表 + 最终一致",性能和可用性都最好。

为什么不用 Seata

订单-库存-支付跨三个库,Seata AT 全局锁在高并发下吞吐崩得很快;TCC 开发成本高。电商场景天然容忍秒级不一致(用户无感),最终一致是最优解。

方案:本地消息表(事务消息)

java
@Transactional  // 订单库本地事务:订单 + 消息 原子落库
public void createOrder(Order o) {
    orderMapper.insert(o);                       // 1. 订单落库(待支付)
    stockClient.prepareDeduct(o);                // 或发"预扣库存"消息
    localMsgMapper.insert(new LocalMsg("STOCK_DEDUCT", payload)); // 2. 同事务写消息表
}
// 事务提交后,后台任务扫消息表 → 发 MQ → 成功标记 sent,失败重试

链路设计

  1. 下单:订单(待支付)+ 预扣库存消息同事务落库 → MQ → 库存服务消费扣减(幂等:订单号 唯一约束防重复扣)。
  2. 支付回调:支付服务收到第三方成功回调 → 更新支付单 → 发"支付成功"消息 → 订单服务推进状态机(待支付→已支付)。
  3. 超时取消:延迟消息(30 分钟)检查订单,未支付则订单关闭 + 库存回补消息。
  4. 失败补偿:任何环节最终失败,逆向补偿(回补库存、退款),消费端全部幂等,重试不产生副作用。

兜底:对账

每条消息表记录有状态(发送/消费/完成),XXL-JOB 定时扫超时未完成记录重推;T+1 对账(订单 vs 支付单 vs 库存流水)闭环修数。

面试加分:讲清"预扣库存"解决超卖、状态机防乱序、幂等靠唯一键而不是分布式锁——这三个细节比方案名字重要。