面试知识库
困难

分布式事务失败与补偿实战#

一句话答案#

分布式事务失败处理的核心是「幂等+补偿+对账」三件套:每个参与者操作必须幂等(唯一键/状态机)、失败时按反向顺序执行补偿(Saga 模式)、最终靠定时对账任务兜底发现不一致。

核心要点

一、失败时序图——下单场景

正常流程:
  订单服务(创建订单) → 库存服务(扣减库存) → 支付服务(冻结余额)
                                                    ↓ 成功
  订单状态更新为"已支付" ← 全部成功 ←───────────────┘

部分失败场景:
  订单服务(创建订单✅) → 库存服务(扣减库存✅) → 支付服务(冻结余额❌超时)
                                                    ↓ 触发补偿
  库存服务(回补库存) ← 逆序补偿 ←──────────────────┘
  订单服务(取消订单) ←──────────────────────────────┘
plaintext

二、补偿顺序原则

原则说明示例
逆序补偿按执行反向顺序回滚支付失败→先回补库存→再取消订单
先补偿后通知补偿完成后再更新状态先回补库存确认成功,再把订单改为已取消
补偿也要幂等补偿操作可能重试回补库存用订单号去重
补偿不一定是逆操作有些操作没有逆操作已发短信无法撤回→记录补偿失败→人工处理

三、幂等设计三种方案

方案原理适用
唯一键(推荐)数据库唯一索引 + INSERT IGNORE/ON DUPLICATE创建类操作(创建订单/扣减记录)
状态机只允许合法状态转换(待支付→已支付,不允许已支付→待支付)状态流转类操作
Token 令牌请求前获取 token,处理时校验并删除前端防重复提交

幂等表设计:

CREATE TABLE idempotent_record (
    idempotent_key VARCHAR(128) PRIMARY KEY,  -- 业务唯一键(如 order_id + action)
    status TINYINT DEFAULT 0,                  -- 0=处理中 1=成功 2=失败
    result TEXT,                                -- 处理结果
    created_at TIMESTAMP,
    expired_at TIMESTAMP                       -- 过期清理
);

-- 使用:
-- 1. INSERT INTO idempotent_record (idempotent_key, status) VALUES (?, 0)
-- 2. 已存在 → 查 status:成功直接返回,处理中等待,失败重试
-- 3. 不存在 → 执行业务 → 更新 status
sql

四、对账机制

定时对账任务(每5分钟/每小时/每天)

  ├─ 拉取时间窗口内的订单记录
  ├─ 对比各服务状态:
  │    订单状态=已支付 + 库存未扣减 → 补扣库存或退款
  │    订单状态=已取消 + 库存已扣减 → 回补库存
  │    订单状态=已支付 + 支付状态=未成功 → 查支付渠道确认

  ├─ 不一致记录 → 自动修复(可修复的)
  │                → 告警+人工处理(不可修复的)
  └─ 输出对账报告
plaintext

对账的一致性窗口:

  • 窗口大小 = 最大允许不一致时间(通常分钟到小时级)
  • 窗口越小→对账频率越高→系统负担越大
  • 核心链路(支付):5分钟对账
  • 非核心(积分/优惠券):小时级或日终对账

五、各事务方案的失败处理对比

方案失败处理一致性适用
2PC协调者发 rollback,参与者回滚强一致数据库层面
TCCCancel 阶段执行补偿最终一致资金/库存
Saga逆序执行补偿事务最终一致长事务/跨服务
本地消息表定时重发+消费端幂等最终一致异步场景
MQ 事务消息half message + 回查最终一致RocketMQ 场景

TCC 的空回滚与悬挂:

  • 空回滚:Try 未执行(超时),但 Cancel 被调用 → Cancel 要判断 Try 是否执行过
  • 悬挂:Cancel 先到,Try 后到 → Try 要判断 Cancel 是否已执行
  • 解决:事务状态表记录每个阶段的执行状态
面试回答(2分钟版)

分布式事务失败处理我总结为幂等、补偿、对账三件套。幂等保证重试安全,最实用的方案是数据库唯一键加 INSERT IGNORE,每个操作都有唯一的业务键比如订单号加操作类型。补偿遵循逆序原则,比如下单流程是创建订单→扣库存→冻结余额,支付失败时逆序先回补库存再取消订单,补偿操作本身也要幂等因为可能重试。对账是最终兜底,定时任务拉取时间窗口内的记录比对各服务状态,发现不一致自动修复或告警人工处理。实际项目中我倾向于用 Saga 模式,每个服务提供正向操作和补偿操作,编排器按顺序调用,失败时触发逆序补偿。TCC 更适合资金场景因为有 Try 阶段预留资源,但要额外处理空回滚和悬挂问题。不管用哪种方案,对账都是必须有的最后一道防线,核心链路 5 分钟对账一次,非核心日终对账。

追问与易错

追问方向:

  • “补偿操作失败了怎么办?”→ 自动重试(指数退避)+ 重试上限后告警人工介入,补偿操作本身必须幂等防止过度补偿,最终靠定时对账兜底
  • “一致性窗口怎么量化?”→ 一致性窗口 = 最大对账间隔 + 最大处理时间,需要对业务方公开 SLA 承诺(如支付 5 分钟内一致,积分 1 小时内一致)
  • “怎么监控分布式事务健康?”→ 核心指标:事务成功率、补偿触发率、对账差异数、平均一致性恢复时间;配合 Dashboard 实时告警异常波动
  • “Saga 编排和协同的区别?”→ 编排式有中心编排器统一控制流程(可视化好但单点),协同式每个服务监听事件自行决定下一步(解耦好但链路追踪难)

易错点:

  • ❌ “用 2PC 就不需要补偿”——2PC 协调者挂了也会不一致,且性能差
  • ❌ “幂等就是去重”——去重只是幂等的一种实现,状态机也是幂等
  • ❌ “对账是可选的”——在分布式系统中对账是最终一致性的必要保障
  • ❌ 忽略补偿的幂等性——补偿操作也会被重试,不幂等就会过度补偿
  • ✅ 核心思路:每个操作=正向+补偿+幂等,全局=编排+对账+告警