Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
- 对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
- 高性能:减少分布式事务解决方案所带来的性能消耗
**Seata **:
- TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
- TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
- RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。
使用注意事项和总结
Seata TC搭建
# 下载
$ wget https://github.com/seata/seata/releases/download/v1.3.0/seata-server-1.3.0.tar.gz
# 解压
$ tar -zxvf seata-server-1.3.0.tar.gz
启动 TC Server
nohup sh bin/seata-server.sh &
- 默认配置下,Seata TC Server 启动在 8091 端点
创建TC所需要的表
TC运行需要将事务的信息保存在数据库,因此需要创建一些表,找到seata-1.3.0源码的script\server\db
这个目录,将会看到以下SQL文件:
修改TC的注册中心
找到seata-server-1.3.0\seata\conf
这个目录,其中有一个registry.conf
文件,其中配置了TC的注册中心和配置中心。
默认的注册中心是file
形式,实际使用中肯定不能使用,需要改成Nacos形式,改动的地方如下图:
需要改动的地方如下:
- type:改成nacos,表示使用nacos作为注册中心
- application:服务的名称
- serverAddr:nacos的地址
- group:分组
- namespace:命名空间
- username:用户名
- password:密码
修改TC的配置中心
TC的配置中心默认使用的也是file
形式,当然要是用nacos作为配置中心了。
直接修改registry.conf
文件,需要改动的地方如下图:
需要改动的地方如下:
- type:改成nacos,表示使用nacos作为配置中心
- serverAddr:nacos的地址
- group:分组
- namespace:命名空间
- username:用户名
- password:密码
上述配置修改好之后,在TC启动的时候将会自动读取nacos的配置。
TC需要存储到Nacos中的配置都哪些,如何推送过去?
在seata-1.3.0\script\config-center
中有一个config.txt
文件,其中就是TC所需要的全部配置。
在seata-1.3.0\script\config-center\nacos
中有一个脚本nacos-config.sh
则是将config.txt中的全部配置自动推送到nacos中,运行下面命令:
# -h 主机,你可以使用localhost,-p 端口号 你可以使用8848,-t 命名空间ID,-u 用户名,-p 密码
$ sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -t d -u nacos -w nacos
推送成功则可以在Nacos中查询到所有的配置,如下图:
修改TC的数据库连接信息
需要修改的配置如下:
## 采用db的存储形式
store.mode=db
## druid数据源
store.db.datasource=druid
## mysql数据库
store.db.dbType=mysql
## mysql驱动
store.db.driverClassName=com.mysql.jdbc.Driver
## TC的数据库url
store.db.url=jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true
## 用户名
store.db.user=root
## 密码
store.db.password=Nov2014
在nacos中搜索上述的配置,直接修改其中的值,比如修改store.mode
,如下图:
当然Seata还支持Redis作为TC的数据库,只需要改动以下配置即可:
store.mode=redis
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.password=123456
启动TC
按照上述步骤全部配置成功后,则可以启动TC,启动成功后,在Nacos的服务列表中则可以看到TC已经注册进入,如下图:
下单流程AT事务
案例流程链接:
HttpClient 远程调用支持
<!-- 实现对 Seata 的自动化配置 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!-- 实现 Seata 对 HttpClient 的集成支持 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-http</artifactId>
<version>1.3.0</version>
</dependency>
微服务版本支持
<!-- 引入 Spring Cloud Alibaba Seata 相关依赖,使用 Seata 实现分布式事务,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
</dependency>
<!-- 使用 seata 1.1.0 版本 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
扩展
Seata表字段详解
客户端
undo_log
在AT模式
中,需要在参与全局事务的客户端数据库中,添加一个undo_log
表,建表语句如下:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT '分支事务ID',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '全局事务ID',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '上下文',
`rollback_info` longblob NOT NULL COMMENT '回滚信息',
`log_status` int(11) NOT NULL COMMENT '状态,0正常,1全局已完成',
`log_created` datetime(6) NOT NULL COMMENT '创建时间',
`log_modified` datetime(6) NOT NULL COMMENT '修改时间',
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
在全局事务的一阶段中,分支事务在获取到全局锁提交事务时,会释放本地锁和连接资源,并在undo_log
表中插入一条数据。
各字段详细说明如下:
字段名 | 说明 |
---|---|
branch_id | 分支事务ID,比如:99302990136558270 |
xid | 全局事务ID ,比如:192.168.58.1:8091:99302990136558268(Seata 服务端地址+ ID) |
context | 回滚信息序列化和压缩格式,serializer=fastjson&compressorType=NONE,表示使用fastjson序列化,没有采用压缩 |
rollback_info | 回滚信息 该字段为longblob 类型(二进制数据) |
log_status | 日志状态,0正常,1全局已完成 |
log_created | 创建时间 |
log_modified | 修改时间 |
其中重要的是rollback_info
,比如在更新一条数据时set money = 97,会查询修改之前该条数据的及修改后的数据状态。
在Navicat 中,可以选中当前数据,然后通过查看选择文本方式展示。
通过Json 格式化工具,可以查看到详细信息。在rollback_info
中,该数据修改之前是98
:
修改之后,该数据是97
:
在第二阶段中,如果全局事务成功
,会收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录
。
在第二阶段中,如果全局事务失败
,会收到 TC 的分支回滚请求
,开启一个本地事务,执行如下操作。
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
- 数据校验:拿 UNDO LOG 中的后镜像与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句。
- 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
服务端
在服务端,需要存储事务会话信息
,支持以下几种方式:
- file本地文件(不支持HA),
- db数据库(支持HA)
- redis(支持HA)
其中本地文件方式,效率最高,但是不支持集群,而且出现问题时,是无法格式化的查看当前数据的,所以推荐使用数据库或者缓存的方式。
使用数据库模式时,需要创建以下三张表:
- global_table:全局事务
- branch_table:分支事务
- lock_table:全局锁
global_table
global_table
记录了全局事务的信息,建表语句如下:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for global_table
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '全局事务ID',
`transaction_id` bigint(20) NULL DEFAULT NULL COMMENT '事务ID',
`status` tinyint(4) NOT NULL COMMENT '状态',
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '应用ID',
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '事务分组名',
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '执行事务的方法',
`timeout` int(11) NULL DEFAULT NULL COMMENT '超时时间',
`begin_time` bigint(20) NULL DEFAULT NULL COMMENT '开始时间',
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '应用数据',
`gmt_create` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
比如,当前应用demo001
发起了一个全局事务,会在这个表中存入以下信息:
branch_table
branch_table
记录了分支事务的信息,建表语句如下:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL COMMENT '分支事务ID',
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '全局事务ID',
`transaction_id` bigint(20) NULL DEFAULT NULL COMMENT '全局事务ID,不带TC地址',
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源分组ID',
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源ID',
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '事务模式,AT、XA等',
`status` tinyint(4) NULL DEFAULT NULL COMMENT '状态',
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户端ID',
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '应用数据',
`gmt_create` datetime(6) NULL DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime(6) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of branch_table
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;
当一个服务调用另一个服务进行全局事务时,可以看到,在该表中插入了当前两个服务分支事务的相关信息,其中重要的有ID、事务模式、客户端地址、数据库连接地址等。
INSERT INTO `branch_table` VALUES (99302990136565280, '192.168.58.1:8091:99302990136565278', 99302990136565278, NULL, 'jdbc:mysql://127.0.0.1:3306/db_account', 'AT', 0, 'demo001:192.168.58.1:2116', NULL, '2022-01-25 16:56:58.092953', '2022-01-25 16:56:58.092953');
INSERT INTO `branch_table` VALUES (99302990136565283, '192.168.58.1:8091:99302990136565278', 99302990136565278, NULL, 'jdbc:mysql://127.0.0.1:3306/db_order', 'AT', 0, 'demo002:192.168.58.1:2617', NULL, '2022-01-25 16:56:58.551257', '2022-01-25 16:56:58.551257');
lock_table
lock_table
记录了锁相关的信息,建表语句如下:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '行键',
`xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '全局事务ID',
`transaction_id` bigint(20) NULL DEFAULT NULL COMMENT '全局事务ID,不带TC 地址',
`branch_id` bigint(20) NOT NULL COMMENT '分支ID',
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源ID',
`table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '表名',
`pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '主键对应的值',
`gmt_create` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`row_key`) USING BTREE,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
如下图:
AT模式执行流程解析
AT分布式事务大致过程:
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播;
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚决议;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
启动阶段
- 自动加载各种Bean及配置信息
- 初始化TM(事务管理器)
- 初始化RM(资源管理器)
- 初始化分布式事务客户端完成,代理数据源
- 连接TC(seata服务端),注册RM
- 连接TC(seata服务端),注册TM
- 扫描并动态代理开启了分布式事务的Bean
执行阶段
执行阶段整体机制分二阶段提交(2PC)
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成
- 回滚通过一阶段的回滚日志进行反向补偿。
在 AT 模式下,用户只需关注自己的业务SQL,用户的业务SQL 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
1. 一阶段TM 开启全局事务
使用了@GlobalTransactional
注解标识的方式执行时,因为进行了动态代理,会进入到拦截器GlobalTransactionalInterceptor
。
@GlobalTransactional(rollbackFor = Throwable.class, timeoutMills = 300000)
拦截器会获取到当前执行的类、方法、@GlobalTransactional
注解的属性。
接着事务管理器TM 会开启全局事务,和seata 服务端进行通信,获取到全局事务xid并绑定到当前线程RootContext
中,标记事务状态为开始。
此时在global_table 表中会插入一条全局事务信息。
2. 一阶段 TM 执行本地事务
开启全局事务后,进入到本地执行方法,执行业务逻辑。
因为Seata 对数据源进行了代理,所以在SQL 执行时,会进入到代理的PreparedStatement(PreparedStatementProxy),首先解析得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
接着进入到ExecuteTemplate,如果是SELECT操作,则不处理直接执行,其他操作(INSERT、UPDATE、DELETE、SELECT_FOR_UPDATE)会创建不同的SQL执行器。
比如 UPDATE操作 会创建UpdateExecutor执行器,执行器在执行方法时,会构建前后镜像,比如以下SQL语句:
# 修改当前账户的余额
UPDATE account_tbl SET user_id=?,money=? WHERE id=?
构建前置镜像时,会通过主键,查询当前数据更新前的状态。
SELECT id, user_id, money FROM account_tbl WHERE id = ? FOR UPDATE
前置镜像记录了更新前该记录的各个字段及对应的值。
前置镜像构建以后,执行正常业务操作,然后构建后置镜像,记录了更新后该记录的各个字段及对应的值。
接着会创建一个全局锁、undo_log回滚日志,全局锁创建时,以表名+主键名
为Key(eg:account_tbl:11111111),undo_log对应的实体类为SQLUndoLog
,都创建成功之后,RM 资源管理器会进行分支事务注册。
依然是远程请求TC进行分支事务注册。
在远程注册分支事务时,TC 会创建分支事务对象BranchSession
,并尝试获取当前记录的全局锁,在查询lock_table
时没有数据,这时会插入一条全局锁数据。
注册后,TC 会在branch_table 表中插入一条分支事务信息。
分支事务和全局锁插入成功后,分支事务调用 UndoLog 管理器,在当前本地数据库的undo_log 表中,插入一条回滚日志记录。
3. 一阶段 执行远程分支事务
若使用的是spring-cloud-starter-alibaba-seata
,自带了Feign 远程传递xid 的支持,在发起Feign 远程请求时,可以看到将xid 塞入了消息头中。
在被调用方,spring-cloud-starter-alibaba-seata
也提供了支持,使用Spring MVC 中的HandlerInterceptor
将消息头中的TX_XID
绑定到RootContext
中。
和之前一样,远程的分支事务,执行时因为远程这个方法是没有@GlobalTransactional注解的,所以不会进入到拦截器,但是代理了数据源,所以在执行SQL 时,还是会进入到代理的Executor中。
执行SQL 时,发现存在xid ,则表明这是一个分布式事务,则会进行分支事务注册处理,之后和第二步一样。
调用TC 注册分支事务时,TC 会查询数据库表中全局事务信息,创建分支事务,可以看到被调用方也生成了当前操作记录的全局锁。
最后,该远程服务也在TC 数据库中存储了自己的分支事务信息和全局锁信息。
4. 二阶段-提交
TM 事务管理器负责开始全局事务、提交或回滚全局事务。所以二阶段提交和回滚都是由发起全局事务的那个服务负责的,也是就使用了@GlobalTransactional
注解的服务。
二阶段的核心代码在TransactionalTemplate
类中:
try {
// 执行业务逻辑
rs = business.execute();
} catch (Throwable var17) {
ex = var17;
// 发生异常,全局回滚
this.completeTransactionAfterThrowing(txInfo, tx, var17);
throw var17;
}
// 无异常,全局提交
this.commitTransaction(tx);
ex = rs;
return ex;
没有异常,会进行全局提交,这里有个配置参数client.tm.commitRetryCount,提交重试次数,提交时依然是调用TC 服务端,提交失败,会重试默认5次。
TC 收到TM全局提交请求后,会删除分支事务全局锁记录,开启提交事件发送请求到各分支事务,各分支收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
5.二阶段-回滚
TM 所在服务发起远程调用发生异常时,会向TC 发送回滚请求,大致流程和全局提交一致。
各个分支事务收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
- 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句: update product set name = ‘TXC’ where id = 1;
- 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。