解决什么问题?

我们都知道数据库可以通过事务来保证原子性操作,但实际场景中存在多个 DB实例或者微服务,例如扣减余额和扣减库存,如果余额和库存不在同一个 DB 实例中我们又该如何保证原子性操作?这就是分布式事务的概念。

首先需要铺垫一些理论:

CAP 理论

CAP 定理是分布式系统中的一个重要定理,它指出在一个分布式系统中,一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)这三个特性不能同时满足,最多只能满足其中两个。以下是对这三个特性的详细解释:

  • 一致性(Consistency):在分布式系统中,一致性指的是所有节点在同一时间具有相同的数据。也就是说,当一个数据在某个节点上被更新后,这个更新能够迅速传播到其他节点,使得所有节点都能看到最新的数据,就好像整个系统只有一个数据副本一样。例如,在一个分布式数据库中,如果一个用户在节点 A 上修改了一条记录,那么其他节点在查询该记录时,应该能够立即看到修改后的结果。
  • 可用性(Availability):可用性意味着系统中的每个请求都能得到响应,而不会出现长时间的等待或系统崩溃导致无法响应的情况。无论系统中是否发生了故障,只要客户端发送请求,系统都应该能够在一定的时间内给出响应,保证服务的正常运行。例如,一个在线购物网站,无论何时用户进行访问、下单等操作,都能得到相应的反馈,而不是出现页面长时间加载或无法访问的情况。
  • 分区容错性(Partition Tolerance):分布式系统通常由多个节点组成,这些节点通过网络进行通信。分区容错性是指当网络出现分区(即部分节点之间无法进行通信)时,系统仍然能够继续运行。也就是说,即使系统中的某些节点之间失去了联系,整个系统也不会因此而崩溃,仍然能够在其他可用的节点上提供服务。例如,在一个跨数据中心的分布式系统中,由于网络故障导致两个数据中心之间的通信中断,但每个数据中心内部的节点仍然能够正常工作,继续为用户提供服务。

⚠️ 在分布式系统中,网络分区(P)一定可能发生(链路抖动、网络设备故障等无法避免),所以 P 是必选项

BASE 理论

BASE 理论指的是基本可用 Basically Available,软状态 Soft State,最终一致性 Eventual Consistency,核心思想是即便无法做到强一致性,但应该采用适合的方式保证最终一致性。

BASE,Basically Available Soft State Eventual Consistency 的简写:
BA:Basically Available 基本可用,分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
S:Soft State 软状态,允许系统存在中间状态,而该中间状态不会影响系统整体可用性。
E:Consistency 最终一致性,系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。
BASE 理论本质上是对 CAP 理论的延伸,是对 CAP 中 AP 方案的一个补充。

柔性事务

不同于 ACID 的刚性事务,在分布式场景下基于 BASE 理论,就出现了柔性事务的概念。要想通过柔性事务来达到最终的一致性,就需要依赖于一些特性,这些特性在具体的方案中不一定都要满足,因为不同的方案要求不一样;但是都不满足的话,是不可能做柔性事务的。

幂等操作

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,支付流程中第三方支付系统告知系统中某个订单支付成功,接收该支付回调接口在网络正常的情况下无论操作多少次都应该返回成功。

解决方案

分布式事务

2PC 和 3PC 在理想情况下可以保证强一致性,但存在全局阻塞的情况,性能受到影响。

TCC 、 Saga 、本地消息表和事务消息 保证的是最终一致性,性能较好,但代码侵入性很高。

两阶段提交 2PC

2PC 流程

两种参与者:协调者和参与者。

  • 第一阶段,协调者让所有参与者执行事务,参与者把执行结果返回给协调者,如果某一个参与者执行失败,协调者会发送回滚事务给所有参与者。

  • 第二阶段:如果所有参与者执行成功,协调者就让所有参与者提交事务。适用于对数据一致性要求高、并发度低的场景,如金融系统转账业务。

优点:理想情况下可以保证分布式节点的强一致性

缺点:

  1. 如果参与者初始就故障,其他参与者会白白执行事务,存在资源浪费。
  2. 协调者挂掉,参与者执行事务后阻塞等待协调者的通知。
  3. 参与者等待协调者时阻塞,会导致其他访问数据库的事务可能被阻塞。

