Skip to content

分布式事务方案选型

事务解决的是数据一致性问题——要么全成功,要么全失败。当操作跨越多个服务、数据库或第三方系统时,就进入了分布式事务的领域。

分布式事务没有银弹。每种方案都在一致性强度、性能损耗、实现复杂度、运维成本之间做取舍。选型的关键不是哪个方案”最好”,而是哪个方案最匹配你的业务约束。

分布式事务的最优解是不需要分布式事务:

  • 服务拆分时把强相关的数据操作留在同一个服务、同一个数据库,用本地事务保证 ACID
  • 评估业务是否真的需要强一致——很多场景下最终一致就够了
  • 将原子操作内聚到单个服务中,而不是分散到多个服务再用分布式事务”兜底”

一致性模型:你的业务需要多”一致”?

Section titled “一致性模型:你的业务需要多”一致”?”

CAP 定理指出,分布式系统在遇到网络分区时,必须在一致性和可用性之间选择:

特性含义
一致性(C)所有节点在同一时间看到相同的数据
可用性(A)每个请求都能在有限时间内得到响应
分区容错(P)网络分区(节点间通信中断)时系统仍能运行

一个常见误解是把”服务宕机”等同于”网络分区”——两者不同。网络分区是指节点都活着但彼此无法通信。在真实的分布式系统中,网络分区虽然不常发生,但必须为它做设计,因此 P 是前提,选择实际上是 CP 还是 AP。

CAP 只描述了分区发生时的选择,但大部分时间系统是正常运行的。PACELC 模型补充了这一点:

如果有分区(P),选择 A 还是 C;否则(E),选择低延迟(L)还是一致性(C)。

这意味着即使没有网络故障,你也在延迟和一致性之间做取舍。这才是日常开发中更频繁遇到的决策。

业务场景一致性要求推荐模型
资金、支付、账户余额强一致CP — 宁可短暂不可用也不能数据错
订单状态、库存扣减最终一致(秒级)AP + 补偿
通知、日志、统计最终一致(分钟级)AP — 异步即可

方案一:Transactional Outbox Pattern(本地消息表)

Section titled “方案一:Transactional Outbox Pattern(本地消息表)”

这是分布式事务中最通用、最可靠的方案,也是行业中应用最广泛的模式。

核心思想:将”业务操作”和”发送消息”放在同一个本地事务中,用数据库事务保证两者的原子性。

本地事务 {
1. 执行业务操作(如创建订单)
2. 写入消息到 outbox 表
}
→ 异步进程读取 outbox 表,投递消息到 MQ
→ 下游消费并处理

轮询(Polling)

最简单的实现:定时任务扫描 outbox 表,发送未投递的消息。

  • 优点:实现简单,无额外基础设施依赖
  • 缺点:轮询间隔决定了延迟下限,频繁轮询对数据库有压力

CDC(Change Data Capture)

用 Debezium 等工具监听数据库 binlog,实时捕获 outbox 表的变更并投递到 Kafka。

  • 优点:近实时、不增加数据库查询压力、与业务代码完全解耦
  • 缺点:需要部署和维护 Debezium + Kafka Connect

对于新项目,推荐 CDC 方案。轮询在早期够用,但随着消息量增长会成为瓶颈。

