分布式事务在复杂场景中的设计
前几篇讲的是"标准场景"下的事务方案,生产里还有三类硬骨头:跨数据源、跨消息队列、跨语言调用。本文围绕这三类场景给出设计方法,并总结柔性事务的兜底方案(对账、补偿、幂等)。
复杂场景总览
三类复杂场景:
├─ 跨数据源:一个服务接多个库 / 多个异构数据库
├─ 跨消息队列:一次业务涉及多套 MQ,或 MQ 与数据库联合
└─ 跨语言调用:Java + Go + Python 混合架构参与同一事务
共同点:
├─ 没有单一框架能全覆盖
└─ 需要"框架 + 设计 + 兜底"三管齐下一、跨数据源场景
场景一:单服务多数据源
典型情况:
一个微服务同时操作两个数据库(如订单库 + 用户库)
方案一:Seata AT(多数据源)
├─ 每个数据源包一个 DataSourceProxy
├─ 同一全局事务下,两个库各注册一个分支事务
└─ 任一失败 → 双库都回滚
方案二:XA(数据库原生)
├─ 两库都支持 XA → 直接 XA 二阶段
└─ 锁资源时间更长Seata 多数据源配置
java
@Configuration
public class DataSourceConfig {
@Bean("orderDataSource")
@ConfigurationProperties(prefix = "spring.datasource.order")
public DataSource orderDataSource() { return new DataSource(); }
@Bean("userDataSource")
@ConfigurationProperties(prefix = "spring.datasource.user")
public DataSource userDataSource() { return new DataSource(); }
// 每个数据源都包一层 Seata 代理
@Bean
@Primary
public DataSourceProxy orderDataSourceProxy(@Qualifier("orderDataSource") DataSource ds) {
return new DataSourceProxy(ds);
}
@Bean
public DataSourceProxy userDataSourceProxy(@Qualifier("userDataSource") DataSource ds) {
return new DataSourceProxy(ds);
}
}注意点:
├─ undo_log 表要在每个库各建一张
├─ 事务分组按库区分,或共用一个分组
└─ 分布式主键 / 路由要对齐(分库分表后行锁粒度)场景二:异构数据库
MySQL + PostgreSQL + TiDB 混用:
├─ Seata AT 已支持多种数据库的镜像生成
├─ 但 SQL 方言差异大 → 复杂 SQL 建议避免
├─ TiDB 本身支持分布式事务 → 同库内无需 Seata
└─ 跨 TiDB 与 MySQL → 用 TCC / SAGA / 消息方案更稳场景三:分库分表 + 分布式事务
分库分表(ShardingSphere)与 Seata 整合:
├─ 逻辑库背后是多个物理库
├─ 一次写操作可能路由到多个物理库
└─ Seata 对每个物理库注册分支事务
设计要点:
├─ 分片键设计要保证"同一全局事务尽量落在同一库"
├─ 全局锁的粒度是物理表行 → 跨分片冲突概率增加
└─ 高并发场景优先 TCC(预留式,无全局锁)二、跨消息队列场景
场景一:业务 + MQ 一致性
问题:写库成功,发消息失败 → 下游不知道
方案:
├─ RocketMQ 事务消息(发送与本地事务一致)
├─ 本地消息表(同库事务 + 定时投递)
└─ 事务消息 + 消费幂等 + 消息对账场景二:多套 MQ 并存
Kafka + RocketMQ + RabbitMQ 混用:
├─ 业务写多个 MQ → 无法保证原子性
├─ 方案:引入"消息路由中间层"
│ └─ 业务只写一个本地消息表 / 一个主 MQ
│ 由路由层转发到其他 MQ(最终一致)
└─ 或:拆分业务边界,一个 MQ 只服务一个领域场景三:MQ 消费端加入全局事务
需求:消费消息后,写库操作也参与上游的全局事务
方案一:XID 随消息头传播
├─ 生产端把 XID 放入消息头
├─ 消费端取出 XID → 续接全局事务
└─ 限制:消息处理是异步的,全局事务可能已超时/提交
方案二:消息只做"触发",消费端开启独立全局事务
├─ 生产端只保证消息可靠投递
└─ 消费端自己开启全局事务处理业务
└─ 常见且可靠,缺点是上下游最终一致靠消息链路
结论:跨 MQ 与数据库的强一致在异步场景下几乎不可行,
采用"可靠投递 + 独立事务 + 对账"是主流消息幂等设计
重复消费是常态(至少一次投递):
├─ 唯一键去重:业务表加唯一约束(订单号、流水号)
├─ 去重表:消费前 insert 去重表,冲突则跳过
├─ 状态机校验:订单状态只有"待支付→已支付"才处理
└─ Redis 幂等:SETNX 标记已处理(注意过期与并发)三、跨语言调用场景
异构语言参与事务的难点
Java 体系的 Seata 是"重客户端":
├─ 需要在应用内集成 RM(数据源代理)
└─ Go / Python / Node 无法直接复用
可选路径:
1. Seata 官方多语言客户端(go、python 等,支持有限)
2. TCC / SAGA 用 HTTP / gRPC 接口参与(语言无关)
3. 消息方案(语言无关性最好)
4. 边界隔离:异构服务用本地事务,通过消息加入流程语言无关方案对比
| 方案 | 语言无关性 | 实现成本 | 一致性 |
|---|---|---|---|
| Seata 官方客户端 | 中(覆盖常用语言) | 中 | 较强 |
| TCC(HTTP 接口) | 高 | 高(三接口) | 较强 |
| SAGA(HTTP 接口) | 高 | 中(状态机) | 最终一致 |
| 消息最终一致 | 高 | 低 | 最终一致 |
| XA(数据库层) | 高 | 低 | 强一致 |
边界划分原则
跨语言场景设计铁律:
├─ 全局事务的"入口"尽量留在 Java 侧(TM)
├─ 异构服务尽量用 TCC / SAGA / 消息参与
├─ 不要把异构服务放进 AT 模式(无代理能力)
└─ 用明确的接口契约(Try/Confirm/Cancel 语义)代替框架绑定示例:Java 编排 Go 服务
Java 侧(SAGA 编排器):
├─ 状态机定义:T1(Java 扣库存) → T2(Go 记账) → T3(Java 发积分)
├─ T2 通过 HTTP 调用 Go 服务
└─ T2 失败 → 触发 C1(Java 补偿库存)
Go 侧:
├─ 实现"记账"与"撤销记账"两个 HTTP 接口
├─ 接口保持幂等
└─ 不需要集成任何分布式事务 SDK四、事务与缓存的一致性
分布式事务与缓存(Redis)也是常见矛盾点:
├─ 问题:更新数据库成功,缓存更新失败 → 读到旧数据
├─ 方案一:Cache-Aside + 延迟双删
│ 更新库 → 删缓存 → 延迟(如 500ms)再删一次
├─ 方案二:消息驱动缓存失效(可靠投递)
├─ 方案三:缓存不跨事务,只做可容忍的读缓存
└─ 原则:缓存一致性走"最终一致"思维,别用事务硬撑五、柔性事务兜底方案
无论选哪个框架,最终都要靠兜底方案保证数据收敛。
1. 对账机制
对账 = 定期比较两端数据,发现差异并修复
实现方式:
├─ 数据库对账:日终跑 SQL 对比双方流水
├─ 消息对账:对比消息表与业务表,找出"发了没消费"的
└─ 外部对账:与支付渠道 / 供应商对账(异步回调 + 主动查询)
修复方式:
├─ 自动修复:补发消息、补记流水、状态回拨
└─ 人工介入:差异告警 → 工单 → 人工处理2. 补偿任务设计
补偿任务 = 定时扫描"半完成"数据,推动流程走完
通用模式:
1. 业务表加状态字段(如 PROCESSING / SUCCESS / FAILED)
2. 定时任务扫描超时未完成的 PROCESSING 记录
3. 按业务规则重试 / 反查 / 补偿
4. 多次失败 → 转人工队列
注意:
├─ 补偿必须幂等(可能重复执行)
├─ 补偿要有限次 + 退避(避免雪崩)
└─ 补偿操作要有完整日志(审计追溯)3. 幂等设计清单
分布式环境下重复调用的防护:
├─ 接口层:幂等键(Idempotency-Key 请求头)
├─ 数据库层:唯一约束 / 状态机校验
├─ 消息层:消费去重
├─ 补偿层:补偿表记录已补偿的 key
└─ 对账层:按流水号去重4. 状态机驱动
用状态机约束"合法流转",从根上防止乱序操作:
订单状态机:
待支付 → 已支付 → 已发货 → 已完成
↘ 已取消(仅待支付可取消)
任何操作先校验当前状态:
已取消的订单不能发货
已发货的订单不能重复支付六、复杂场景设计原则
设计原则五条:
├─ 1. 缩小事务边界:能本地事务绝不分布式
│ 拆分业务,减少跨库 / 跨服务调用
├─ 2. 异步化:能最终一致就别强一致
│ 核心链路同步,非核心链路异步 + 对账
├─ 3. 幂等是底线:所有入口、所有补偿都要幂等
├─ 4. 对账兜底:没有对账的分布式事务是"裸奔"
└─ 5. 监控闭环:事务成功率、回滚率、对账差异全程可视总结
跨数据源、跨消息队列、跨语言调用这三类复杂场景,靠单一框架解决不了全部问题,正确姿势是框架 + 架构设计 + 兜底机制的组合:跨数据源用 Seata 多数据源或 XA;跨 MQ 走"可靠投递 + 独立事务 + 对账";跨语言用 TCC/SAGA 接口化或消息方案。最后用对账、补偿、幂等、状态机四件套兜底,让系统在异常频发的生产环境中依然能把数据收敛到一致。