Skip to content

当前有个本地操作 A,远程操作 B,我需要保证 A 和 B 事务的一致性,你会如何实现? ​

  • 标签:后端、场景题

🏛 业务架构师 ​

本地操作 A(如本地事务写库)和远程操作 B(调下游服务/写另一个系统)的一致性,是分布式事务最常见形态。我的答案是本地消息表 + 最终一致为主,按业务强度分档补充。

第一档:本地消息表(80% 场景的正解)

本地事务: 写业务表 + 写消息表(同一DB, 原子)
异步:     定时任务/MQ 把消息表记录投递给远程B
远程B:    幂等处理, 返回成功 -> 消息表标记完成
兜底:     超时未完成的记录重试 + 告警人工介入
  • 核心:用本地事务的原子性保证"业务成功则消息必达",用重试 + 幂等保证 B 最终执行
  • A 成功 B 失败:重试兜底;B 成功但响应丢失:B 的幂等性让重试无害
  • 我最推荐它,因为每一步都可观测(消息表就是看板)、可人工补偿,比黑盒事务框架可控

第二档:TCC(资金级强要求) Try 预留资源 → Confirm 确认 / Cancel 释放,每个参与方实现三个接口。侵入大、开发成本高,只在资金冻结/扣减这种"中间态必须是业务正确"的场景用。

第三档:Saga(长流程编排) 正向链路 + 逆向补偿链,适合跨多系统的长事务(开户流程:创建账户→发卡→通知),接受"中间态可见",靠补偿回滚。

架构师的关键观点

  1. 先问业务能容忍多久不一致:秒级(通知类)→ 消息表异步就行;必须同生共死(余额扣减)→ TCC;大多数团队高估了自己对强一致的需要
  2. 幂等是一切方案的地基:没有幂等,任何重试机制都会制造重复数据
  3. 必须配对账:本地消息表方案我一定会加 T+1 对账(A 表 vs B 系统数据核对),把"漏网之鱼"从用户投诉变成日终工单——资损兜底靠对账,不靠框架
  4. 拒绝 XA:两阶段提交在互联网高并发场景性能和可用性都不达标,我原则上不用

🏗 系统架构师 ​

本地操作 A(如本地 DB 事务)与远程操作 B(如调第三方/远程服务)的一致性,本质是无法用一个事务包住网络调用的问题。我按可靠性递进给方案。

方案一:最大努力通知 + 对账(弱一致场景) A 成功后执行 B,B 失败就重试 + 告警。适合 B 是通知类操作(发消息),失败可人工补。必须配对账兜底。

方案二:本地消息表(我最推荐的工程方案) 核心是把"要做 B"这个意图与 A 放进同一个本地事务:

text
BEGIN;
  执行 A(扣款/写订单)
  INSERT INTO local_message(status=PENDING, payload=B参数)
COMMIT;

后台任务扫 PENDING 消息调 B,成功标 DONE;失败退避重试,超限进死信人工处理。一致性保证:A 与"待做 B"原子落库,B 最终一定被尝试到。这是国产电商最普遍的实现,简单、可审计、可回溯。

方案三:事务消息(RocketMQ half 消息) A 提交前后走 MQ 的事务消息协议:发 half 消息 → 执行本地事务 A → commit/rollback 消息 → MQ 把消息投给 B 的消费方。相当于 MQ 替你实现了本地消息表的扫描逻辑,且多了回查机制(MQ 反查 A 是否成功)。

方案四:TCC/Saga(强业务补偿) B 必须可逆时:Try 预留资源、Confirm/Cancel 二阶段;或 Saga 编排正逆操作。成本高,留给资金级链路。

关键共性:B 必须幂等(重试会重复送达);失败要有终止条件与人肉出口;全链路 traceId 串联便于追账。我的一句话:本地事务保 A 与意图原子,异步重试保 B 最终执行,幂等与对账兜住一切意外。

💻 全栈开发专家 ​

本地事务 + 远程调用的组合,我按一致性要求选方案。

方案一:本地消息表(我的默认选择)

核心思想:把"执行远程操作"这个意图和本地事务绑在一起存下来,异步投递 + 重试直到成功。

sql
-- 与业务表同库
CREATE TABLE outbox_message (
  id BIGINT PRIMARY KEY,
  biz_no VARCHAR(64), msg_type VARCHAR(32), payload JSON,
  status TINYINT DEFAULT 0,          -- 0待发送 1成功 3死亡
  retry_count INT DEFAULT 0,
  next_retry_time DATETIME,
  KEY idx_status_time(status, next_retry_time)
);
java
@Transactional
public void doA() {
    localMapper.updateA(...);                 // 本地操作 A
    outboxMapper.insert(newMessage(bizNo));   // 意图同事务落库
}
// 后台任务扫描 status=0 且到期的消息,调用远程 B
// B 成功 -> 标记1;失败 -> 指数退避重试,超限进死信人工

远程操作 B 必须幂等(带 bizNo 去重),因为重试必然产生重复请求。

方案二:RocketMQ 事务消息

半消息 → 本地事务 → commit/rollback → 回查,MQ 保证"本地成功则消息必达"。语义同本地消息表,只是把存储和重试交给了 MQ,省了自建表但强依赖 MQ。

方案三:TCC/Saga

B 是"扣减类"强一致要求操作时,用 Try-Confirm-Cancel 或 Saga 编排补偿,代价是每个参与方实现三个接口/补偿逻辑,只用在资金类链路。

兜底原则:无论哪种,都要有对账任务核对 A/B 状态,差异自动修复 + 告警,这是最后防线。