数据不一致的情况:第二阶段,如果部分参与者收到提交命令,部分参与者因为网络原因没有收到提交命令,就会导致数据不一致。

三阶段提交 3PC

3PC 流程

为了解决 2PC 的两个问题,引入了3PC,在开头增加了询问是否故障的 canCommit 阶段,就是询问机器是否可以正常运行。其他两个阶段与 2PC 一致。

引入了超时机制。第二阶段,如果协调者超时没有收到参与者的回复,协调者就会认为执行异常,通知参与者回滚。第三阶段,如果参与者超时没有收到协调者的提交命令,则自动进行提交。

三阶段提交也不能完全保证数据一致性,如果存在网络丢包或者阻塞问题,比如由于网络阻塞导致协调者发送的回滚命令未按时到达,就会导致错误提交。

2PC 偏向于牺牲可用性(阻塞),3PC 尝试改善可用性,但牺牲了一致性。

现代分布式系统往往不用 2PC/3PC 来追求强一致性,而是采用:

  • 共识协议:Paxos、Raft(强一致性,适合元数据、配置管理)。
  • 补偿机制:TCC(Try-Confirm-Cancel)、Saga(长事务补偿,保证最终一致性)。
  • 幂等操作 + 消息重试:保证即使失败重试,也不会导致数据错误。

TCC

TCC 是一种 柔性事务(最终一致性)方案,它不是数据库级事务,而是应用层事务控制,由于它对代码侵入性很强,一般不用。

它把一个完整业务拆成三步:

  1. Try(预留):检查并预留资源(不真正扣减)。
  2. Confirm(确认):真正执行业务操作(提交)。
  3. Cancel(取消):如果失败则回滚,把预留的资源释放掉。

举个例子:转账 100 元(A → B)

假设要实现从 账户 A 转账 100 元给账户 B
在分布式场景下,A 和 B 可能不在同一个数据库/服务里,需要跨服务保证一致性。

如果用 2PC:

  • 第一步锁定 A、B 的账户行 → 可能阻塞很久。
  • 如果协调者宕机,A 和 B 状态可能不一致。

如果用 TCC 来做,我们把业务拆成 Try、Confirm、Cancel 三步:

Step 1: Try(预留资源)

  • A 账户服务:检查余额是否足够,预留 100 元(冻结)。
  • B 账户服务:检查账户是否正常,预留可接收额度。

👉 此时并没有真正扣减 A 的钱,也没有加到 B 上,只是把资源锁定。

Step 2: Confirm(确认提交)

  • 如果所有服务的 Try 都成功:
    • A 账户服务:真正扣减余额(-100)。
    • B 账户服务:真正加钱(+100)。

👉 这一步才是业务生效。

Step 3: Cancel(取消操作)

  • 如果在 Try 过程中发现问题(比如 A 余额不足、B 账户异常),或者 Confirm 阶段某个服务执行失败:
    • A 账户服务:解冻预留的 100 元。
    • B 账户服务:撤销接收额度。

👉 确保即使失败,也能回滚到初始状态。

TCC 的优缺点:

优点:

  • 灵活:业务逻辑由应用自己控制,而不是强依赖数据库。
  • 避免了长时间锁表/行,提高性能。
  • 明确的补偿机制(Cancel)。

缺点:

  • 开发复杂:每个操作都要实现 Try/Confirm/Cancel 三个接口。
  • 补偿要考虑幂等性(Cancel 可能被调用多次)。
  • 业务侵入性强:应用层要设计好冻结、解冻逻辑。

Saga

Saga 也是一种 柔性事务 模型,和 TCC 一样,目标是解决分布式系统里的事务一致性问题。不同点在于:

  • TCC:每个步骤都有 Try / Confirm / Cancel 三个接口。
  • Saga:把一个大事务拆分成一系列 有序的子事务(Tx1, Tx2, …),每个子事务都有一个 补偿操作(Cx1, Cx2, …)。
  • 如果中途有步骤失败,就执行前面已完成步骤的 补偿操作,保证最终一致性。

👉 可以理解为:Saga 是一串“正向操作 + 对应补偿”的链条。

举个例子:电商下单

用户下单,流程包括:

  1. 扣减库存(Tx1)
  2. 扣减余额(Tx2)
  3. 生成订单(Tx3)

