分布式事务是指在多个独立的数据存储节点(如数据库、服务、微服务等)之间协调完成的一组操作,这些操作要么全部成功提交,要么全部回滚,以保证数据的一致性。

事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

在分布式系统中,由于网络延迟、节点故障等问题,实现事务的一致性比单机事务复杂得多。

分布式事务的产生的原因

  • 数据库分库分表

  • 应用 SOA 化(业务多服务拆分)

  • 分布式场景,多节点

挑战与目标

分布式事务的核心挑战,就是在不可靠的分布式环境中,尽可能地维持事务的原子性和一致性

在单机数据库中,事务满足 ACID 特性:

  • Atomicity(原子性):操作不可分割;

  • Consistency(一致性):事务前后数据合法;

  • Isolation(隔离性):并发事务互不干扰;

  • Durability(持久性):提交后结果永久保存。

而在分布式环境下,ACID 的实现变得极其困难,因为:

  • 网络可能延迟、丢包、分区;

  • 节点可能宕机;

  • 各子系统可能使用不同数据库或技术栈。

典型场景举例

  1. 电商下单

    • 扣减库存(库存服务)
    • 创建订单(订单服务)
    • 扣减用户余额(账户服务)

    三者必须同时成功或同时失败。

  2. 跨数据库转账

    • 从 MySQL 的 A 账户扣款
    • 向 PostgreSQL 的 B 账户加款

    需跨异构数据库保证一致性。

  3. 微服务架构中的状态同步

    • 用户服务创建用户
    • 消息服务发送欢迎邮件
    • 日志服务记录审计日志

    要么全做,要么全不做(或通过补偿机制最终一致)。

分布式事务的分类(按一致性强度):

类型 一致性级别 典型方案 特点
强一致性事务 严格 ACID 2PC、XA 数据实时一致,但性能低、可用性差
最终一致性事务 宽松一致性 TCC、Saga、消息队列 允许短暂不一致,通过补偿或重试达成最终一致,性能高

实际系统中,最终一致性方案使用更广泛(如电商、支付),因为强一致性代价太高。

分布式事务通常选择 CP(优先一致性和分区容错),在网络分区时可能拒绝服务(如 2PC 阻塞)。而最终一致性方案则偏向 AP,牺牲强一致换取高可用。

事务协议

XA 是 2PC 的工业标准实现规范,而 3PC 是 2PC 的理论扩展。

2PC 是思想,XA 是它的工业标准落地,3PC 是它的“理想化升级版”但难以实用。

特性对比

维度 2PC XA 3PC
一致性保证 强一致性(ACID) 强一致性(ACID) 强一致性
是否阻塞 是(协调者或参与者故障会导致阻塞) 是(继承 2PC 的阻塞特性) 理论上非阻塞(超时自动决策)
容错能力 协调者单点故障 → 系统停滞
参与者故障 → 可恢复但复杂
同 2PC 能容忍协调者或部分参与者故障
通信轮次 2 轮(Prepare + Commit/Abort) 2 轮(通过 xa_prepare + xa_commit/rollback) 3 轮(CanCommit + PreCommit + DoCommit)
复杂度 中等 中等(需数据库/MQ 支持 XA 接口) 高(状态机复杂,需维护更多阶段状态)
工业应用 广泛(尤其在传统数据库) 广泛(Oracle、MySQL、PostgreSQL、ActiveMQ 等支持) 极少(因理论假设强,实际网络环境不满足)
网络分区容忍性 不容忍(CP 系统) 不容忍 不满足(异步网络下无法保证一致性)

XA 是 2PC 的升级标准规范,XA = 2PC + 标准化接口 + 资源管理约定

  • 2PC/ XA 追求强一致性,接受阻塞;
  • 3PC 试图在协调者故障时避免阻塞
  • 超时机制:3PC 在 PreCommit 阶段后引入超时,若未收到 DoCommit,参与者可自行提交(假设多数已同意)。

典型问题对比

问题 2PC / XA 3PC
协调者宕机 所有参与者阻塞,直到协调者恢复 参与者在超时后可自行决策(提交或回滚)
参与者宕机 协调者需等待或超时回滚(可能不一致) 类似,但状态更多,恢复更复杂
脑裂(网络分区) 可能出现部分提交、部分回滚(数据不一致) 仍无法避免,甚至可能加剧不一致
  • 传统数据库跨库事务(如银行转账,订单中心等)时,可选择2PC / XA 协议

  • 高并发、高可用、可接受最终一致时,使用 Saga / TCC / 消息队列 模式的实现

