Seata分布式事务
开篇:微服务拆分后,事务怎么办?
假设你要从工商银行转 1000 块钱到建设银行。工商银行扣了你 1000,但建设银行那边因为系统故障没有加上。这笔钱就"人间蒸发"了。
在单体应用中,扣款和加款在同一个数据库里,用一个本地事务就能保证要么都成功、要么都回滚。但微服务拆分后,订单服务、库存服务、账户服务各用各的数据库,一个本地事务管不了跨数据库的操作。
这就是 分布式事务 要解决的问题:如何让分布在不同服务、不同数据库上的多个操作,像一个事务一样,要么全部成功,要么全部回滚。
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,目前是这个领域最主流的框架之一。
一、为什么本地事务不够用?
先看一个典型的下单场景:
public void createOrder() {
orderService.create(); // 创建订单(订单库)
stockService.decrease(); // 扣减库存(库存库)
accountService.deduct(); // 扣减余额(账户库)
}这三个操作分别在三个数据库上执行。@Transactional 注解只能管住一个数据库的事务。如果订单创建成功了,库存扣减也成功了,但账户扣款失败了——前两个操作已经提交,没法回滚了。
这就是分布式事务问题的本质:多个独立的本地事务之间缺乏协调机制。
二、分布式事务的理论基础
2.1 CAP 定理
分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三者最多只能同时满足两个。由于网络分区不可避免,实际中只能在 C 和 A 之间做取舍。选 C 就是 CP 系统(如 ZooKeeper),选 A 就是 AP 系统(如 Eureka)。
2.2 BASE 理论
BASE 是对 CAP 中 AP 方向的延伸:
- Basically Available(基本可用):允许部分功能降级。
- Soft State(软状态):允许数据存在中间状态。
- Eventually Consistent(最终一致性):经过一段时间后,数据最终达到一致。
大多数分布式事务方案都是基于 BASE 理论,追求最终一致性。只有 XA/2PC 这类方案追求强一致性。
三、2PC 与 3PC 协议
3.1 两阶段提交(2PC)
两阶段提交是最经典的分布式事务协议。用婚礼来类比非常形象:
牧师先分别问新郎和新娘"你愿意吗?"(Prepare 阶段),两个人都说"I do"之后,牧师才宣布"你们现在是合法夫妻了"(Commit 阶段)。如果有一个人说"我不愿意",婚礼就取消。
第一阶段(Prepare):
- 协调者向所有参与者发送 Prepare 请求。
- 参与者执行事务操作(写日志、加锁),但不提交。
- 参与者返回 Yes(准备好了)或 No(有问题)。
- 此时资源处于锁定状态,等待协调者最终指令。
第二阶段(Commit/Rollback):
- 如果所有参与者都返回 Yes,协调者发送 Commit,参与者正式提交并释放锁。
- 如果有任何一个返回 No 或超时,协调者发送 Rollback,所有参与者回滚并释放锁。
2PC 的三个致命问题:
同步阻塞:参与者锁住资源等待协调者指令,期间其他事务无法访问这些资源。如果协调者迟迟不发指令,参与者就一直阻塞。
单点故障:协调者是整个流程的中枢。如果协调者在第二阶段挂了,参与者就永远不知道该提交还是回滚。
数据不一致:第二阶段如果因为网络问题,Commit 通知只发给了部分参与者,就会出现有的提交了、有的没提交的情况。
最严重的场景:协调者和某个已执行操作的参与者同时挂掉。新选出来的协调者可以询问其他存活的参与者来决定提交或回滚,但挂掉的那个参与者恢复后,它之前的操作可能和新协调者的决策不一致,导致数据永久分裂。
3.2 三阶段提交(3PC)
3PC 在 2PC 的基础上做了两个改进:多了一个 CanCommit 阶段和引入了超时机制。
CanCommit 阶段:协调者先问参与者"你能执行这个事务吗?"参与者只做检查不做实际操作。如果有人说不行,立刻中止,避免白白执行事务操作。
PreCommit 阶段:如果所有人都说可以,再执行预提交操作(执行事务但不提交)。这一步相当于 2PC 的第一阶段。
DoCommit 阶段:如果预提交都成功,通知所有参与者正式提交。
超时机制是 3PC 的关键改进:如果参与者在 DoCommit 阶段超时未收到协调者指令,会自动提交事务。这解决了协调者挂掉导致参与者永久阻塞的问题。
但超时自动提交也带来了新问题:如果协调者实际发的是 Rollback,但因为网络延迟没到达,参与者超时后自动提交了,还是会出现数据不一致。所以 3PC 并没有根本解决一致性问题,且多了一轮网络通信,实际使用并不多。
四、Seata 架构
Seata 的设计哲学是:一个分布式事务由若干个本地事务组成。它定义了三个角色:
- TC(Transaction Coordinator):事务协调器。独立部署的服务端进程,维护全局事务和分支事务的状态,驱动提交或回滚。可以集群部署保证高可用。
- TM(Transaction Manager):事务管理器。嵌入在应用中的客户端模块,负责开启和结束全局事务。谁先发起调用,谁就是 TM。
- RM(Resource Manager):资源管理器。嵌入在应用中的客户端模块,负责管理分支事务,向 TC 注册分支并汇报状态。
一次分布式事务的完整流程:
- TM 向 TC 申请开启全局事务,TC 生成全局唯一的 XID。
- TM 通过 RPC 调用各个 RM,XID 在调用链中传播,将多个微服务的子事务关联在一起。
- 各个 RM 向 TC 注册分支事务,通过 XID 关联到全局事务。
- 各 RM 执行本地事务操作,并向 TC 汇报执行结果。
- 调用链结束后,TM 根据结果告诉 TC 要提交(全部成功)还是回滚(有失败)。
- TC 协调所有 RM 执行提交或回滚。
五、四种事务模式详解
5.1 AT 模式(自动补偿)
AT 模式是 Seata 最核心、用得最多的模式。它对业务代码零侵入,只需要把 @Transactional 换成 @GlobalTransactional 就行。
核心原理:通过代理数据源(DataSource Proxy),在执行 SQL 前后自动记录数据快照(undo log),需要回滚时根据快照反向补偿。
一阶段详细流程
- RM 解析 SQL,得到 SQL 类型、表名、where 条件等信息。
- 根据解析结果查询变更前的数据,保存为 before image(前置镜像)。
- 执行 SQL 进行数据变更。
- 查询变更后的数据,保存为 after image(后置镜像)。
- 将 before/after image 组成 undo log 记录。
- 向 TC 注册分支事务,申请该行数据的全局锁。
- 将业务数据变更和 undo log 在同一个本地事务中提交。
- 向 TC 汇报本地事务执行结果。
这里第 7 步是关键:通过本地事务的 ACID 特性,保证业务数据和回滚日志的原子性。
二阶段 - 全局提交
如果所有分支都成功,TC 异步通知各 RM 删除 undo log 并释放全局锁。因为一阶段已经把数据提交了,所以二阶段的提交操作非常快,几乎是瞬间完成。
二阶段 - 全局回滚
如果有分支失败,TC 通知所有 RM 进行回滚:
- 根据 XID 和 Branch ID 找到对应的 undo log。
- 将当前数据与 after image 比对:如果一致,说明没有其他事务修改过,可以安全回滚;如果不一致,检查是否与 before image 一致(可能已经回滚或未提交成功),如果都不一致则说明有脏写,需要人工介入。
- 根据 before image 生成反向 SQL 执行回滚。
- 删除 undo log,释放全局锁。
// 使用方式极其简单
@GlobalTransactional
public void purchase(String userId, String commodityCode, int count) {
orderService.create(userId, commodityCode, count);
stockService.decrease(commodityCode, count);
accountService.deduct(userId, count * 10);
}AT 模式的全局锁机制
AT 模式通过全局锁来防止多个全局事务同时修改同一行数据。全局锁的粒度是表名 + 主键值,由 TC 统一管理。
注意区分全局锁和本地锁:
- 本地锁:数据库自身的行锁,一阶段提交后就释放了。
- 全局锁:TC 维护的逻辑锁,直到全局事务结束才释放。
这意味着一阶段后,虽然本地锁释放了,但全局锁还在。其他本地事务可以读到数据,但另一个全局事务如果要修改同一行,需要等全局锁释放。
AT 模式的脏读问题
AT 模式存在全局事务层面的"逻辑脏读"。来看一个具体场景:
@GlobalTransactional
public boolean buy() {
inventoryService.decreaseInventory(); // 步骤1:库存扣减
orderService.createOrder(); // 步骤2:创建订单
}步骤 1 执行成功后,库存服务的本地事务已经提交。此时如果有另一个线程查询库存,它会读到扣减后的值。但如果步骤 2 失败了,全局事务会回滚,库存会被恢复。那个读到扣减后值的线程就读到了一个"即将被撤销"的数据。
这不是 MySQL 事务隔离级别里说的脏读(读到未提交的数据),而是分布式事务特有的"逻辑脏读"(读到已提交但即将被回滚的数据)。目前没有完美的解决方案,如果不能接受就用 TCC 或 XA。
undo log 的数据格式
{
"branchId": 641789253,
"xid": "xid:xxx",
"undoItems": [{
"sqlType": "UPDATE",
"beforeImage": {
"tableName": "product",
"rows": [{"fields": [
{"name": "id", "type": 4, "value": 1},
{"name": "name", "type": 12, "value": "TXC"}
]}]
},
"afterImage": {
"tableName": "product",
"rows": [{"fields": [
{"name": "id", "type": 4, "value": 1},
{"name": "name", "type": 12, "value": "GTS"}
]}]
}
}]
}5.2 TCC 模式(手动补偿)
TCC 是 Try-Confirm-Cancel 的缩写,是一种对业务代码有侵入的分布式事务方案。开发者需要手动实现三个操作:
- Try:预留资源。比如冻结账户中的 100 元(不是直接扣,是从"可用余额"转到"冻结金额")。
- Confirm:确认操作。Try 全部成功后,将冻结金额正式扣除(从"冻结金额"中扣掉)。
- Cancel:取消操作。释放 Try 阶段预留的资源(从"冻结金额"转回"可用余额")。
用一个具体的转账例子:A 要分别转 100 元到 B 和 200 元到 C。
- Try:冻结 A 的 300 元(可用余额 -300,冻结金额 +300)。
- Confirm(全部 Try 成功):A 冻结金额 -300,B 余额 +100,C 余额 +200。
- Cancel(某个 Try 失败):A 冻结金额 -300,可用余额 +300(解冻)。
TCC 的优点是不依赖数据库的特性,可以跨 Redis、MySQL、ES 等异构数据源。缺点是代码侵入性大,每个操作都要写 Try/Confirm/Cancel 三个接口。
TCC 的三个经典问题
空回滚:在全局事务中,参与者 A 的 Try 因网络问题没有执行成功,但全局事务超时触发了 Cancel。A 收到 Cancel 请求时其实什么都没 Try 过——这就是空回滚。需要在 Cancel 中做判断:检查事务控制表是否有 Try 记录,没有就直接跳过。
幂等问题:Confirm/Cancel 执行后因为网络超时没有及时收到 TC 的确认,TC 会重试,导致 Confirm/Cancel 被调用多次。解决方案:在事务控制表中记录操作状态(tried=0, committed=1, rollbacked=2),重复调用时检查状态直接返回。
悬挂问题:Try 因网络拥堵超时,全局事务触发 Cancel 并完成回滚。之后拥堵的 Try 请求到达并执行了资源预留——这笔资源就再也没人来释放了。解决方案:Cancel 时如果发现没有 Try 记录,插入一条 suspended 状态的记录;Try 执行前检查,发现 suspended 就拒绝执行。
三个问题统一用一张 分布式事务控制表 解决:
CREATE TABLE distribute_transaction (
tx_id VARCHAR(128) PRIMARY KEY,
state INT COMMENT '0:try, 1:confirm, 2:cancel, 3:suspended'
);TCC 是强一致性还是最终一致性?
TCC 是最终一致性。Try 阶段和 Confirm 阶段之间存在时间间隔,在这个间隔内数据处于"中间状态"(如"资金已冻结但未扣款"),用户可能看到这种中间状态。这违反了强一致性的定义。
Confirm/Cancel 失败了怎么办?
最常用的策略是重试。因为 Try 已经预留了资源,Confirm 大概率会成功。如果多次重试仍然失败,则记录日志并发送报警,由人工介入处理。Cancel 失败也类似,重试 + 报警 + 人工兜底。
TCC 和 2PC 的区别
最大区别在于事务的粒度:
- TCC 把一次业务操作拆成三个独立事务(Try/Confirm/Cancel 各自独立开启、独立提交),不长时间占用资源,性能好。
- 2PC 是一个事务分两阶段,Prepare 阶段不提交,资源一直锁到 Commit,阻塞时间长,性能差。
所以 2PC 是强一致性,TCC 是最终一致性。在高并发场景下,TCC 的性能远优于 2PC。
5.3 SAGA 模式(长事务补偿)
SAGA 模式将一个长事务拆成多个短事务(T1, T2, T3...),每个短事务都有一个对应的补偿操作(C1, C2, C3...)。正向依次执行 T1 -> T2 -> T3,如果 T3 失败,反向执行补偿 C2 -> C1。
适合的场景:
- 业务流程长、参与方多。
- 包含外部接口调用(如调微信支付),无法做 TCC 的资源预留。
- 遗留系统接入,无法改造成 TCC 的三个接口。
SAGA 没有 Try 阶段的资源预留,也没有全局锁,隔离性最弱。并发场景下可能出现脏写。如果业务对隔离性有要求,不建议使用。
5.4 XA 模式(强一致性)
XA 是经典的两阶段提交协议在数据库层面的标准实现。Seata 的 XA 模式直接利用数据库对 XA 协议的原生支持:
- 一阶段执行 SQL 后调用
XA PREPARE,资源被锁定但不提交。 - 二阶段根据协调结果调用
XA COMMIT或XA ROLLBACK。
优点:强一致性、完全隔离、对业务无侵入,支持多种数据库和多种语言。
缺点:性能差——资源一直锁到二阶段结束,在高并发场景下会成为严重瓶颈。
适合金融、支付等对数据一致性要求极高的场景。
六、AT vs XA 深入对比
这是面试高频题,需要理解透彻。
| 维度 | AT | XA |
|---|---|---|
| 一阶段 | 执行 SQL 并提交本地事务,记录 undo log | 执行 SQL 但不提交(XA PREPARE) |
| 二阶段 - 提交 | 异步删除 undo log(非常快) | XA COMMIT(正式提交并释放锁) |
| 二阶段 - 回滚 | 根据 undo log 反向补偿 | XA ROLLBACK(直接回滚) |
| 资源锁定时间 | 短(一阶段即释放本地锁) | 长(锁到二阶段结束) |
| 性能 | 高 | 低 |
| 一致性 | 最终一致性(有脏读窗口) | 强一致性 |
| 适用场景 | 高吞吐量的常规场景 | 强一致性要求的金融场景 |
AT 的核心优势:一阶段就提交了本地事务,不长时间锁定数据库资源,吞吐量高。代价是在提交后、全局事务结束前存在一个"脏读窗口"。
七、模式选择指南
| 维度 | AT | TCC | SAGA | XA |
|---|---|---|---|---|
| 一致性 | 最终一致 | 最终一致 | 最终一致 | 强一致 |
| 隔离性 | 基于全局锁 | 基于资源预留 | 无隔离 | 完全隔离 |
| 代码侵入性 | 无 | 高(需写三个接口) | 高(需写补偿逻辑) | 无 |
| 性能 | 高 | 非常高 | 非常高 | 低 |
| 适用场景 | 关系型数据库的常规场景 | 高性能、非关系型数据库 | 长事务、外部接口 | 金融级强一致性 |
选择建议:
- 大多数场景用 AT 模式——零侵入,性能好,够用。
- 涉及 Redis/ES 等非关系型存储,用 TCC 模式——可以跨异构数据源。
- 调用外部系统(支付、物流),用 SAGA 模式——外部系统没法做 TCC 改造。
- 金融级强一致性要求,用 XA 模式——牺牲性能换绝对一致。
八、基于 MQ 的分布式事务
除了 Seata,还有一类基于消息队列的分布式事务方案,不需要引入额外的事务中间件。
8.1 本地消息表
核心思路:将"发消息"和本地业务操作放在同一个数据库事务中。
- 在本地数据库建一张消息表。
- 业务操作和消息记录在同一个事务中提交,保证原子性。
- 定时任务扫描消息表,将未发送成功的消息投递到 MQ。
- 消费者处理消息后回调更新消息状态。
- 如果投递失败,定时任务会不断重试,直到成功。
优点是方案简单、可靠性高。缺点包括:
- 需要额外建表和定时任务。
- 定时扫表有延迟,消息堆积时扫表会变慢。
- 不适合多参与方的场景(消息表一条记录只能对应一个下游)。
- 回滚困难——适合"上游成功则下游必须成功"的场景,不适合需要全局回滚的场景。
8.2 事务消息(RocketMQ)
RocketMQ 原生支持事务消息,将一次消息发送拆成两步:
- 发送 Half 消息(暂存在 MQ,消费者不可见)。
- 执行本地事务。
- 根据本地事务结果发送 Commit(消息变为可见)或 Rollback(删除消息)。
- 如果长时间未收到确认,MQ 会主动回查本地事务状态。
相比本地消息表,事务消息不需要建额外的表和定时任务,但需要 MQ 支持事务消息特性。目前 RocketMQ 对事务消息的支持最成熟。
为什么不能直接发普通消息?因为"执行本地事务"和"发送 MQ 消息"这两个操作无法保证原子性。可能本地事务提交了但消息发送失败,也可能消息发送成功但本地事务因网络超时误判为失败而回滚。事务消息通过两阶段机制 + 回查解决了这个问题。
8.3 最大努力通知
最简单的方案:发消息后不保证一定送达,只做有限次重试。重试多次仍然失败就放弃,后续通过对账来兜底。
适合非核心链路,如下单后发送邮件通知、开通后发送欢迎短信等。消息丢了也不会影响核心业务。
三种方案对比
| 方案 | 可靠性 | 复杂度 | 适用场景 |
|---|---|---|---|
| 本地消息表 | 高 | 中 | 上游成功则下游必须成功 |
| 事务消息 | 高 | 中 | MQ 支持事务消息时的首选 |
| 最大努力通知 | 低 | 低 | 非核心链路 |
本地消息表和事务消息可以互相替代。如果 MQ 支持事务消息,优先用事务消息;如果有持久化或对账需求,用本地消息表。
九、分布式事务方案全景
| 方案 | 一致性 | 适用场景 | 代表实现 |
|---|---|---|---|
| XA(2PC/3PC) | 强一致 | 数据库级强一致性要求 | Seata XA, MySQL XA |
| AT | 最终一致 | 关系型数据库的常规场景 | Seata AT |
| TCC | 最终一致 | 高性能、异构数据源 | Seata TCC, Hmily |
| SAGA | 最终一致 | 长事务、外部接口 | Seata Saga |
| 本地消息表 | 最终一致 | 单向异步事务 | 自研 |
| 事务消息 | 最终一致 | 异步场景 | RocketMQ 事务消息 |
| 最大努力通知 | 弱一致 | 非核心链路 | 自研 |
选择时主要考虑三个因素:一致性要求、性能要求、实现复杂度。能用最终一致性的就不要用强一致性,性能差距是数量级的。
十、柔性事务的基础条件
在实现最终一致性的分布式事务方案时,以下三个基础能力不可或缺:
可查询操作
在分布式事务执行过程中,如果某个步骤出错,需要知道其他操作的处理情况。这就要求每个服务都提供查询接口,能通过全局唯一标识(如订单号、流水号)查询操作的执行状态。
以一个支付订单处理为例:
public void completeOrder() {
orderDao.update(); // 本地更新订单状态
accountService.update(); // 调用资金账户服务加款
pointService.update(); // 调用积分服务增加积分
merchantNotifyService.notify(); // 通知商户支付结果
}当积分服务调用失败时,需要查询资金账户服务的状态来决定后续操作,这就依赖于资金账户服务提供可查询的接口。
幂等操作
幂等性是指同一个操作执行多次和执行一次的效果相同。数学表示就是 f(f(x)) = f(x)。
在分布式事务中,由于网络不可靠,重试是非常常见的操作。如果一个接口不保证幂等,重试就可能导致数据错乱——比如"扣减库存"执行了两次,库存就多扣了。
常见的幂等实现方式:
- 用唯一请求 ID 做去重(最常用)。
- 数据库层面用乐观锁(版本号)。
- 状态机控制,已处理的状态不再重复处理。
可补偿操作
要想实现回滚,每个操作都需要有对应的反向操作。比如"给积分账户增加积分"的补偿操作就是"给积分账户扣减积分"。补偿操作同样需要满足幂等性。
这三个条件——可查询、幂等、可补偿——是所有柔性事务方案的基石。不管是 TCC、SAGA 还是基于消息的方案,都离不开这三个基础能力。在设计微服务接口时,应当把这些能力作为标准规范来遵守。
十一、常见面试题精选
Q1:Seata 的 AT 模式的实现原理是什么?
AT 模式基于两阶段提交。一阶段通过代理数据源,在执行 SQL 前后记录 before/after image 作为 undo log,然后和业务数据一起提交本地事务并释放本地锁。二阶段提交时异步删除 undo log;回滚时根据 undo log 生成反向 SQL 进行补偿。核心优势是一阶段就提交,不长时间锁资源。
Q2:AT 模式会不会出现脏读?
会出现全局事务层面的"逻辑脏读"。因为一阶段就提交了本地事务,其他事务可以读到已提交但可能被全局回滚的数据。这不是 MySQL 的本地事务脏读,而是分布式事务特有的问题。如果不能接受就用 TCC 或 XA。
Q3:TCC 的空回滚和悬挂是什么?如何解决?
空回滚:Try 没执行就收到 Cancel。悬挂:Cancel 先于 Try 执行导致 Try 预留的资源无人释放。两者都通过引入分布式事务控制表解决:Cancel 时检查有无 Try 记录(空回滚),并插入防悬挂标记;Try 时检查标记是否已存在(悬挂)。
Q4:Seata 四种模式各自适合什么场景?
AT 适合关系型数据库的常规场景(最常用);TCC 适合高性能或异构数据源场景;SAGA 适合长事务和外部接口调用;XA 适合强一致性要求的金融场景。
Q5:AT 和 XA 的核心区别是什么?
核心在一阶段的处理方式:AT 一阶段提交本地事务释放本地锁,性能高但有脏读窗口;XA 一阶段只 PREPARE 不提交,资源锁到二阶段结束,性能差但强一致。
Q6:TCC 和 2PC 有什么区别?
最大区别在事务粒度。TCC 把一次业务操作拆成三个独立的事务(Try/Confirm/Cancel 各自独立提交),不长时间占用资源,性能高,但是最终一致性。2PC 是一个事务分两个阶段,Prepare 阶段不提交,资源一直锁到 Commit/Rollback,性能差,但是强一致性。
Q7:什么是事务消息?和普通消息有什么区别?
事务消息将一次消息发送拆成两步:先发 Half 消息(消费者不可见),再执行本地事务,最后根据本地事务结果 Commit 或 Rollback 消息。MQ 还提供回查机制,确保消息状态最终确定。普通消息无法保证"本地事务"和"发消息"的原子性。
十二、如何选择分布式事务方案?
选择分布式事务方案时,需要综合考虑以下几个因素:
一致性要求:核心链路(如下单扣库存)需要较强的一致性保证,可以用 Seata AT 或 TCC。非核心链路(如发送通知邮件)用最大努力通知即可。金融场景(资金操作)考虑 XA。
性能要求:根据 CAP 理论,一致性和可用性不可兼得。2PC/XA 的性能最差(资源长时间锁定),TCC/AT 性能较好(资源锁定时间短),基于消息的方案性能最好(完全异步)。
实现成本:TCC 和 2PC 的实现成本最高,代码侵入性也大。AT 模式零侵入但只支持关系型数据库。基于消息的方案需要消息中间件的支持。
数据规模:基于消息的方案在数据量特别大时可能出现消息堆积,导致一致性保障不及时。Seata 方案在这方面表现更好。
参与方数量:本地消息表不适合多参与方场景(一条消息记录只能对应一个下游)。如果事务涉及多个参与方,建议使用 Seata 或事务消息。
简单总结:能异步的就异步,能最终一致的就不要强一致,能用框架的就不要自己造轮子。
以下是一个简化的决策流程:
- 是否需要强一致性? -> 是 -> XA 模式
- 是否涉及非关系型数据源? -> 是 -> TCC 模式
- 是否涉及外部系统调用? -> 是 -> SAGA 模式
- 是关系型数据库的常规场景? -> 是 -> AT 模式
- 是单向异步(上游成功下游必须成功)? -> 是 -> 本地消息表 / 事务消息
- 是非核心链路? -> 是 -> 最大努力通知
小结
分布式事务是微服务架构中绑定遇到的难题。核心矛盾是一致性和性能的取舍:
- 强一致性:XA(2PC/3PC),代价是资源锁定和性能下降。
- 最终一致性:AT、TCC、SAGA、事务消息、本地消息表,各有侧重。
- 弱一致性:最大努力通知,简单但可能丢消息。
Seata 的价值在于用统一的 TC/TM/RM 架构封装了多种事务模式,让开发者根据场景灵活选择。实际项目中,大多数场景用 AT 模式就够了——一个 @GlobalTransactional 注解搞定。只有特殊需求才需要考虑 TCC 或 XA。
最后记住一个原则:能用最终一致性的就不要用强一致性,性能差距是数量级的。