Saga 正常执行

  • Tx1:库存服务扣减库存
  • Tx2:支付服务扣减余额
  • Tx3:订单服务生成订单

执行完成 → 事务结束。

Saga 出现失败(比如余额不足)

假设 Tx2(扣减余额)失败了:

  • 执行补偿操作:
    • Cx1:库存服务回滚(加回库存)
  • Saga 结束,保证最终一致性。

Saga 出现失败(比如生成订单失败)

如果 Tx3(生成订单)失败:

  • 执行补偿操作:
    • Cx2:支付服务回滚(退回余额)
    • Cx1:库存服务回滚(加回库存)
  • Saga 结束。

Saga 的特点

优点

  • 实现相对简单:只需要正向操作 + 补偿操作,不像 TCC 需要 Try/Confirm/Cancel 三套接口。
  • 长事务友好:适合跨多个系统、耗时长的业务(比如机票预订、酒店预订、支付组合场景)。
  • 最终一致性:保证系统最终达到一致状态。

缺点

  • 中间状态可见:补偿之前,部分子事务已经生效(比如库存已扣减,但订单失败了 → 瞬间用户看到库存减少)。
  • 补偿可能失败:必须保证补偿操作的幂等性和可靠性。
  • 业务侵入性:每个子事务都需要设计对应的补偿逻辑。

事务消息

RocketMq事务消息

事务消息通常分两阶段:

  1. 半消息(Prepared Message)
    生产者先把消息投递到 MQ,但标记为“半消息”,消费者看不到。
  2. 本地事务执行
    生产者执行数据库事务,比如扣减库存、更新订单状态。
  3. 确认或回滚消息
    • 如果数据库事务成功,生产者向 MQ 提交确认,半消息转为真正的消息,消费者可以消费。
    • 如果数据库事务失败,生产者通知 MQ 回滚消息,半消息被丢弃。
  4. 补偿机制(事务回查)
    如果 MQ 在一段时间内没有收到生产者的确认,MQ 会回查生产者,询问本地事务到底是成功还是失败,然后根据结果决定提交或回滚。

举个例子:电商下单扣库存

假设用户在电商系统里下单,需要执行两个动作:

  1. 订单服务:生成订单记录(写数据库)。
  2. 库存服务:扣减商品库存(异步消费 MQ 消息)。

如果用事务消息,流程是这样的:

  1. 订单服务发送一条“扣库存”的 半消息 到 MQ,消费者看不到。
  2. 订单服务开启本地事务:
    • 在订单数据库里插入一条“待支付”的订单。
    • 本地事务提交成功。
  3. 本地事务成功后,订单服务再告诉 MQ:确认提交半消息。
    • MQ 将这条“扣库存”的消息变为可见。
    • 库存服务消费到消息,减少库存。
  4. 如果订单数据库事务失败(比如插入订单出错),则通知 MQ 回滚半消息,消息直接被删除。
  5. 如果网络抖动,确认消息丢失,MQ 会主动回查订单服务:
    • 如果发现订单已创建,则重新提交半消息。
    • 如果发现订单不存在,则回滚半消息。

这样,保证了 订单和扣库存消息的一致性

本地消息表

本地消息表

  1. 当系统 A 被其他系统调用发生数据库表更操作,首先会更新数据库的业务表,其次会往相同数据库的消息表中插入一条数据,两个操作发生在同一个事务中
  2. 系统 A 的脚本定期轮询本地消息往 mq 中写入一条消息,如果消息发送失败会进行重试
  3. 系统 B 消费 mq 中的消息,并处理业务逻辑。如果本地事务处理失败,会在继续消费 mq 中的消息进行重试,如果业务上的失败,可以通知系统 A 进行回滚操作

本地消息表实现的条件:

  1. 消费者与生成者的接口都要支持幂等
  2. 生产者需要额外的创建消息表
  3. 需要提供补偿逻辑,如果消费者业务失败,需要生产者支持回滚操作

容错机制:

  1. 步骤 1 失败时,事务直接回滚
  2. 步骤 2、3 写 mq 与消费 mq 失败会进行重试
  3. 步骤 3 业务失败系统 B 向系统 A 发起事务回滚操作

此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。