两阶段提交(2PC 协议)

事务管理器分两个阶段来协调资源管理器,第一阶段准备资源,也就是预留事务所需的资源,如果每个资源管理器都资源预留成功,则进行第二阶段资源提交,否则协调资源管理器回滚资源。

2PC 能保证强一致性(ACID),它包含两个角色:

  • 参与者(Participants):执行事务操作的节点。

  • 协调者(Coordinator):负责协调各参与者的提交或回滚。

  1. 准备阶段(Prepare Phase)

    • 协调者向所有参与者发送“准备”请求,询问是否可以提交事务。
    • 参与者执行事务操作(但不提交),记录事务日志(如 undo/redo log),并锁定资源。
    • 若参与者可以提交,则返回 Yes;否则返回 No

    具体流程图如下:

    img
  2. 提交阶段(Commit Phase)

    • 如果所有参与者都返回 Yes,协调者发送 Commit 命令,各参与者正式提交事务并释放资源。
    • 若任一参与者返回 No 或超时,协调者发送 Abort 命令,所有参与者回滚事务。

    协调者节点收到所有参与者节点反馈的ACK消息后,完成事务:
    img

事务回滚:

如果任意一个参与者节点在第一阶段返回 No,或者协调者在第一阶段的询问超时之前,无法获取所有参与者节点的响应消息时,那么这个事务将会被回滚,具体流程如下:

① 协调者向所有参与者发出 rollback 回滚操作的请求
② 参与者利用阶段一写入的undo信息执行回滚,并释放在整个事务期间内占用的资源
③ 参与者在完成事务回滚之后,向协调者发送回滚完成的ACK消息
④ 协调者收到所有参与者反馈的ACK消息后,取消事务

img

存在的问题:单点故障(协调者宕机则系统阻塞)、同步阻塞、性能差、恢复复杂。

二阶段提交确实能够提供原子性的操作,但是还有以下缺点:

(1)性能问题:执行过程中,所有参与节点都是事务阻塞性的,当参与者占有公共资源时,其他第三方节点访问公共资源就不得不处于阻塞状态,为了数据的一致性而牺牲了可用性,对性能影响较大,不适合高并发高性能场景

