Seata分布式事务
目录
- 概念
- 本地事务
- 分布式事务
- 全局事务
- 分支事务
- 全局事务和分支事务的关系
- Seata
- Seata解决分布式事务的思路
- Seata部署服务器端(TC)
- 微服务整合Seata客户端
- XA模式
- 工作流程
- XA 模式配置与使用
- XA 模式优缺点
- AT模式
- 工作流程
- AT 模式优缺点
- AT 模式配置与使用
- TM和RM的交互
概念
本地事务
- 在传统的单体应用中,我们通常使用本地事务来保证数据一致性。本地事务依赖于关系型数据库本身的ACID特性(原子性、一致性、隔离性、持久性),所有操作都在同一个数据库连接中完成,要么全部成功,要么全部失败
分布式事务
- 随着互联网技术的快速发展,软件系统从单体应用转变为分布式应用。在微服务架构中,一个业务需要联调多个微服务实现,每个微服务都是其中一个环节,也都有自己的数据库事务。所以,分布式事务简单来说就是一个业务涉及的多个微服务事务要保证ACID属性,这些事务要么同时成功,要么同时失败。
- 举一个简单的例子,订单微服务和库存微服务,下单的同时订单微服务需要远程调用库存微服务进行减库存操作,要保证订单服务和库存服务对数据库的操作同成功,或者同失败。
- 当然,分布式事务也有另外两个场景,一个服务连接多个数据库,多个微服务连接同一个数据库,这些都是分布式事务的应用场景,解决思路和措施都是一样的。
在分布式事务处理中,有两个核心概念:全局事务和分支事务。
全局事务
- 全局事务是指整个分布式事务的范围,它是由一系列分布在多个微服务上的操作组成的逻辑单元。全局事务需要保证所有这些操作要么全部成功,要么全部失败,以维护分布式系统的数据一致性。
- 在Java微服务场景中,全局事务通常由一个事务管理器(Transaction Manager, TM) 来定义和控制。TM负责界定全局事务的边界(开始、提交或回滚),并根据所有分支事务的执行状态做出最终决策。
分支事务
- 分支事务是全局事务中的一个组成部分,通常是每个微服务执行的本地事务
- 每个分支事务负责管理其自身的资源(如数据库),并向全局事务协调者(TC)报告自己的执行状态
全局事务和分支事务的关系
- 全局事务和分支事务是整体与部分的关系。一个全局事务包含多个分支事务,全局事务的状态取决于所有分支事务的执行结果。它们共同协作,确保分布式系统中的数据操作满足原子性要求。
Seata
Seata 是一款由阿里开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
Seata解决分布式事务的思路
- 其实分布式事务产生的一个重要原因,就是参与事务的多个分支事务互相无感知,不知道彼此的执行状态。因此解决分布式事务的思路就是找一个统一的事务协调者(TC),与多个分支事务通信(RM),检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。
- Seata也不例外,在Seata的事务管理中有三个重要的角色
角色 | 作用 |
---|---|
TC-事务协调者 | 维护全局和分支事务的状态,协调全局事务提交或回滚。 |
TM-事务管理器 | 定义全局事务的范围、开始全局事务、提交或回滚全局事务 |
RM-资源管理器 | 管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚 |
- 其中,TM和RM可以理解为Seata的客户端部分,引入到参与事务的微服务依赖中即可。将来TM和RM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。
- 而TC服务则是事务协调中心,是一个独立的微服务,需要单独部署。
Seata部署服务器端(TC)
TC Server 的部署模式主要分两种:
- file 模式:事务会话信息存储在本地文件系统中,仅适用于测试和学习
- db 模式:事务会话信息持久化到共享数据库中,适用于生产环境,支持 TC 集群和高可用。
这篇博客主要讲下db模式的部署,这也是TC Server 一般的部署方式,值得说明的是,TC Server也是一个springboot微服务,部署方式与其他boot项目没什么区别,无非就是改改配置文件。
- 下载 Seata Server
从 Seata 官网的下载页面(https://seata.apache.org/zh-cn/download/seata-server)获取最新版本的 seata-server-*.zip包并解压到非中文目录 - 准备数据库(以mysql为例)
创建一个专门用于Seata的数据库(如seata);
执行 Seata 发行包中 script/server/db/mysql.sql的 SQL 脚本,创建全局事务表 global_table、分支事务表 branch_table和全局锁表 lock_table。 - 添加nacos seata配置
你也可以把seata相关配置全配置到application配置文件中,这样就无需从nacos配置中心拉取配置,但是一般生产环境中,seata TC服务器在一个集群中会有很多实例,这些实例共用一个数据库资源(第2步配置的),因此避免重复配置,最好seata配置放在nacos中,避免所有seata TC重复配置。
在 Nacos 控制台 (http://127.0.0.1:8848/nacos) 中,创建一个新的配置,yaml或者properties文件格式都可以
# 数据存储模式,db代表使用数据库进行持久化(生产环境推荐)
store:mode: dbdb:datasource: druiddbType: mysql# 根据你的MySQL版本选择驱动driverClassName: com.mysql.cj.jdbc.Driver # MySQL 8.x# driverClassName: com.mysql.jdbc.Driver # MySQL 5.xurl: jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=UTCuser: rootpassword: your_strong_password_here # 请替换为你的数据库密码minConn: 5maxConn: 30globalTable: global_tablebranchTable: branch_tablelockTable: lock_tablequeryLimit: 100maxWait: 5000
- 修改本地配置:连接注册与配置中心
修改本地application配置文件,配置nacos注册中心和配置中心相关配置,目的是让本地Seata Server将服务ip,端口,名称空间,group(组),集群信息注册到nacos注册中心,方便以后Seata 客户端调用;其次就是从指定的nacos配置中心的特定组(group)中拉取本地Seata Server相关的运行配置,主要是数据库信息
# 示例: Seata 1.5.0+ 的 application.yml 部分配置
seata:config:type: nacos # 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取nacos:server-addr: 127.0.0.1:8848 # Nacos服务器地址group: SEATA_GROUP # 存储Seata配置的Group,通常为 SEATA_GROUPdataId: seataServer.properties # 在Nacos中存储配置的文件名registry:type: nacos # tc服务的注册中心类,这里选择nacosnacos:application: seata-tc-server # TC服务在Nacos中注册的服务名称,可自定义server-addr: 127.0.0.1:8848group: DEFAULT_GROUPnamespace: "" # Nacos命名空间ID,默认为空cluster: SH # TC集群名称,可自定义(如SH, HZ)
- 启动 TC Server
进入 Seata 解压目录的 bin文件夹,执行启动脚本:
Linux/Unix: sh seata-server.sh
Windows: 双击运行 seata-server.bat
微服务整合Seata客户端
- 引入客户端依赖
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><version>2.2.1.RELEASE</version> <!-- 请选择与你的Spring Cloud Alibaba版本兼容的Seata版本 -->
</dependency>
- 配置 Seata 客户端:在 yaml中进行配置,确保与服务端(TC)连接
seata:registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址type: nacos # 注册中心类型 nacosnacos:server-addr: 192.168.150.101:8848 # nacos地址namespace: "" # namespace,默认为空group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUPapplication: seata-server # seata服务名称username: nacospassword: nacostx-service-group: hmall # 事务组名称service:vgroup-mapping: # 事务组与tc集群的映射关系hmall: "SH" # TC集群名称
- 创建 undo_log表:在每个参与分布式事务的业务的数据库中,都需要创建 undo_log表,用于 Seata AT 模式记录回滚日志。
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 AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
XA模式
Seata 的 XA 模式和 AT 模式是两种主流的分布式事务解决方案,它们在实现机制、一致性保证和适用场景上各有特点,XA 模式和AT模式都是采用传统的两阶段提交(2PC)协议来实现强一致性
工作流程
- 第一阶段 - 执行阶段 (Prepare Phase)
- TM 向 TC 申请开启一个全局事务。
- TM 调用各个分支事务(RM)
- 每个 RM 在本地执行业务 SQL,但并不提交,而是等待 TC 的进一步指令。在此期间,相关数据库资源会被锁定
- RM 将本地事务的执行状态(成功或失败)报告给 TC
- 第二阶段 - 提交/回滚阶段 (Commit/Rollback Phase)
- 提交:如果 TC 收到所有 RM 的“成功”报告,则向所有 RM 发送提交指令。各 RM 接收到指令后提交本地事务,并释放资源锁。
- 回滚:如果 TC 收到任何一个 RM 的“失败”报告,则向所有 RM 发送回滚指令。各 RM 接收到指令后回滚本地事务,并释放资源锁
XA 模式配置与使用
- Application.yml 配置
关键是指定 data-source-proxy-mode: XA。
seata:enabled: trueapplication-id: ${spring.application.name}tx-service-group: my_xa_tx_group # 事务组名称,可自定义data-source-proxy-mode: XA # 指定使用XA模式
- 代码中使用
- 事务发起方 (T M) :在全局事务入口方法上添加 @GlobalTransactional。
- 事务参与者 (RM) :无需在本地方法上添加 @Transactional注解,因为事务由 Seata 通过 XA 协议管理。
@Service
public class OrderServiceImpl implements OrderService {@GlobalTransactional(name = "xaCreateOrder", timeoutMills = 300000, rollbackFor = Exception.class)@Overridepublic void createOrder(Order order) {// 1. 本地操作orderMapper.insert(order);// 2. 远程调用其他服务(这些服务内的操作也会被纳入同一全局XA事务)storageFeignClient.deduct(order.getProductId(), order.getCount());accountFeignClient.debit(order.getUserId(), order.getMoney());}
}
XA 模式优缺点
- 优点:保证了数据的强一致性(ACID),且对业务代码无侵入。
- 缺点:性能较差。因为在第一阶段执行后到第二阶段完成前,数据库资源会一直处于锁定状态,在高并发场景下可能成为瓶颈。
AT模式
AT 模式是 Seata 默认且主推的模式。它在 XA 模式的基础上进行了优化,通过一阶段提交并生成回滚日志的方式,大大减少了资源锁定的时间,提升了性能,实现了最终一致性。
工作流程
- 第一阶段 - 执行阶段 (Prepare Phase)
- TM 向 TC 申请开启一个全局事务。
- TM 调用各个分支事务(RM)
- 在执行业务 SQL 前,RM 的数据源代理会先拦截 SQL,查询数据的前镜像(Before Image),即修改前的数据状态。
- RM 执行业务 SQL 并提交本地事务,立即释放本地数据库锁
- 在执行业务 SQL 后,RM 会再次查询数据的后镜像(After Image),即修改后的数据状态
- 将前后镜像信息、SQL 本身等组成回滚日志 (undo_log),存入业务数据库的 undo_log表中,业务数据和回滚日志在同一个本地事务中提交。
- RM 向 TC 注册分支事务并报告执行状态。
- 第二阶段 -基于回滚日志的提交或回滚
- 提交:如果所有分支事务成功,TC 会异步通知所有 RM 删除对应的 undo_log记录即可。因为一阶段已经提交,数据本身就是最终状态。
- 回滚:如果任何分支事务失败,TC 会通知所有 RM 进行回滚。RM 根据 undo_log中的前镜像信息,生成逆向 SQL(如 INSERT 对应 DELETE,UPDATE 对应反向 UPDATE)并执行,将数据恢复至事务开始前的状态,然后删除 undo_log记录。
AT 模式优缺点
- 优点:性能较好(一阶段即释放本地锁),对业务代码无侵入
- 缺点:存在短暂的数据不一致(最终一致),且需要依赖关系型数据库和 undo_log表。
AT 模式配置与使用
yaml配置保持默认配置即可,AT模式是seata的默认工作模式
- 创建 undo_log表
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 AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
- 代码中使用
- 事务发起方 (T M) :同样在全局事务入口方法上添加 @GlobalTransactional。
- 事务参与者 (RM) :每个参与者的本地方法上需要添加 **@Transactional** 注解,以确保本地事务的开启和回滚日志的正确记录。
@Service
public class OrderServiceImpl implements OrderService {@GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)@Overridepublic void createOrder(Order order) {orderMapper.insert(order); // 本地操作storageFeignClient.deduct(order.getProductId(), order.getCount()); // 远程调用}
}
// RM微服务
@Service
public class StorageServiceImpl implements StorageService {@Transactional // AT模式下,RM的本地方法需要@Transactional@Overridepublic void deduct(String commodityCode, int count) {storageMapper.reduceInventory(commodityCode, count); // 本地操作}
}
TM和RM的交互
最后再讲下TM和RM的交互流程,其他微服务的RM是如何知道自己是属于哪个全局事务的。
分支事务识别全局事务的过程,本质是 XID 在分布式系统调用链中传递 的过程:
- 生成与起始(事务发起方 - TM:
- 当使用 @GlobalTransactional注解的方法被调用时,TM 会向 TC 申请开启一个新的全局事务。
- TC 会生成一个全局唯一的 XID,并返回给 TM。
- 这个 XID 会被设置到当前线程的上下文中
- 传播(服务调用链:
- 当 TM(事务发起方服务)通过 RPC(如 Feign、Dubbo)调用其他微服务(RM)时,Seata 的客户端组件(如 Feign 拦截器)会自动地将当前线程上下文中的 XID 添加到 RPC 请求的 header 中(例如 tx_xid)
- 这样,XID 就随着业务请求一起传递到了下游服务
- 接收与注册(事务参与方 - RM)
- 下游服务(RM)的 Seata 客户端拦截器(如 Servlet 过滤器)会从收到的 RPC 请求的 header 中提取出 XID
- 提取出的 XID 会被 绑定到下游服务的当前线程上下文中
- 当该服务执行业务方法(通常是本地数据库事务)时,其 RM 组件会从当前线程上下文中获取到这个 XID,然后带着这个 XID 向 TC 注册分支事务。注册成功后,TC 就明确知道这个分支事务属于哪个全局事务了
- 如果这个下游服务还需要继续调用其他服务,上述传播过程会重复进行,XID 会通过 RPC Header 继续向下传递,确保整个调用链中的所有分支事务都能获取到同一个 XID。