序言
在单体架构时代,我们依赖数据库原生事务和 Spring 的 **@Transactional**注解就能轻松解决所有数据一致性问题。但随着微服务架构的普及,系统被拆分为多个独立部署的服务,每个服务拥有自己的数据库,传统的本地事务机制彻底失效。一次简单的电商下单操作可能需要跨订单、库存、账户、支付四个服务调用,任何一个环节出现网络超时或服务宕机,都会导致严重的数据不一致 ------ 订单生成了但库存没扣,钱付了但订单没创建,这些问题轻则造成用户投诉,重则导致企业资产流失。
Seata(Simple Extensible Autonomous Transaction Architecture)作为阿里巴巴开源的分布式事务解决方案,凭借其高性能、低侵入、易上手的特点,已经成为微服务架构下解决分布式事务问题的事实标准。本文将从分布式事务的本质问题出发,深入解析 Seata 的核心原理、四种工作模式的实现细节,并结合完整的代码案例,带你从理论到实践全面掌握 Seata,最终实现生产环境的高可用部署。
一、问题的提出:从单体服务到微服务的事务困境
1.1 单体架构下的事务:简单而美好
在单体架构中,所有业务逻辑运行在同一个进程中,共享同一个数据库连接。此时的事务管理非常简单,我们只需要在方法上添加 **@Transactional**注解,Spring 事务管理器会自动帮我们处理事务的开启、提交和回滚。
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockMapper stockMapper;
@Transactional(rollbackFor = Exception.class)
public Long createOrder(Order order) {
// 1. 创建订单
orderMapper.insert(order);
// 2. 扣减库存
stockMapper.decreaseStock(order.getProductId(), order.getQuantity());
return order.getId();
}
}
代码在单体架构下完美工作,它依赖数据库的ACID 特性来保证原子性:要么两个操作都成功提交,要么任何一个失败都回滚到事务开始前的状态。**数据库通过锁机制和事务日志(Redo Log、Undo Log)**来实现这一点,整个过程对开发者完全透明。
1.2 微服务架构下的事务:噩梦的开始
当我们将系统拆分为微服务后,订单服务和库存服务变成了两个独立部署的应用,分别连接自己的数据库。此时,上面的代码就会出现严重的问题:
这里利用 Feign 进行远程调用,不了解的小伙伴可以看看微服务的相关内容。
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockFeignClient stockFeignClient;
@Transactional(rollbackFor = Exception.class)
public Long createOrder(Order order) {
// 1. 订单服务本地操作:创建订单
orderMapper.insert(order);
// 2. 远程调用库存服务扣减库存
stockFeignClient.decreaseStock(order.getProductId(), order.getQuantity());
return order.getId();
}
}
这段代码看起来和单体架构下几乎一样,但实际上存在致命的缺陷:**@Transactional **注解只能管理订单服务本地的数据库事务,无法控制远程调用的库存服务事务。
我们来分析一个典型的失败场景:
- 用户发起下单请求
- 订单服务创建订单,本地事务执行成功
- 订单服务调用库存服务扣减库存
- 如果库存服务某一时刻因为网络超时或服务宕机,扣减操作失败
- 订单服务抛出异常,本地事务回滚,订单被删除
此时就出现了数据不一致:订单已经回滚了,但如果库存服务实际上已经扣减了库存(只是响应超时了),就会导致库存凭空消失,造成企业资产损失。反之,如果订单创建成功,库存扣减失败但订单没有回滚,就会出现超卖问题。
这就是微服务架构下的分布式事务难题:一次业务操作跨越多个服务和多个数据库,本地事务无法保证全局数据的一致性。
1.3 分布式事务的核心命题
分布式事务的本质是要保证:在分布式系统中,一系列跨服务、跨数据源的操作要么全部成功,要么全部失败,最终确保全局数据的一致性。
为了解决这个问题,我们需要一个全局的事务协调者,来协调各个分支事务的执行状态。所有分支事务都要向协调者报告自己的执行结果,协调者根据所有分支的结果来决定最终是全局提交还是全局回滚。
二、理论基础:分布式系统的 CAP 与 BASE 定理
在深入了解 Seata 之前,我们必须先掌握分布式系统的两个核心理论:CAP 定理和 BASE 理论,它们是所有分布式事务解决方案的理论基础。
2.1 CAP 定理:分布式系统的不可能三角
1998 年,加州大学的计算机科学家 Eric Brewer 提出了著名的 CAP 定理:一个分布式系统不可能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三个基本需求,最多只能同时满足其中的两个。
2.1.1 一致性(Consistency)
一致性指的是所有用户在同一时间访问分布式系统中的任意节点,得到的数据必须是完全一致的。也就是说,当一个数据更新操作成功后,所有节点上的该数据都应该同时更新到最新状态。
例如,在一个分布式数据库集群中,当用户在节点 A 更新了一条数据后,其他用户在节点 B、C、D 上读取这条数据时,都应该能看到更新后的结果,而不是旧数据。
2.1.2 可用性(Availability)
可用性指的是用户访问集群中的任意健康节点,必须能在合理的时间内得到合理的响应,而不是超时或错误。也就是说,系统必须一直处于可用状态,能够正常处理用户的请求。
例如,一个电商网站在双十一期间,即使流量暴增,也必须保证用户能够正常浏览商品、下单和支付,不能出现大面积的服务不可用。
2.1.3 分区容错性(Partition tolerance)
分区容错性指的是当分布式系统中的部分节点因为网络故障而失去连接,形成独立的分区时,整个系统仍然能够继续对外提供服务。
在分布式系统中,网络故障是不可避免的。节点之间可能会因为网络延迟、丢包、断网等原因无法通信,形成多个独立的分区。分区容错性要求系统在这种情况下仍然能够正常工作,不能因为网络分区而崩溃。
2.1.4 CAP 的取舍
CAP 定理告诉我们,分布式系统无法同时满足这三个特性,我们必须在其中做出取舍。那么,我们应该如何选择呢?
首先,分区容错性(P)是必须满足的。因为在分布式系统中,网络分区是必然会发生的,我们无法避免。如果一个系统不具备分区容错性,那么当网络出现问题时,整个系统就会崩溃,这在分布式环境下是不可接受的。
因此,我们只能在一致性(C)和可用性(A)之间做出选择:
- 选择 C 而放弃 A:当网络分区发生时,为了保证数据的一致性,系统必须拒绝用户的请求,直到所有节点的数据都同步完成。这种系统被称为 CP 系统,例如 ZooKeeper、HBase 等。
- 选择 A 而放弃 C:当网络分区发生时,为了保证系统的可用性,系统可以继续对外提供服务,但可能会返回不一致的数据。当网络恢复后,系统再通过异步同步的方式来达到最终一致性。这种系统被称为 AP 系统,例如 Eureka、Cassandra 等。
2.2 BASE 理论:CAP 的折中方案
CAP 定理告诉我们,分布式系统无法同时满足强一致性和高可用性。但在实际的业务场景中,很多时候我们并不需要强一致性,只需要保证最终一致性即可。BASE 理论就是对 CAP 定理的一种折中,它允许系统在一段时间内处于不一致的状态,但最终要达到一致。
BASE 理论包含三个核心思想:
2.2.1 基本可用(Basically Available)
基本可用指的是当分布式系统出现故障时,允许损失部分可用性,保证核心功能可用。
例如,在电商网站的秒杀活动中,当流量暴增时,系统可以将非核心功能(如商品推荐、用户评价)降级,只保证下单和支付这些核心功能可用。或者,系统可以对用户进行限流,只允许部分用户进入,避免整个系统崩溃。
2.2.2 软状态(Soft State)
软状态指的是允许系统中的数据存在中间状态,并且这个中间状态不会影响系统的整体可用性。
例如,在转账业务中,当用户 A 向用户 B 转账时,系统可以先将 A 的账户余额扣除,然后将 B 的账户余额标记为 "待入账" 状态。此时,系统处于一个中间状态,A 和 B 的账户余额加起来不等于转账前的总和。但这个中间状态是暂时的,当转账完成后,B 的账户余额会被更新,系统最终达到一致状态。
2.2.3 最终一致性(Eventually Consistent)
最终一致性指的是虽然系统在短时间内可能处于不一致的状态,但经过一段时间后,所有节点的数据最终会达到一致。
最终一致性是 BASE 理论的核心。在分布式系统中,我们通常不需要保证数据的强一致性,只需要保证最终一致性即可。例如,在电商系统中,用户下单后,库存可能不会立即扣减,而是在几秒钟后才更新。但只要最终库存和订单数量是一致的,就不会影响业务的正常运行。
2.3 分布式事务的两种模式
基于 CAP 和 BASE 理论,分布式事务可以分为两种模式:
- CP 模式:强一致性事务。所有分支事务执行后互相等待,同时提交,同时回滚,达成强一致。但在事务等待过程中,系统处于弱可用状态。典型的实现有 2PC、3PC、Seata 的 XA 模式。
- AP 模式:最终一致性事务。各分支事务分别执行和提交,允许出现结果不一致,然后通过补偿措施来恢复数据,实现最终一致。这种模式具有高可用性,是大多数互联网业务的首选。典型的实现有Seata的 TCC、SAGA、AT 模式。
三、Seata 核心概念:三大角色与整体架构
Seata 是一款开源的分布式事务解决方案,它通过三个核心角色的协同工作,来实现分布式事务的管理。这三个角色分别是:事务协调器(TC)、事务管理器(TM)和资源管理器(RM)。
3.1 三大核心角色
3.1.1 事务协调器(TC,Transaction Coordinator)
TC 是全局事务的 "总指挥",是 Seata 的核心服务。它负责维护全局事务和分支事务的状态,根据所有分支事务的执行结果,协调驱动各分支事务最终提交或回滚。
TC 的主要职责包括:
- 接收 TM 的请求,开启全局事务,生成全局唯一的 XID
- 接收 RM 的注册请求,管理分支事务
- 记录全局事务和分支事务的状态
- 当所有分支事务执行完毕后,根据 TM 的决议,向所有 RM 发送全局提交或回滚指令
- 处理事务超时、异常等情况
3.1.2 事务管理器(TM,Transaction Manager)
TM 定义全局事务的范围与边界,通常由业务系统的入口服务担任。它负责向 TC 发起开启全局事务的请求,在业务逻辑结束时,发起全局提交或回滚的决议。
TM 的主要职责包括:
- 向 TC 申请开启一个全局事务,获取全局唯一的 XID
- 将 XID 传播到所有参与全局事务的微服务中
- 监控所有分支事务的执行状态
- 当所有分支事务都执行成功时,向 TC 发起全局提交请求
- 当任何一个分支事务执行失败时,向 TC 发起全局回滚请求
3.1.3 资源管理器(RM,Resource Manager)
RM 管理实际的数据库或服务资源,每个微服务节点都可以作为一个 RM。它负责向 TC 注册自己为该 XID 下的一个分支事务,执行本地业务逻辑,并严格接收并执行 TC 发出的提交或回滚指令。
RM 的主要职责包括:
- 向 TC 注册分支事务,报告分支事务的执行状态
- 执行本地业务操作,管理本地事务
- 接收 TC 的指令,执行本地事务的提交或回滚
- 在 AT 模式下,生成和管理 Undo Log,用于数据回滚
3.2 Seata 的整体工作流程
Seata 的分布式事务执行过程可以分为五个步骤:
- 开启全局事务:TM 向 TC 申请开启一个全局事务,TC 创建全局事务并返回一个全局唯一的 XID,作为全局事务的标识。
- 注册分支事务:TM 依次调用各微服务(RM1、RM2 等)。RM 在执行本地事务前,向 TC 注册自己为该 XID 下的一个分支事务。
- 执行分支事务:各个 RM 执行各自的本地业务逻辑(如扣减库存、创建订单),并将执行结果上报给 TC。
- 全局决议:当所有分支事务执行完毕,TM 根据收集到的业务执行结果,向 TC 发起最终的全局提交或全局回滚决议。
- 全局提交 / 回滚:TC 协调所有 RM 执行统一的提交或回滚操作,确保所有微服务的数据要么全部成功提交,要么一起回滚,达成最终一致性。
3.3 Seata 的整体架构
Seata 采用了微服务架构,主要由以下几个部分组成:
- TC Server:事务协调器服务,独立部署,负责全局事务的协调和管理(TM 和 RM都要向 TC 发送请求)。
- TM Client:事务管理器客户端,集成在业务服务中,负责开启、提交和回滚全局事务。
- RM Client:资源管理器客户端,集成在业务服务中,负责管理本地资源,执行本地事务的提交和回滚。
- 注册中心:用于 TC 和微服务之间的服务发现,支持 Nacos、Eureka、Consul 等。
- 配置中心:用于存储 Seata 的配置信息,支持 Nacos、Apollo 等。
TC 服务的部署:
需要打开你下载好的 seata 的 conf 目录下的 register.conf 目录进行修改(这里给出 nacos 的相关配置):
注册中心 配置中心


