用户下单后,订单、库存、支付分属三个独立服务与数据库,单靠本地事务无法跨库回滚。落地首选 Saga 编排式(长流程、最终一致)或 TCC(高并发预扣、业务层强一致),并辅以一张全局事务状态表、幂等补偿与定时对账兜底。下文给出可直接复用的方案与片段。
一、为什么本地事务救不了三服务
订单服务写自己的库、库存服务扣自己的库、支付服务扣用户的库,三者之间没有任何共享的数据库连接。任何一个环节在远端提交之后失败,前面已提交的动作不会自动撤销,于是出现”订单生成了、库存没扣、钱却付了”的脏数据。两阶段提交(2PC)虽能做到跨库强一致,却要求所有参与者同时加锁等待协调者,可用性差、吞吐低,不适合电商高并发下单链路。
二、三种主流方案怎么选
| 方案 | 一致性强度 | 性能开销 | 适用环节 |
|---|---|---|---|
| 2PC / XA | 强一致 | 高(锁等待) | 低并发内部系统 |
| TCC | 业务层强一致 | 中 | 预扣库存、资金交易 |
| Saga | 最终一致 | 低 | 长流程履约、下单链路 |
| 可靠消息 | 最终一致 | 低 | 异步解耦、非核心同步 |
秒杀、高并发下单优先 TCC,手动锁定资源避免数据库锁阻塞;普通下单到支付的常规链路优先 Saga,每个步骤提交本地事务后立即释放,失败再逆向补偿;商品搜索索引等非核心同步用可靠消息异步补齐。
三、Saga 编排式落地
3.1 先建一张全局事务状态表
协调器先把整个事务登记进一张表,每推进一个步骤就更新状态与游标。进程崩溃重启后,能从表里读出断点续跑,这是 Saga 可靠性的底座。
CREATE TABLE saga_order_tx (
saga_id VARCHAR(64) PRIMARY KEY,
order_id VARCHAR(64),
state VARCHAR(32), -- INIT/ORDER_DONE/STOCK_DONE/PAID/DONE/FAILED
step_index INT,
payload JSON, -- 各步骤补偿所需的参数
created_at DATETIME,
updated_at DATETIME
);
3.2 正向流程与补偿流程
- 协调器插入 saga 记录,状态置 INIT,调用订单服务创建”待支付”订单;
- 订单成功后状态改 ORDER_DONE,调用库存服务扣减库存;
- 库存成功后状态改 STOCK_DONE,调用支付服务扣款;
- 支付成功状态改 DONE,事务完成;任一步抛错则进入补偿分支。
补偿按反向顺序执行:支付失败先恢复库存、再取消订单;库存失败直接取消订单。每一步补偿必须幂等,重复调用不产生二次影响。
def execute_order_saga(saga_id, user_id, product_id, qty, amount):
tx = load(saga_id)
try:
if tx.step_index <= 0:
order_id = order_svc.create(user_id, product_id, qty, amount)
tx.order_id, tx.step_index = order_id, 1
if tx.step_index <= 1:
stock_svc.deduct(product_id, qty)
tx.step_index = 2
if tx.step_index <= 2:
pay_svc.charge(user_id, amount)
tx.state = "DONE"
except Exception:
compensate(tx) # 反向撤销已成功的步骤
save(tx)
3.3 TCC 备选:预扣而非直扣
高并发场景把”直接扣减”换成”冻结预占”。Try 阶段校验资格、冻结库存、预占资金,不生成正式订单;Confirm 阶段正式固化;Cancel 阶段释放冻结。全程不持数据库长锁,能扛住大促瞬时峰值。
四、两个必须堵住的坑
幂等是底线。重试、补偿、网络重发都会让同一动作触发多次,必须用”全局事务 ID + 状态机”双重控制,避免重复扣减或重复退款。对账是兜底。定时任务扫描 saga 表里长时间停在中间状态的记录,比对订单、库存、支付三方真实状态,自动补跑或告警人工介入。
五、用可靠消息异步补齐非核心环节
下单主链路只保订单、库存、支付三件事。像同步商品搜索索引、发短信通知、记行为日志这类非核心动作,别塞进 Saga 主流程,改用本地消息表(发件箱)异步补齐:业务写库与消息写在同一个本地事务,独立线程扫描未发送消息投递到 MQ,消费端幂等处理。这样主链路只做最关键的三步,耗时与失败面都更小。
真实案例里,订单服务调用成功、库存服务却超时最常见:订单已落库,库存没扣,用户付款后等两天收不到货。这正是跨服务操作必须要么全成、要么全撤的典型场景,单靠重试订单服务救不回来。
六、把状态与补偿接入监控
Saga 跨多个服务运行,最怕”看不见”。给每个 saga 分配全局 traceId,串联订单、库存、支付三方的日志;把 saga 状态、补偿触发次数、对账修复结果上报监控面板。没有可观测,补偿悄悄失败你也不会察觉,往往等资损出现才暴露。
七、落地三步清单
- 梳理链路,标出哪些步骤必须强一致(如资金)、哪些可最终一致;
- 选 TCC 或 Saga,建全局事务状态表,逐步骤写幂等补偿;
- 接监控与定时对账,上线先小流量灰度验证补偿逻辑。
常见问题(FAQ)
Q1:TCC 和 Saga 怎么选?
高并发预扣库存、资金交易选 TCC;长流程下单履约选 Saga。
Q2:补偿操作要注意什么?
必须幂等且可重试,按反向顺序执行,状态机记录推进进度。
Q3:消息丢了怎么办?
用本地消息表或发件箱定时重投;消费端幂等,失败进死信队列。