(2)可靠性问题:2PC非常依赖协调者,当协调者发生故障时,尤其是第二阶段,那么所有的参与者就会都处于锁定事务资源的状态中,而无法继续完成事务操作(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

(3)数据一致性问题:在阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

(4)二阶段无法解决的问题:协调者在发出 commit 消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了,那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

三阶段提交(3PC 协议)

为解决 2PC 的阻塞问题,在其基础上增加一个“预提交”阶段。

  1. CanCommit:协调者询问参与者是否可以执行事务(轻量试探)。

  2. PreCommit:若所有参与者同意,则协调者发送 PreCommit,参与者准备提交并响应。

  3. DoCommit:协调者确认后发送正式提交命令。

处理流程如下:

img

与2PC相比,3PC降低了阻塞范围,并且在等待超时后,协调者或参与者会中断事务,避免了协调者单点问题,阶段三中协调者出现问题时,参与者会继续提交事务。

数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者因为网络问题无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

特点

  • 引入超时机制,允许参与者在协调者故障时自行决策。

  • 减少阻塞,但无法完全避免不一致(网络分区时仍可能分裂决策)。

  • 实现复杂,实际应用较少。

  • 3PC 要求网络 “同步通信”(消息延迟有上限),但真实网络是异步的(可能任意延迟),因此 3PC 无法在实际分布式系统中保证一致性

XA 协议(2PC 的标准)

XA是一个分布式事务协议,由Tuxedo提出。

XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。

弱点:性能较低,无法满足高并发场景。在mysql数据库中支持不太理想,mysql的XA实现,没有记录准备阶段日志,主备切换回导致主库与备库数据不一致

参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

img

事务模式

分布式事务实现的多种模式,一般是应用协议,具体实现分布式事务。

模式 一致性类型 类型 阻塞 性能 业务侵入 适用场景
TCC 最终一致性 补偿型 金融、电商核心链路
依赖业务代码实现 Try/Confirm/Cancel 三阶段
Saga 最终一致性 长事务编排 长流程、微服务编排
本地事务+补偿操作,流程由业务或编排器控制
消息队列 最终一致性 中间件扩展 异步解耦、通知类场景
RocketMQ/Kafka 事务消息,厂商特定实现
Seata(AT/XA) 强/最终一致性 框架提供 视模式 中高 低(AT) 微服务架构通用方案
AT 模式是自定义协议,XA 模式基于 XA 协议

AT 模式

AT 模式 是开源分布式事务框架 Seata 提供的一种对业务无侵入的分布式事务解决方案。其核心目标是:像使用本地事务一样使用分布式事务,开发者只需关注业务 SQL,Seata 自动完成全局事务的协调、回滚和提交。

  • 业务代码零改造(只需加 @GlobalTransactional 注解);

  • 无长时间锁(本地事务提交即释放 DB 锁);

  • 高性能(一阶段直接提交,二阶段异步)。

Seata AT 模式包含三个核心角色:

组件 作用
事务协调者
TC(Transaction Coordinator)
全局事务协调器,维护全局事务状态,驱动二阶段提交/回滚
事务管理器
TM(Transaction Manager)
全局事务发起者(通常在业务服务中),定义事务边界(@GlobalTransactional)。
开启、提交或回滚全局事务
资源管理器
RM(Resource Manager)
分支事务管理者(集成在业务服务中),负责 undo log 管理、与 TC 通信。
与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

RM 以 Java Agent 或 SDK 形式嵌入应用,拦截 JDBC 操作

其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。

核心思想

“一阶段提交 + 二阶段异步回滚”,基于 支持本地 ACID 事务关系型数据库

  • 一阶段(Phase 1):本地事务提交时,Seata 自动记录 undo log(回滚日志),并注册分支事务到 TC,本地事务直接提交(不锁定资源)。

  • 二阶段(Phase 2)

    • 提交:TC 通知各分支“删除 undo log”(异步、快速);
    • 回滚:TC 通知各分支“根据 undo log 逆向生成补偿 SQL 并执行”。
sequenceDiagram
    participant TM as TM(@GlobalTransactional)
    participant TC as TC(协调器)
    participant RMA as RM-A(服务A)
    participant RMB as RM-B(服务B)

    TM->>TC: 开启全局事务(XID)
    TC-->>TM: 返回 XID

    TM->>RMA: 执行 SQL (带 XID)
    RMA->>RMA: 解析SQL → 生成 before/after image
    RMA->>RMA: 插入 undo_log + 业务SQL → 本地提交
    RMA->>TC: 注册分支事务

    TM->>RMB: 执行 SQL (带 XID)
    RMB->>RMB: 同上:生成 undo_log + 本地提交
    RMB->>TC: 注册分支事务

    alt 全局提交
        TC->>RMA: 删除 undo_log
        TC->>RMB: 删除 undo_log
        Note right of TC: 异步快速完成
    else 全局回滚
        TC->>RMA: 回滚分支(执行补偿SQL)
        TC->>RMB: 回滚分支(执行补偿SQL)
        Note right of TC: 基于 undo_log 自动还原
    end

场景:跨库转账(account_db1 → account_db2)

  1. 一阶段:本地提交 + 注册分支

    1
    2
    3
    4
    5
    6
    7
    @GlobalTransactional
    public void transfer(String from, String to, BigDecimal amount) {
    // 1. 扣减 from 账户(db1)
    accountMapper.decrease(from, amount);
    // 2. 增加 to 账户(db2)
    accountMapper.increase(to, amount);
    }
    1. TM 向 TC 申请开启全局事务,TC 返回全局事务 ID(XID);

    2. XID 通过 RPC 上下文传递到所有参与的服务;

    3. 每个本地事务执行时(如 decrease):

      • RM 解析 SQL,生成 before image(更新前数据)after image(更新后数据)
      • undo log(包含 before/after image)与业务 SQL 放入同一本地事务,一同提交;
      • 向 TC 注册分支事务(携带 XID、资源 ID、undo log 等);
    4. 本地事务提交数据库行锁立即释放

  2. 二阶段:提交或回滚

    • 全局提交(所有分支成功)
      1. TC 向所有 RM 发送 “删除 undo log” 指令;
      2. RM 异步删除对应 undo log;
      3. 无数据库操作,性能极高。
    • 全局回滚(任一分支失败)
      1. TC 向所有 RM 发送 “回滚分支” 指令;
      2. RM 根据 undo log 的 before image,自动生成反向 SQL 并执行:
        UPDATE account SET balance = 1000 WHERE id = '1';
      3. 回滚必须幂等(Seata 通过 undo log 状态保证)。

AT 模式的“脏读”问题:一阶段本地提交后,其他事务可读到“中间状态”(如 A 已扣款但 B 未到账)。

解决方案:Seata 提供 全局锁(Global Lock) 机制(需开启),在更新前检查是否有未提交的全局事务。

Undo Log 机制

  1. Undo Log 表结构(需业务库手动创建)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    CREATE TABLE `undo_log` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id` bigint(20) NOT NULL,
    `xid` varchar(100) NOT NULL,
    `context` varchar(128) NOT NULL,
    `rollback_info` longblob NOT NULL,
    `log_status` int(11) NOT NULL,
    `log_created` datetime NOT NULL,
    `log_modified` datetime NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    具体参考:官网

  2. Undo 生成策略

    1. UPDATE:记录修改行的 before/after;
    2. INSERT:记录主键,回滚时 DELETE;
    3. DELETE:记录整行数据,回滚时 INSERT。
    • 不支持 DDL(如 ALTER TABLE);

    • 要求表有主键(用于定位回滚行);

    • SQL 必须可解析(Seata 支持大部分 DML,但复杂 JOIN 可能失败)。

隔离性

  • 写隔离:一阶段本地事务提交前,需要确保先拿到 全局锁 。拿不到不能提交本地事务。且尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

  • 读隔离:在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。必需要求全局的 读已提交 ,可以通过 SELECT FOR UPDATE 语句的代理。该语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回

TCC 模式

TCC(Try-Confirm-Cancel)是一种业务层面的补偿型事务模型,不要求底层数据库支持分布式事务(不是让数据库自动回滚,而是由业务代码主动执行补偿逻辑)。

一般适用于高并发、高性能系统(如电商、金融)。不过需业务逻辑配合设计。

特性 说明
无长时间锁定资源,性能好 Try 阶段仅短暂锁定(如加数据库行锁),Confirm/Cancel 阶段无需锁
高并发 资源预留后即可释放数据库连接,适合高吞吐场景
业务侵入性强 需为每个业务操作开发 Try/Confirm/Cancel 三套逻辑
最终一致性 不保证中间状态一致性(如冻结期间余额不可用),但最终一致
幂等性要求高 网络超时可能导致重复调用,Confirm/Cancel 必须幂等

核心思想

一个大事务拆解成多个小事务

try阶段检查并预留资源,确保在confirm阶段有资源可用,最大程度的确保confirm阶段能够执行成功。

sequenceDiagram
    participant TM as 事务管理器(TC)
    participant ServiceA as 服务A
    participant ServiceB as 服务B

    TM->>ServiceA: Try()
    TM->>ServiceB: Try()
    alt 所有 Try 成功
        TM->>ServiceA: Confirm()
        TM->>ServiceB: Confirm()
        Note right of TM: 全局提交
    else 任一 Try 失败
        TM->>ServiceA: Cancel()
        TM->>ServiceB: Cancel()
        Note right of TM: 全局回滚
    end

将传统事务的“提交/回滚”语义,转化为业务可理解的三步操作

  1. Try:预留资源(如冻结金额、占用库存),检查业务可行性,不真正执行业务(一致性和隔离性)。

    1. 幂等性(可重复执行);
    2. 快速失败(如余额不足立即返回);
    3. 资源隔离(防止被其他请求使用)。
  2. Confirm:真正执行业务(如扣款、出库),在所有参与方 Try 成功后,真正完成业务。。

    1. 必须幂等(因网络问题可能重复调用);
    2. 尽量不失败(理想情况下 Confirm 不应出错);
    3. 无需再校验业务条件(Try 已保证)。
  3. Cancel:释放预留资源(回滚),若任一参与者 Try 失败,或 Confirm 超时,释放已预留资源。。

    1. 必须幂等
    2. 必须能成功执行(即使系统异常也要保证补偿)。
img
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Try: 冻结转出账户 100 元,检查转入账户是否存在
@Compensable(confirmMethod = "transferConfirm", cancelMethod = "transferCancel")
@Transactional
public STATE transferTry(long fromAccountId, long toAccountId, int amount) {
//检查 Tom 账户
try {
if (tom.balance < amount) throw new InsufficientFunds();
tom.frozen += amount; // 冻结资金
tom.balance -= amount;
// 不动 Tracy,仅校验其存在性
verifyTracyExists(toAccountId);
return SUCCESS;
} catch (Exception e) {
return FAIL;
}
}

// Confirm: 解冻并完成转账(实际是将冻结资金“转正”)
@Transactional
public void transferConfirm(long fromAccountId, long toAccountId, int amount) {
//Tom 账户-100元
//tracy 账户+100元
tom.balance += amount; // 入账
// tracy 的冻结资金已扣减,无需操作
state = "confirmed"
}

// Cancel: 解冻资金,恢复原状
@Transactional
public void transferCancel(long fromAccountId, long toAccountId, int amount) {
//解除Tom账户锁定资金
tom.balance += amount;
tom.frozen -= amount;
}

TCC框架是如何保证数据一致性?

img

TCC依赖于一条事务处理记录,在开始TCC事务前标记创建此记录,然后在TCC的每个环节持续更新此记录的状态,这样就可以知道事务执行到那个环节了,当一次执行失败,进行重试时同样根据此数据来确定当前阶段,并判断应该执行什么操作。

挑战与应对

通常需引入 事务日志表 或使用 Seata TCC 模式 自动处理这些问题。

  1. 空回滚(Cancel 执行时 Try 未执行)

    • 原因:网络超时导致 TM 误判 Try 失败,直接发 Cancel。

    • 解决方案:Cancel 前检查是否执行过 Try(如记录事务日志)。

  2. 幂等控制

    为每个事务生成全局唯一 ID(xid),服务端记录已处理的 xid。

  3. 悬挂问题(Try 在 Cancel 之后到达)

    • 原因:Cancel 先执行(因超时),Try 后到(网络延迟)。

    • 解决方案:Try 前检查是否已 Cancel(通过事务状态表)。

应用场景

  1. 电商下单

    • Try:冻结库存、预占优惠券、校验地址
    • Confirm:扣减库存、使用优惠券、生成订单
    • Cancel:释放库存、归还优惠券
  2. 金融转账

    • Try:冻结转出金额,校验转入账户
    • Confirm:完成转账
    • Cancel:解冻金额
  3. 机票/酒店预订

    • Try:预占座位/房态
    • Confirm:出票/确认入住
    • Cancel:释放资源

Saga 模式

一种用于长事务 的分布式事务模式,特别适用于跨多个服务、执行时间较长、无法使用传统 ACID 事务的业务场景(如订单履约、旅行预订、贷款审批等)。

它通过将一个全局事务拆解为一系列本地事务,并为每个本地事务定义对应的补偿操作(Compensation),从而在发生失败时实现回滚(补偿),最终达成最终一致性

  • 适用于长流程、长时间运行的事务。

  • 无锁、高吞吐。

  • 不能保证中间状态隔离性(可能读到“部分完成”状态)。

  • 补偿逻辑复杂,可能无法完全回滚(如发短信已发出)。

核心思想

**“分而治之 + 事后补偿”**机制,

  • 正向操作链:T1 → T2 → T3 …

  • 失败时反向补偿:若 T3 失败,则执行 C2 → C1(补偿 T2、T1)

sequenceDiagram
    participant Coordinator as Saga协调器
    participant Service1 as 服务1
    participant Service2 as 服务2
    participant Service3 as 服务3

    Coordinator->>Service1: 执行 Step1
    Service1-->>Coordinator: 成功
    Coordinator->>Service2: 执行 Step2
    Service2-->>Coordinator: 成功
    Coordinator->>Service3: 执行 Step3
    alt Step3 成功
        Coordinator-->>Coordinator: 全局成功
    else Step3 失败
        Coordinator->>Service2: 补偿 Compensate2
        Service2-->>Coordinator: 回滚成功
        Coordinator->>Service1: 补偿 Compensate1
        Service1-->>Coordinator: 回滚成功
        Note right of Coordinator: 反向补偿已执行步骤
    end
  • 不追求强一致性,允许中间状态短暂不一致;

  • 每个步骤都是一个本地事务(可独立提交);

  • 失败时通过反向操作(补偿)撤销已生效的步骤

  • 无需长时间锁定资源,适合高并发、长时间运行的业务。

两种实现模式

  1. Choreography(事件驱动):各服务通过事件通信,自主决定下一步。

    • 无中心协调者
    • 每个服务监听事件并自主决定下一步
    • 通过事件链驱动流程,失败时发布补偿事件
       sequenceDiagram
        participant Order as 订单服务
        participant Inventory as 库存服务
        participant Payment as 支付服务
    
        Order->>Inventory: 扣减库存事件
        Inventory->>Payment: 发起支付事件
        Payment->>Order: 付款成功事件
        Order->>Order: 确认订单
    
        Note right of Payment: 若支付失败
        Payment->>Inventory: 取消库存扣减事件
        Inventory->>Order: 取消订单事件

    优点:服务解耦,无单点瓶颈。
    缺点:流程分散,难以追踪;补偿逻辑散布各处。

  2. Orchestration(编排):由中心协调器控制流程和补偿。

    • 引入 Saga Coordinator(协调器) 控制整个流程;
    • 协调器顺序调用各服务,并记录执行状态;
    • 失败时显式调用补偿接口
       sequenceDiagram
        participant Coordinator as Saga协调器
        participant Order as 订单服务
        participant Inventory as 库存服务
        participant Payment as 支付服务
    
        Coordinator->>Order: 提交订单
        Coordinator->>Inventory: 扣减库存
        Coordinator->>Payment: 发起扣款
    
        alt 支付失败
            Coordinator->>Inventory: 取消库存扣减 (补偿)
            Coordinator->>Order: 取消订单 (补偿)
        end

    优点:流程集中,易监控、重试、调试。
    缺点:协调器成为中心依赖(但可通过持久化状态保证可靠性)。

补偿机制

正向流程(Forward Flow)

  1. 执行步骤 1 → 提交本地事务;

  2. 执行步骤 2 → 提交本地事务;

  3. 所有步骤成功 → 全局成功。

逆向补偿(Compensation)

  • 若步骤 N 失败,则反向执行 N-1, N-2, …, 1 的补偿操作

  • 补偿操作必须幂等(可能被重复调用);

  • 补偿不能保证 100% 回滚(如“发送短信”无法撤回)。

    操作 补偿方式
    扣减库存 释放库存
    冻结金额 解冻金额
    创建订单 标记订单为“已取消”
    发送邮件 无法撤销 → 改为“发送取消通知”

Saga 无法保证隔离性(Isolation):在事务完成前,其他事务可能读到“中间状态”(如订单已创建但未支付)。

解决方案:语义锁(如订单状态机)、乐观锁业务规则校验

应用场景

通常为符合的场景:流程长、步骤多、能补偿、可接收最终一致

  1. 旅行预订

    • 预订航班 → 预订酒店 → 租车
    • 若租车失败 → 取消酒店、取消航班
  2. 电商订单履约

    • 创建订单 → 锁库存 → 扣款 → 发货
    • 若扣款失败 → 释放库存、取消订单
  3. 贷款审批

    • 提交申请 → 征信查询 → 风控审核 → 放款
    • 任一环节失败 → 撤销已执行步骤

消息模式

基于消息队列的最终一致性(本地消息表 / 消息事务)。

利用消息中间件(如 Kafka、RocketMQ)保证异步操作的最终一致性。

  1. 本地消息表

    • 在业务数据库中建消息表,与业务操作在同一事务中写入。
    • 后台任务轮询消息表,发送消息到 MQ。
  2. 事务消息(如 RocketMQ 的 Half Message)

    • 发送“半消息” → 执行本地事务 → 根据结果提交或回滚消息。
    • 消费者收到消息后执行下游操作。

优点

  • 解耦、高性能、高可用。

  • 适合异步、最终一致性场景。

缺点

  • 不支持强一致性。

  • 需处理消息重复、丢失等问题。

Seata 框架

Seata 是一款开源的分布式事务解决方案,致力于提供高性能简单易用的分布式事务服务。

Seata 支持多种模式:

  • AT 模式(自动补偿):基于本地事务+全局锁,对业务无侵入。

  • TCC 模式:如上所述。

  • Saga 模式:长事务编排。

  • XA 模式:基于标准 XA 协议(类似 2PC)。

AT 模式

AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案

Seata 在内部做了对数据库操作的代理层,使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。

两阶段提交协议的演变

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源(全局锁)。

    1. 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
    2. 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
    3. 执行业务 SQL:更新这条记录。
    4. 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
    5. 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中
    6. 提交前向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁
    7. 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
    8. 将本地事务提交的结果上报给 TC。
  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 通过一阶段的回滚日志进行反向补偿。

    提交

    1. 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
    2. 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

    回滚

    1. 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。

    2. 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。

    3. 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。

    4. 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句。

    5. 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

1
2
3
4
5
@GlobalTransactional
public void purchase(String userId, String commodityCode, int count, int money) {
jdbcTemplateA.update("update stock_tbl set count = count - ? where commodity_code = ?", new Object[] {count, commodityCode});
jdbcTemplateB.update("update account_tbl set money = money - ? where user_id = ?", new Object[] {money, userId});
}

注意事项:

  • 必须要在业务数据库中添加undo_log表,可参考 官网

  • 必须在所有涉及事务的服务中都引入seata依赖和相关配置,且所有服务的数据库都需要添加undo_log表

  • 事务分组的映射关系,seata 的配置在各个服务之间需保持一致,尤其是 vgroup-mapping

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    # seata 服务器的配置
    seata:
    registry:
    type: nacos
    nacos:
    application: seata-server
    server-addr: localhost:8848
    group: SEATA_GROUP
    cluster: order-seata # 必须与应用中 seata.service.vgroup-mapping 下子节点的值一致
    username:
    password:

    # 应用中的配置
    seata:
    enabled: true
    # Seata 应用编号,默认为 ${spring.application.name}
    application-id: ${spring.application.name}
    # Seata 事务组编号,用于 TC 集群名,默认为 default_tx_group
    tx-service-group: order_tx_group
    service:
    # 虚拟组和分组的映射
    vgroup-mapping:
    # 该节点名称 order_tx_group 必须与上面节点tx-service-group的值保持一致 (若nacos上没有对应名称的配置,就需要手动添加)
    order_tx_group: order-seata
    group-list:
    # 分组和 Seata 服务的映射
    # 该节点名 order-seata 必须与 配置文件中 seata.registry.nacos.cluster的值相对应(注册至nacos注册中心的集群名,默认default)
    # 该节点名 order-seata 必须与上面order_tx_group节点的值相对应
    order-seata: 127.0.0.1:8091
    registry:
    type: nacos
    nacos:
    application: seata-server
    server-addr: 127.0.0.1:8848
    cluster: order-seata # 必须与上面 seata.service.group-list 的子节点名相对应
    username:
    password:
    group: SEATA_GROUP
  • 注解 @GlobalTransactional 为代理模式,必须符合动态代理,否则无效

  • @GlobalTransactional 之前的所有方法,不能处理异常,包括 @SentinelResource 异常熔断,@SneakyThrows捕获,或者通过@FeignClient 调用执行的 熔断降级处理,各个服务之间更不能 catch 其他任何异常

TCC 模式

  • 通过 @TwoPhaseBusinessAction 注解定义 Try/Confirm/Cancel;

  • 自动处理幂等、空回滚、悬挂等问题;

  • 提供事务日志(事务状态存储)。

1
2
3
4
5
6
7
8
9
10
11
12
@TwoPhaseBusinessAction(name = "transfer", commitMethod = "confirm", rollbackMethod = "cancel")
public boolean prepare(BusinessActionContext context, String userId, double amount) {
// Try 逻辑
}

public boolean confirm(BusinessActionContext context) {
// Confirm 逻辑
}

public boolean cancel(BusinessActionContext context) {
// Cancel 逻辑
}

Saga 模式

通过状态机定义 Saga 流程(JSON 配置)。

状态机定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"StateLanguageVersion": "1.0",
"StartState": "CreateOrder",
"States": {
"CreateOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "create",
"CompensateState": "CancelOrder",
"NextState": "ReserveInventory"
},
"ReserveInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryService",
"ServiceMethod": "reserve",
"CompensateState": "ReleaseInventory",
"End": true
}
}
}

使用案例

MySQL 的 XA 事务

MySQL 从 5.0 开始支持 XA 事务。XA 事务通过以下标准命令操作:

  • 存储引擎为 InnoDB(MyISAM 不支持 XA)。

  • innodb_support_xa(MySQL 5.7+ 默认启用,8.0 已移除此参数,并始终支持)。

  • 应用程序需扮演 事务管理器(TM) 角色,协调多个资源。

示例场景:假设一个分布式事务涉及 两个数据库实例(或同一实例中的两个表),需同时更新:

  • account_db:用户账户余额

  • order_db:订单状态

执行流程:

  1. 开启 XA 事务(为每个 RM 分配全局事务 ID)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    -- 为第一个资源(如 account 表)开启 XA 事务
    XA START 'txn_id_123', 'account_branch';

    -- 执行业务操作
    UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;

    -- 结束第一阶段操作
    XA END 'txn_id_123', 'account_branch';

    -- 为第二个资源(如 order 表)开启同一全局事务
    XA START 'txn_id_123', 'order_branch';

    -- 执行业务操作
    UPDATE orders SET status = 'paid' WHERE order_id = 1001;

    XA END 'txn_id_123', 'order_branch';
  2. 准备阶段(PREPARE)

    1
    2
    3
    -- 对两个分支分别 prepare
    XA PREPARE 'txn_id_123', 'account_branch';
    XA PREPARE 'txn_id_123', 'order_branch';
    • 写入 undo logredo log

    • 将事务状态标记为 PREPARED

    • 持久化到磁盘(保证崩溃后可恢复)

  3. 提交或回滚(根据协调者决策)

    1
    2
    3
    4
    5
    6
    7
    -- 全部 prepare 成功 → 提交
    XA COMMIT 'txn_id_123', 'account_branch';
    XA COMMIT 'txn_id_123', 'order_branch';

    -- 若任一分支失败 → 回滚
    -- XA ROLLBACK 'txn_id_123', 'account_branch';
    -- XA ROLLBACK 'txn_id_123', 'order_branch';
  4. 查看事务状态

    1
    2
    3
    4
    5
    6
    7
    8
    XA RECOVER;  -- 显示所有处于 PREPARED 状态的 XA 事务

    -- 输出
    +----------+--------------+--------------+------------+
    | formatID | gtrid_length | bqual_length | data |
    +----------+--------------+--------------+------------+
    | 1 | 11 | 14 | txn_id_123 |
    +----------+--------------+--------------+------------+
    • XA 事务会长时间持有行锁,影响并发性能。

    • 不支持 DDL 语句(如 ALTER TABLE)。

    • 应用层需实现协调逻辑(或使用 Seata、Atomikos 等 TM 框架)。

MySQL 崩溃恢复

在任意节点崩溃后,如何保证事务的原子性?这依赖于 可靠的日志

  • InnoDB 的 XA 恢复

    • 启动时自动扫描 ibdata1 中的 undo log,找出 PREPARED 事务。
    • 若应用连接断开,可通过 XA RECOVER + 手动 COMMIT/ROLLBACK 处理。
  • 日志双写保障:redo log + binlog(在 MySQL 半同步复制中用于崩溃一致性)。

本地事务日志 + 全局状态记录的模式实现:

  1. 参与者(RM)的日志

    每个参与者在执行 2PC 时,必须记录以下日志到持久化存储(如磁盘):

    阶段 日志内容 作用
    执行本地事务后 UNDO LOG(回滚用) + REDO LOG(重做用) 支持崩溃后回滚或提交
    收到 PREPARE 写入 <TransactionID, PREPARED> 到事务日志 标记事务已进入 2PC
    收到 COMMIT 写入 <TransactionID, COMMITTED>,并释放资源 完成提交
    收到 ABORT 写入 <TransactionID, ABORTED>,执行 undo 回滚
  2. 协调者(TM)的日志

    协调者也需记录全局事务状态,状态包括:INIT, PREPARING, PREPARED, COMMITTING, COMMITTED, ABORTED

崩溃恢复的几种情况

  • 情况 1:参与者崩溃后重启

    读取本地事务日志时发现存在 PREPARED ,且无最终状态的事务时:

    • 向协调者查询该事务的最终决策

    • 若协调者不可用,则阻塞等待

  • 情况 2:协调者崩溃后重启

    读取自己的日志,对处于 PREPARED 状态的事务:

    • 向所有参与者查询状态;

    • 若所有参与者都 PREPARED,则继续发送 COMMIT

    • 若有任一参与者未 PREPARED,则发送 ABORT

  • 情况 3:协调者永久宕机(无恢复)

    参与者将永远阻塞PREPARED 状态(需人工干预或超时机制,但是2PC 本身无超时)。