分布式事务补偿机制设计方法详解(详解补偿的注意事项与失败兜底方案)

实现补偿机制,核心是保证每个正向步骤都有可逆向、幂等、可重试的补偿操作,并用持久化日志与状态机记录进度。补偿失败时不能放任,要用指数退避重试、死信队列隔离、告警人工介入,再借定时对账兜底修复。下文拆解设计要点与失败处理。

一、补偿不是”反向调一遍”那么简单

Saga 把一个长事务拆成多个本地事务,某步失败就逆向执行已成功步骤的补偿。但补偿本身是一次真实的数据写操作,同样会超时、会重复、会失败。如果补偿没设计好,系统会停在”部分已撤销、部分没撤销”的中间态,比不补偿更危险。补偿失败最贵的代价不是重试次数,而是停在中间态的那段时间——资金可能已经扣了,库存却没释放,占用额度卡住后续交易。

二、设计补偿必须盯住的五个问题

关注点 正向操作 补偿操作的要求
幂等 可被重试 同参数重复调用结果一致
可重试 失败即中止 失败后能安全重跑
顺序 正向依次执行 严格反向撤销
隔离 提交后即生效 用语义锁避免脏读
持久化 内存状态 落库日志,崩溃可恢复

2.1 幂等与可重试

补偿会因为它自己失败而被反复触发,也会因消息重发被调用多次。每个补偿步骤用”全局事务 ID + 业务 ID”做去重键,执行前先查是否已补偿过,已做则直接返回成功。

2.2 补偿顺序必须反向

正向是创建订单 → 扣库存 → 扣款,补偿就该退款 → 恢复库存 → 取消订单。顺序错了会破坏数据,例如先取消订单再去退一笔根本没完成的支付。

2.3 隔离要靠语义锁

Saga 没有 ACID 的隔离性,补偿执行期间别的服务可能读到中间态数据。常用四招:语义锁(把记录标成 pending)、交换更新(设计可任意顺序执行的更新)、悲观视图(读未提交时带告警)、重读校验(提交前确认数据没被改过)。

2.4 补偿也要能补偿

理想情况下补偿是”简单可逆”的,比如退款、恢复库存。但像”已发货”这种物理动作难以回滚,应在设计阶段就改成可补偿的形态,或把不可逆步骤排到流程末尾。

2.5 进度必须落库

用一张事务日志表记录每一步的状态与游标,进程崩溃重启后从断点续跑,而不是从头再来一遍。

三、一个带重试与死信的补偿器

def compensate(saga):
    saga.state = "COMPENSATING"
    save(saga)
    for step in reversed(done_steps(saga)):
        for attempt in range(1, 6):        # 指数退避重试
            try:
                if not step.compensated:
                    step.compensate
                    step.compensated = True
                    save(saga)
                break
            except Exception as e:
                if attempt == 5:
                    send_to_dead_letter(saga, step, e)  # 转入死信,等人工
                    saga.state = "FAILED"
                    alert(f"补偿失败 saga={saga.id} step={step.name}")
                sleep(2 ** attempt)

四、补偿失败了怎么办

补偿失败分两类,处理方式不同:

  1. 瞬时故障(网络抖动、锁等待):用指数退避自动重试,多数能自己恢复;
  2. 永久故障(数据已不一致、下游宕机):停止重试,把该事务丢进死信队列,触发告警让人工介入,同时冻结相关资源避免扩大损失。

定时对账是兜底防线:扫描长时间停在 COMPENSATING / FAILED 的事务,比对各服务真实状态,自动补跑补偿或人工修复。对账不依赖补偿链路的运气,是真正的兜底。死信队列里的记录要带足上下文(saga_id、失败步骤、异常堆栈),运维接单后能直接定位,而不是重新翻全量日志。

五、补偿与正向的隔离冲突

Saga 没有 ACID 隔离,补偿执行期间别的服务可能在读中间态数据。除语义锁外,还要把订单状态机锁在 COMPENSATING,禁止对该订单发起新的正向操作,避免正向写入与补偿并发打架,制造更难排查的脏数据。

六、用对账 SQL 兜底

定时任务扫出卡在中间态的事务,交给对账处理器补跑或转人工。

-- 找出卡在中间态超过 10 分钟的事务
SELECT saga_id, state, step_index, updated_at
FROM saga_order_tx
WHERE state NOT IN ('DONE', 'FAILED')
  AND updated_at < NOW - INTERVAL 10 MINUTE;

七、生产检查清单

  1. 每个正向步骤都有对应补偿,且补偿幂等、可重试;
  2. 补偿顺序严格反向,参数能从持久化日志恢复;
  3. 补偿失败进死信并告警,对账任务每日跑。

八、隔离四招再展开

语义锁把记录标成 pending,别的服务读到就跳过或等待;交换更新设计成可任意顺序执行的操作,顺序错了也能收敛;悲观视图读未提交数据时带告警;重读校验在提交前确认数据没被别人改过。四招按业务代价选,不必全用。

常见问题(FAQ)

Q1:补偿和正向都要幂等吗?

都要。补偿会被重试和重发,靠事务 ID 去重防重复。

Q2:补偿一直失败会怎样?

转死信队列并告警,人工介入,对账任务兜底修复。

Q3:不可逆操作怎么补偿?

尽量改成可补偿形态,或放到流程末尾,失败时人工处理。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
拓扑大师的头像拓扑大师普通用户

相关推荐

返回顶部