在下载 seata 服务时,要注意:
1.注意 jdk 的版本,不同 seata 版本的垃圾回收器不同,注意与当前环境变量中的 jdk 兼容或者更改bin目录下 seata-server.bat 配置文件的 jdk 指定。
2.注意 jvm 内存分配问题,同上。
3.注意pom文件中的 mysql 驱动的版本(大多数情况是没问题的)。如果是数据库配置时区问题 ,则在 nacos 连接数据库的 url 加 &serverTimezone=Asia/Shanghai ;如果发送请求时出现超时问题,则可能是 mysql 驱动版本出现问题。
四、Seata 的四种工作模式详解
Seata 提供了四种不同的分布式事务解决方案,分别是 XA 模式、AT 模式、TCC 模式和 SAGA 模式。每种模式都有其适用的场景和优缺点,我们可以根据业务需求选择合适的模式。
4.0 SpringBoot 集成 Seata
在讲解 Seata 的模式前,我们需要先在框架中集成 Seata,这里以 SpringBoot 为例:
引入 Seata 相关依赖 (和你下载的 seata 版本保持一致):
XML
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本较低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
application.yml 的配置
目的是让微服务通过注册中心找到 seata 服务
bash
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-server
username: nacos
password: nacos
tx-service-group: seata-demo # 事务名称,根据这个获取TC服务的cluster名称
service:
vgroup-mapping: # 事务组与TC服务cluster的映射关系
seata-demo: SH
4.1 XA 模式:强一致性的两阶段提交
XA 模式是基于数据库原生的 XA 规范实现的强一致性事务模式。XA 规范是 X/Open 组织定义的分布式事务处理标准,几乎所有主流的关系型数据库(如 MySQL、Oracle、PostgreSQL)都对 XA 规范提供了支持。
4.1.1 XA 模式的执行流程
XA 模式采用两阶段提交协议(2PC),整个过程分为准备阶段和提交 / 回滚阶段:
第一阶段(准备阶段):
- TM 向 TC 申请开启全局事务,获取 XID。
- TM 调用各个微服务的接口。
- RM 向 TC 注册分支事务。
- RM 执行本地业务 SQL,但不提交事务。
- RM 将本地事务的执行状态(成功或失败)上报给 TC。
第二阶段(提交 / 回滚阶段):
- TC 检查所有分支事务的执行状态。
- 如果所有分支事务都执行成功,TC 向所有 RM 发送提交指令。
- RM 提交本地事务,释放数据库资源。
- 如果有任何一个分支事务执行失败,TC 向所有 RM 发送回滚指令。
- RM 回滚本地事务,释放数据库资源。
4.1.2 XA 模式的代码实现
要在 Seata 中使用 XA 模式,非常简单,只需要两步:
第一步:修改 application.yml 文件,开启 XA 模式
bash
seata:
# 开启数据源代理的XA模式
data-source-proxy-mode: XA
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-server
tx-service-group: seata-demo
service:
vgroup-mapping:
seata-demo: SH
第二步:给发起全局事务的入口方法添加 @GlobalTransactional 注解
java
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountFeignClient accountFeignClient;
@Autowired
private StockFeignClient stockFeignClient;
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public Long create(Order order) {
// 1. 创建订单
orderMapper.insert(order);
// 2. 扣减用户余额
accountFeignClient.decrease(order.getUserId(), order.getMoney());
// 3. 扣减商品库存
stockFeignClient.decrease(order.getProductId(), order.getQuantity());
return order.getId();
}
}
4.1.3 XA 模式的优缺点
优点:
- 强一致性:满足 ACID 原则,所有分支事务要么全部提交,要么全部回滚,数据完全一致。
- 无代码侵入:Seata 已经完成了 XA 模式的自动装配,开发者只需要添加一个注解即可使用,不需要修改业务代码。
- 数据库原生支持:几乎所有主流的关系型数据库都支持 XA 规范,实现简单。
缺点:
- 性能差:在第一阶段,RM 执行完 SQL 后不提交事务,会一直持有数据库锁,直到第二阶段结束才释放。这会导致数据库资源被长时间占用,并发性能差。
- 依赖关系型数据库:只能用于支持 XA 规范的关系型数据库,无法用于非关系型数据库(如 Redis、MongoDB)。
4.1.4 XA 模式的适用场景
XA 模式适用于对数据一致性和隔离性有极高要求的业务场景,例如银行转账、金融交易等。但由于其性能较差,不适合高并发的互联网业务。
4.2 AT 模式:高性能的最终一致性模式(默认推荐)
AT 模式是 Seata 的默认模式,也是最常用的模式。它同样采用两阶段提交协议,但弥补了 XA 模式中资源锁定周期过长的缺陷,实现了高性能的最终一致性。
4.2.1 AT 模式的执行流程
AT 模式的执行流程也分为两个阶段:
第一阶段:
- TM 向 TC 申请开启全局事务,获取 XID。
- TM 调用各个微服务的接口。
- RM 向 TC 注册分支事务。
- RM 执行业务 SQL,并生成 Undo Log(数据快照)。
- RM 提交本地事务,释放数据库资源。
- RM 将执行结果上报给 TC。
第二阶段(提交):
- 如果所有分支事务都执行成功,TC 向所有 RM 发送提交指令。
- RM 只需要删除 Undo Log 即可,不需要执行其他操作。
第二阶段(回滚):
- 如果有任何一个分支事务执行失败,TC 向所有 RM 发送回滚指令。
- RM 根据 Undo Log 中的数据快照,将数据恢复到更新前的状态。
- RM 删除 Undo Log。
4.2.2 Undo Log 的结构
Undo Log 是 AT 模式实现回滚的关键,它记录了数据更新前后的快照。Undo Log 存储在每个微服务的数据库中,表名为undo_log。
undo_log表的结构如下:
sql
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,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
其中,**rollback_info **字段存储了数据更新前后的二进制快照。当需要回滚时,Seata 会解析这个字段,生成反向的 SQL 语句,将数据恢复到原来的状态。
4.2.3 AT 模式的脏写问题与全局锁
AT 模式在第一阶段就提交了本地事务,释放了数据库锁。这会带来一个问题:如果两个全局事务同时修改同一行数据,就可能出现脏写。
例如:
- 全局事务 1 修改账户 A 的余额,从 100 减到 90,提交本地事务,释放数据库锁。
- 全局事务 2 修改账户 A 的余额,从 90 减到 80,提交本地事务,释放数据库锁。
- 全局事务 1 因为某个分支事务失败,需要回滚。此时,它会根据 Undo Log 将余额恢复到 100,覆盖了全局事务 2 的修改。
这就是 AT 模式的脏写问题。为了解决这个问题,Seata 引入了全局锁机制。
全局锁机制流程:
全局锁由 TC 维护,它记录了当前正在操作某行数据的全局事务 XID。当一个全局事务要修改某行数据时,必须先向 TC 申请获取该行数据的全局锁。只有获取到全局锁的事务才能执行修改操作。
如果全局事务 1 已经获取了某行数据的全局锁,那么全局事务 2 在申请全局锁时会失败,它会不断重试,直到全局事务 1 释放全局锁或者超时(这里获取全局锁时,会进行获取重试,当达到一定时间时,仍获取不到,会释放全局锁并回滚之前的操作,避免发生死锁)。这样就保证了同一时间只有一个全局事务能够修改某行数据,避免了脏写问题。
另一个脏些问题:
当然可能还会出现一种情况,一个不受全局锁控制的事务这时也来修改资源,这个事务无需全局锁就可以修改资源,也会发生脏写问题,当然这种情况其实发生的概率很小,具体原因如下:
1.全局事务的执行大多数情况是成功的,二阶段是提交而不是回滚。
2.分布式事务的耗时比较长(业务链路比较长),因此并发比较低,不会出现同时操作资源。
3.在实际业务中会尽量避免多个事务同时操作资源。
但是也不能保证百分百避免,seata 里其实有一个 双快照 的解决方案,上述的案例的事务1在修改资源前 保存一次快照,称为 before-image,修改资源后再保存一次快照,称为 after-image。如果事务1在二阶段要进行回滚,它会拿 after-image 与数据库目前的资源情况进行比较:如果一致,说明之前没有其他事务(不受全局锁控制)对资源修改,正常回滚;如果不一致,则说明有事务对资源进行了修改,此时需要记录异常,发送警告、人工介入。
4.2.4 AT 模式的代码实现
使用 AT 模式同样非常简单:
第一步:在每个微服务的数据库中创建 undo_log 表 执行上面给出的undo_log表创建 SQL。
第二步:修改 application.yml 文件,开启 AT 模式
bash
seata:
# 开启数据源代理的AT模式(默认就是AT,可以不写)
data-source-proxy-mode: AT
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-server
tx-service-group: seata-demo
service:
vgroup-mapping:
seata-demo: SH
第三步:在 TC 服务的数据库中创建 lock_table 表
全局锁存储在 TC 服务的数据库中,表名为lock_table:
sql
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(128) DEFAULT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(36) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第四步:给业务入口方法添加 @GlobalTransactional 注解
和 XA 模式一样,只需要在入口方法上添加 **@GlobalTransactional**注解即可。
4.2.5 AT 模式的优缺点
优点:
- 性能好:一阶段完成就提交本地事务,释放数据库资源,锁持有时间短,并发性能高。
- 无代码侵入:和 XA 模式一样,开发者只需要添加一个注解即可使用,不需要修改业务代码。
- 读写隔离:通过全局锁机制实现了读写隔离,避免了脏写问题。
缺点:
- 最终一致性:两阶段之间属于软状态,数据可能存在短暂的不一致,最终才会达到一致。
- 快照生成有性能开销:生成 Undo Log 会有一定的性能开销,但比 XA 模式的性能好很多。
4.2.6 AT 模式的适用场景
AT 模式适用于绝大多数基于关系型数据库的分布式事务场景,尤其是高并发的互联网业务,如电商下单、库存管理等。它是 Seata 推荐的默认模式。
4.3 TCC 模式:细粒度控制的补偿模式
TCC(Try-Confirm-Cancel)模式是一种手动实现补偿的模式,它将一个分布式事务拆分为三个阶段:Try 阶段、Confirm 阶段和 Cancel 阶段。TCC 模式通过业务逻辑层定义操作和补偿,为分布式事务提供了更细粒度的资源控制能力。
4.3.1 TCC 模式的三个阶段
- Try 阶段:资源检测和预留。检查业务参数和状态,锁定并预留后续业务逻辑执行所需的全部资源,保证业务操作的前置条件满足。
- Confirm 阶段:确认执行。使用 Try 阶段预留的资源,真正执行业务逻辑,完成最终的状态变更。该阶段必须保证操作的幂等性。
- Cancel 阶段:取消回滚。当事务执行失败时,释放 Try 阶段锁定的所有资源,将数据恢复到事务开始前的初始状态。该阶段也必须保证操作的幂等性。
4.3.2 TCC 模式的执行流程
- TM 向 TC 申请开启全局事务,获取 XID。
- TM 调用各个微服务的 Try 接口。
- RM 执行 Try 阶段的逻辑,预留资源,并向 TC 注册分支事务。
- 所有 Try 阶段执行完毕后,TM 根据执行结果向 TC 发起全局提交或回滚。
- 如果所有 Try 都成功,TC 向所有 RM 发送 Confirm 指令,RM 执行 Confirm 阶段的逻辑。
- 如果有任何一个 Try 失败,TC 向所有 RM 发送 Cancel 指令,RM 执行 Cancel 阶段的逻辑。
4.3.3 TCC 模式的常见问题与解决方案
TCC 模式虽然提供了更细粒度的控制,但也带来了一些复杂的问题,需要开发者手动处理:
1. 幂等性问题 Confirm 和 Cancel 接口可能会被 Seata 重复调用,因此必须保证幂等性。也就是说,无论调用多少次,最终的结果都应该是一样的。
解决方案:在数据库中记录每个分支事务的执行状态,每次调用 Confirm 或 Cancel 时,先检查状态,如果已经执行过,就直接返回成功。
2. 空回滚问题 当某分支事务的 Try 阶段因为网络阻塞而没有执行时,全局事务可能会因为超时而触发 Cancel 操作。此时,Cancel 接口被调用,但 Try 还没有执行,这就是空回滚。
解决方案:在 Cancel 接口中,先判断该分支事务是否已经执行了 Try 阶段。如果没有执行,就直接返回成功,不需要执行回滚逻辑。
3. 业务悬挂问题 对于已经空回滚的业务,如果后续 Try 阶段的请求到达并执行了,那么这个分支事务就永远不会有 Confirm 或 Cancel 操作了,这就是业务悬挂。
解决方案:在 Try 接口中,先判断该分支事务是否已经执行过 Cancel 操作。如果已经执行过,就拒绝执行 Try 逻辑。
4.3.4 TCC 模式的代码实现
我们以账户服务扣减余额为例,实现一个完整的 TCC 接口。
第一步:创建账户冻结表
为了记录冻结金额和事务状态,我们需要创建一个 **account_freeze_tbl**表:
sql
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(128) NOT NULL,
`user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
`freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
`state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
第二步:定义 TCC 接口
sql
@LocalTCC
public interface AccountTCCService {
/**
* Try阶段:冻结用户余额
* @param userId 用户id
* @param money 扣减金额
* @return 是否成功
*/
@TwoPhaseBusinessAction(name = "accountTCC", commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDecrease(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money") int money);
/**
* Confirm阶段:确认扣减
* @param context 事务上下文
* @return 是否成功
*/
boolean confirm(BusinessActionContext context);
/**
* Cancel阶段:回滚扣减
* @param context 事务上下文
* @return 是否成功
*/
boolean cancel(BusinessActionContext context);
}
第三步:实现 TCC 接口
sql
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper freezeMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean tryDecrease(String userId, int money) {
// 1. 获取XID
String xid = RootContext.getXID();
// 2. 避免业务悬挂:判断是否已经执行过Cancel
AccountFreeze freeze = freezeMapper.selectById(xid);
if (freeze != null && freeze.getState() == 2) {
// 已经执行过Cancel,拒绝执行Try
return false;
}
// 3. 检查可用余额是否充足
Account account = accountMapper.selectById(userId);
if (account.getMoney() < money) {
// 余额不足,扣减失败
return false;
}
// 4. 扣减可用余额
accountMapper.decrease(userId, money);
// 5. 记录冻结金额和事务状态
freeze = new AccountFreeze();
freeze.setXid(xid);
freeze.setUserId(userId);
freeze.setFreezeMoney(money);
freeze.setState(0); // 0: try
freezeMapper.insert(freeze);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean confirm(BusinessActionContext context) {
// 1. 获取XID
String xid = context.getXid();
// 2. 幂等性判断:如果已经执行过Confirm,直接返回成功
AccountFreeze freeze = freezeMapper.selectById(xid);
if (freeze == null || freeze.getState() == 1) {
return true;
}
// 3. 删除冻结记录
freezeMapper.deleteById(xid);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancel(BusinessActionContext context) {
// 1. 获取XID
String xid = context.getXid();
// 2. 幂等性判断:如果已经执行过Cancel,直接返回成功
AccountFreeze freeze = freezeMapper.selectById(xid);
if (freeze == null || freeze.getState() == 2) {
return true;
}
// 3. 空回滚判断:如果Try还没执行,直接返回成功
if (freeze.getState() != 0) {
return true;
}
// 4. 恢复可用余额
accountMapper.increase(freeze.getUserId(), freeze.getFreezeMoney());
// 5. 更新冻结记录状态为Cancel
freeze.setState(2);
freezeMapper.updateById(freeze);
return true;
}
}
第四步:在业务入口使用 TCC 服务
sql
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountTCCService accountTCCService;
@Autowired
private StockFeignClient stockFeignClient;
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public Long create(Order order) {
// 1. 创建订单
orderMapper.insert(order);
// 2. 调用账户服务的TCC Try接口冻结余额
accountTCCService.tryDecrease(order.getUserId(), order.getMoney());
// 3. 扣减商品库存
stockFeignClient.decrease(order.getProductId(), order.getQuantity());
return order.getId();
}
}
4.3.5 TCC 模式的优缺点
优点:
- 性能最好:一阶段完成就提交本地事务,释放数据库资源,不需要生成快照,也不需要全局锁,性能最高。
- 细粒度控制:可以对每个资源进行精确的控制,适合对性能要求极高的场景。
- 不依赖数据库事务:可以用于非关系型数据库(如 Redis、MongoDB)。
缺点:
- 代码侵入性高:需要开发者手动编写 Try、Confirm、Cancel 三个接口,开发成本高。
- 复杂度高:需要手动处理幂等性、空回滚、业务悬挂等问题,容易出错。
- 最终一致性:属于最终一致性事务。
4.3.6 TCC 模式的适用场景
TCC 模式适用于对性能要求极高、对数据一致性要求也很高的核心业务场景,例如金融支付、交易系统等。它也适合需要使用非关系型数据库参与事务的场景。
4.4 SAGA 模式:长事务的解决方案
SAGA 模式适用于处理长流程、长周期的分布式事务。它将分布式事务拆分为多个可独立提交的本地事务,通过 "正向执行 + 反向补偿" 的机制保证数据最终一致性。
4.4.1 SAGA 模式的执行流程
SAGA 模式的执行逻辑非常简单:
- 正向执行:按顺序依次执行每个本地事务 T1 → T2 → T3 → ... → Tn。
- 反向恢复:如果某一步骤 Ti 执行失败,则按相反顺序执行补偿操作 C (i-1) → ... → C2 → C1,撤销之前已经完成的步骤。
例如,一个订单全流程包括创建订单、扣减库存、支付、发货四个步骤:
- 执行 T1:创建订单
- 执行 T2:扣减库存
- 执行 T3:支付(失败)
- 执行 C2:恢复库存
- 执行 C1:取消订单
4.4.2 SAGA 模式的两种实现方式
SAGA 模式有两种常见的实现方式:
- 编排式:由一个中央协调者来控制整个事务的流程,协调者调用各个服务的接口,并根据执行结果决定下一步是继续执行还是执行补偿操作。
- 编排式:每个服务在完成自己的事务后,发布一个事件,其他服务监听这个事件并执行自己的事务。如果某个服务执行失败,它会发布一个补偿事件,其他服务监听这个事件并执行补偿操作。
Seata 目前支持编排式的 SAGA 模式,通过状态机来定义事务流程和补偿逻辑。
4.4.3 SAGA 模式的优缺点
优点:
- 性能好:每个本地事务都独立提交,释放数据库资源,性能高。
- 适合长事务:可以处理流程很长、周期很长的事务,例如包含人工审核的业务流程。
- 无锁:不需要全局锁,并发性能高。
缺点:
- 代码侵入性高:需要为每个业务步骤编写对应的正向操作和反向补偿操作,开发成本高。
- 无隔离性:没有事务隔离机制,可能会出现脏写、脏读等问题。
- 复杂度高:当事务流程很长时,补偿逻辑会变得非常复杂,难以维护。
4.4.4 SAGA 模式的适用场景
SAGA 模式适用于业务流程长、业务参与者多的场景,例如订单全流程状态流转、金融跨行转账、包含人工审核 / 干预的长事务场景等。
4.5 四种模式对比
为了方便大家选择合适的模式,我们将四种模式进行对比:
| 特性 | XA 模式 | AT 模式 | TCC 模式 | SAGA 模式 |
|---|---|---|---|---|
| 一致性 | 强一致 | 最终一致 | 最终一致 | 最终一致 |
| 隔离性 | 完全隔离 | 基于全局锁隔离 | 基于资源预留隔离 | 无隔离 |
| 代码侵入 | 无 | 无 | 有,需编写三个接口 | 有,需编写状态机和补偿业务 |
| 性能 | 差 | 好 | 非常好 | 非常好 |
| 开发成本 | 低 | 低 | 高 | 高 |
| 运维复杂度 | 低 | 中 | 高 | 高 |
| 适用场景 | 对一致性、隔离性有高要求的业务 | 基于关系型数据库的大多数分布式事务场景 | 对性能要求较高的事务;有非关系型数据库参与的事务 | 业务流程长、业务流程多;参与者包含其它公司或遗留系统服务 |
五、Seata 的高可用
TC 服务作为 Seata 的核心服务,必须保证高可用,否则整个分布式事务系统将无法工作。Seata 支持基于 Nacos 的集群部署,实现 TC 服务的高可用。
TC 集群部署步骤:
- 准备多台服务器,安装相同版本的 Seata TC。
- 配置所有 TC 服务使用同一个数据库存储事务信息。
- 配置所有 TC 服务注册到同一个 Nacos 注册中心,使用相同的服务名称。
- 微服务配置文件中不需要修改,Seata 会自动从 Nacos 中发现所有可用的 TC 服务,并进行负载均衡。
常见问题:
1. MySQL 驱动版本问题 Seata 对 MySQL 驱动版本有一定的要求,如果驱动版本不兼容,可能会出现各种奇怪的问题。建议使用 5.1.47 或 8.0.20 以上版本的 MySQL 驱动。
2. 时区问题 如果数据库和应用服务器的时区不一致,可能会导致事务超时、日志时间错误等问题。建议在数据库连接 URL 中添加serverTimezone=Asia/Shanghai参数,指定时区为东八区。
3. 事务超时问题 Seata 默认的事务超时时间是 60 秒,如果业务执行时间超过 60 秒,事务会被自动回滚。可以通过修改 TC 服务的service.default.grouplist配置来调整超时时间。
4. 幂等性问题 无论是哪种模式,都必须保证业务接口的幂等性。因为网络问题,Seata 可能会重复调用同一个接口,如果接口不幂等,就会导致数据重复操作。
5. 数据一致性校验 即使使用了 Seata,也建议定期进行数据一致性校验,特别是在系统出现异常或故障后。可以通过编写定时任务,对比订单、库存、账户等数据,确保数据一致。
六、总结
Seata 作为一款优秀的开源分布式事务解决方案,为微服务架构下的数据一致性问题提供了完整的解决方案。它通过 TC、TM、RM 三个核心角色的协同工作,实现了四种不同的事务模式,能够满足各种业务场景的需求。
在实际的项目中,我们应该根据业务的特点和需求,选择合适的事务模式:
- 对于大多数普通业务场景,AT 模式是首选,它无代码侵入、性能好、易于使用。
- 对于对一致性要求极高的金融业务,可以选择 XA 模式。
- 对于对性能要求极高的核心业务,可以选择 TCC 模式。
- 对于长流程、长周期的业务,可以选择 SAGA 模式。
当然,Seata 也不是银弹。在引入 Seata 之前,我们应该仔细评估业务是否真的需要分布式事务。如果业务可以通过其他方式(如本地事务 + 异步消息)来保证最终一致性,那么就不需要引入 Seata,避免过度设计。
随着微服务架构的不断普及,分布式事务问题会越来越常见。Seata 作为目前最成熟、最实用的分布式事务解决方案之一,将会在未来的微服务开发中发挥越来越重要的作用。