CREATE TABLE outbox_event (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合根类型,如 Order、Inventory',
aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合根 ID',
event_type VARCHAR(128) NOT NULL COMMENT '事件类型,如 OrderCreated',
payload JSON NOT NULL COMMENT '事件内容',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0=待发送 1=已发送',
retry_count INT NOT NULL DEFAULT 0,
max_retries INT NOT NULL DEFAULT 5,
next_retry_at DATETIME(3) COMMENT '下次重试时间',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
INDEX idx_status_retry (status, next_retry_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Outbox 事件表';

消息至少投递一次(at-least-once),消费端必须实现幂等:

  • 方案一:消费前查幂等表,已处理则跳过
  • 方案二:利用数据库唯一约束,重复插入自然失败
  • 关键:幂等键的选择比幂等机制本身更重要——用业务 ID + 操作类型,而不是消息 ID

跨服务的异步协作,是大多数微服务架构的首选方案。尤其适合:

  • 大流量场景(异步解耦,削峰填谷)
  • 对延迟要求不苛刻(秒级最终一致可接受)
  • 团队跨部门协作(无需对方接入特定中间件)

RocketMQ 的事务消息是 Outbox Pattern 的中间件化实现:

1. 发送半消息(Half Message)→ MQ 暂存,消费者不可见
2. 执行本地事务
3. 根据本地事务结果,提交或回滚半消息
4. 如果 MQ 未收到确认,主动回查本地事务状态
维度Outbox + CDCRocketMQ 事务消息
基础设施DB + Debezium + KafkaRocketMQ
业务侵入低(只需写 outbox 表)中(需实现回查接口)
可靠性依赖 DB 事务依赖 MQ 的半消息机制
适用 MQ任意仅 RocketMQ

团队已在使用 RocketMQ 且不想引入 CDC 基础设施时,事务消息是更轻量的选择。但要注意它锁定了 MQ 选型——如果未来要迁移到 Kafka,事务消息的实现需要全部重写。


Seata 是一个开源的分布式事务中间件,提供多种模式。适合团队能够承受额外运维一个有状态中间件的成本。

原理:通过 SQL 拦截,自动生成 undo log,失败时自动回滚。

全局事务开始
→ 分支事务1:执行 SQL,自动生成前后镜像(undo log)
→ 分支事务2:同上
全局事务提交 → 删除 undo log
全局事务回滚 → 根据 undo log 自动恢复

优点:业务代码零侵入,加个注解就能用。

缺点与风险

  • 全局锁机制可能导致热点行竞争和死锁
  • 每个分支事务都要生成前后镜像,写放大明显
  • 只支持关系型数据库
  • undo log 表需要部署到每个参与方的数据库

适用场景:内部管理系统、低并发的后台操作。不建议用在高并发的 C 端核心链路

原理:业务方自行实现三个阶段的逻辑,Seata 负责协调执行。

阶段职责示例(库存扣减)
Try预留资源冻结库存(available -= 10, frozen += 10)
Confirm确认提交扣除冻结(frozen -= 10)
Cancel释放资源解冻库存(available += 10, frozen -= 10)

优点

  • 无全局锁,性能好
  • 不限数据库类型
  • 资源预留期间不阻塞其他事务

缺点

  • 业务侵入性强——每个参与方都要实现三个接口
  • 需要新增字段(如冻结数量)来支持资源预留
  • 空回滚、悬挂问题需要额外处理

适用场景:高并发核心链路(支付、库存),对性能要求高且团队有能力维护补偿逻辑。

原理:将长事务拆分为一系列本地事务,每个本地事务有对应的补偿操作。失败时按逆序执行补偿。

T1 → T2 → T3(失败)→ C2 → C1

优点

  • 适合长流程、跨部门的业务编排
  • 支持通过状态机定义流程,可视化程度高
  • 天然适合有第三方调用的场景(第三方不可能接入你的事务框架)

缺点

  • 补偿逻辑复杂,每个步骤都需要幂等
  • 没有隔离性——中间状态对外可见
  • 状态机本身的学习和维护成本

适用场景:跨部门长流程(订单 → 支付 → 库存 → 物流),涉及外部系统调用。


协调者(Transaction Manager)分两阶段协调所有参与者:

阶段一(Prepare):协调者问所有参与者"能提交吗?" → 参与者锁定资源并响应
阶段二(Commit/Rollback):全部同意则提交,任一拒绝则回滚
  • 同步阻塞:Prepare 到 Commit 之间所有参与者持锁等待
  • 协调者单点:协调者宕机,参与者锁定状态无法释放
  • 数据不一致:Commit 阶段部分参与者网络超时,可能出现不一致

早期 XA 被广泛否定,但现代分布式数据库(如 TiDB、CockroachDB、OceanBase)在内部实现了优化版的 2PC,通过 Raft/Paxos 共识解决了协调者单点和一致性问题。

如果你的业务确实需要跨库强一致,优先考虑使用原生支持分布式事务的数据库,而不是在应用层实现 XA。

应用层的 XA 基本不推荐。但如果选用了 TiDB/OceanBase 等分布式数据库,其内置的事务机制本质上是优化过的 2PC,可以放心使用。


方案五:工作流引擎(Temporal / Cadence)

Section titled “方案五:工作流引擎(Temporal / Cadence)”

这是近年来快速崛起的现代方案,值得重点关注。

将分布式事务建模为持久化的工作流(Durable Workflow)

// Temporal Workflow 示例(概念性)
public void orderWorkflow(OrderRequest request) {
OrderResult order = activities.createOrder(request); // 自动重试
InventoryResult inv = activities.deductInventory(order); // 失败自动补偿
activities.sendNotification(order); // 幂等执行
}

工作流引擎保证:

  • 每个 Activity 至少执行一次,失败自动重试
  • 工作流状态持久化,进程崩溃后从断点恢复
  • 支持定义补偿逻辑(类似 SAGA,但编程模型更自然)
  • 内置超时、重试策略、可视化监控
维度Outbox + MQSeataTemporal
编程模型事件驱动(异步回调)注解/接口顺序代码(像写本地逻辑)
状态管理自行维护中间件管理引擎自动持久化
可观测性需自建有限内置 Dashboard
学习曲线中高
运维成本MQ + DBSeata ServerTemporal Server + DB
  • 业务流程复杂、步骤多、涉及多个外部系统
  • 需要可视化流程状态和执行历史
  • 团队愿意接受新的编程模型

业务需要分布式事务吗?
├── 能通过服务合并避免 → 合并服务,用本地事务
└── 不能避免
├── 需要强一致?
│ ├── 可以用分布式数据库 → TiDB / OceanBase(内置优化 2PC)
│ └── 不能换数据库
│ ├── 低并发内部系统 → Seata AT
│ └── 高并发核心链路 → TCC
└── 最终一致可接受?
├── 流程简单(2-3 步)→ Outbox Pattern(轮询或 CDC)
├── 已用 RocketMQ → 事务消息
├── 流程复杂 / 涉及第三方 → SAGA 或 Temporal
└── 长流程 + 需要可视化 → Temporal

无论选择哪种方案,幂等都是基础要求。每个参与方都必须能够安全地处理重复请求。

  • 所有远程调用设置合理的超时时间
  • 重试使用指数退避 + 抖动(jitter),避免重试风暴
  • 区分可重试错误(网络超时)和不可重试错误(参数校验失败)

分布式事务的调试难度远高于本地事务。从第一天就要建立:

  • 全局事务 ID 贯穿所有服务的日志
  • 每个分支事务的状态、耗时、重试次数
  • 补偿操作的执行情况
  • 长时间未完成的事务告警

所有自动化方案都需要人工兜底:

  • 死信队列处理多次重试失败的消息
  • 提供管理后台查看和手动处理异常事务
  • 对账机制:定期比对上下游数据,发现不一致及时修复