当前有个本地操作 A,远程操作 B,我需要保证 A 和 B 事务的一致性,你会如何实现?
- 标签:后端、场景题
🏛 业务架构师
本地操作 A(如本地事务写库)和远程操作 B(调下游服务/写另一个系统)的一致性,是分布式事务最常见形态。我的答案是本地消息表 + 最终一致为主,按业务强度分档补充。
第一档:本地消息表(80% 场景的正解)
本地事务: 写业务表 + 写消息表(同一DB, 原子)
异步: 定时任务/MQ 把消息表记录投递给远程B
远程B: 幂等处理, 返回成功 -> 消息表标记完成
兜底: 超时未完成的记录重试 + 告警人工介入- 核心:用本地事务的原子性保证"业务成功则消息必达",用重试 + 幂等保证 B 最终执行
- A 成功 B 失败:重试兜底;B 成功但响应丢失:B 的幂等性让重试无害
- 我最推荐它,因为每一步都可观测(消息表就是看板)、可人工补偿,比黑盒事务框架可控
第二档:TCC(资金级强要求) Try 预留资源 → Confirm 确认 / Cancel 释放,每个参与方实现三个接口。侵入大、开发成本高,只在资金冻结/扣减这种"中间态必须是业务正确"的场景用。
第三档:Saga(长流程编排) 正向链路 + 逆向补偿链,适合跨多系统的长事务(开户流程:创建账户→发卡→通知),接受"中间态可见",靠补偿回滚。
架构师的关键观点
- 先问业务能容忍多久不一致:秒级(通知类)→ 消息表异步就行;必须同生共死(余额扣减)→ TCC;大多数团队高估了自己对强一致的需要
- 幂等是一切方案的地基:没有幂等,任何重试机制都会制造重复数据
- 必须配对账:本地消息表方案我一定会加 T+1 对账(A 表 vs B 系统数据核对),把"漏网之鱼"从用户投诉变成日终工单——资损兜底靠对账,不靠框架
- 拒绝 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 状态,差异自动修复 + 告警,这是最后防线。