分布式事务—Seata
简介
1、什么是 Seata
seata 是事务,是保持一致性的,在高并发情况下不安全,需要加分布式锁。
2、Seata 的分布式事务机制
如何才能做到不对业务进行侵入的情况下实现分布式事务呢?
Seata客户端是通过对数据源进行代理实现的,使用的是 DataSourceProxy类,所以在程序这边,我们只需要将对应的代理类注册为 Bean即可。
2.1、架构图
-
RM(Resource Manager):资源管理者,管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。用于直接执行本地事务的提交和回滚。
-
TM(Transaction Manager):事务管理者,定义全局事务的范围、开始全局事务、提交或回滚全局事务。TM 是分布式事务的核心管理者。比如现在我们需要在借阅服务中开启全局事务,来让其自身、图书服务、用户服务都参与进来,也就是说一般全局事务发起者就是 TM。
-
TC(Transaction Coordinator):事务协调者,维护全局和分支事务的状态,协调全局事务提交或回滚,这个就是我们的 Seata服务器,用于全局控制,比如在 XA模式下就是一个协调者的角色,而一个分布式事务的启动就是由 TM 向 TC 发起请求,TC 再来与其他的 RM 进行协调操作。 TM 请求 TC,从而开启一个全局事务,TC 会生成一个 XID 作为该全局事务的编号,XID 会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起;
RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联;
TM 请求 TC,告知 TC 对应 XID 的全局事务是进行提交还是回滚;然后 TC 驱动 RM 将对应 XID 的自己的本地事务进行提交还是回滚;

2.2、Seata 支持 4种事务模式
官方文档:Seata 是什么? | Apache Seata
2.2.1、AT模式
AT模式同样是分阶段提交的事务模型,不过缺弥补了 XA模型中资源锁定周期过长的缺陷。即 2PC模式的升级版。
一阶段,Seata 会拦截“业务SQL”,首先解析 SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段如果确认提交的话,因为“业务SQL”在一阶段已经提交至数据库, 所以 Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。当然如果发送错误,需要回滚,那么就用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和“after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
-
阶段一RM的工作:
-
注册分支事务
-
记录 undo_log(数据快照,分为前镜像 before image 和后镜像 after image)
-
执行业务sql 并提交
-
报告事务状态
-
阶段二提交时RM的工作:
-
删除 undo_log 即可
-
阶段二回滚时RM的工作:
-
根据 undo_log 恢复数据到更新前


2.2.2、TCC模式
-
Try:资源的检测和预留;
-
Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
-
Cancel:预留资源释放,可以理解为try的反向操作。

2.2.3、XA模式
要求数据库支持 XA模式才可以。
-
RM一阶段工作:
-
注册分支事务到 TC
-
执行业务SQL 但不提交
-
向 TC 报告事务状态
-
TC二阶段工作:
-
检查各分支事务执行状态,如果都成功,则通知所有 RM 提交事务;如果有失败,则通知所有 RM 回滚事务
-
RM三阶段工作:
-
接收 TC 的命令,提交或回滚事务

2.2.4、Saga模式
Sega模式用于处理长事务,每个执行者需要实现事务的正向操作和补偿操作。也分成两阶段。
-
一阶段:直接提交本地事务
-
二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

优点:
-
事务参与者可以基于事件驱动实现异步调用,吞吐高
-
一阶段直接提交事务,无锁,性能好
-
不用编写 TCC 中的三个阶段,实现简单 缺点:
-
软状态持续时间不确定,时效性差
-
没有锁,没有事务隔离,会有脏写
2.2.5、四种模式对比
| XA | AT | TCC | SAGA | |
|---|---|---|---|---|
| 一致性 | 强一致 | 弱一致 | 弱一致 | 最终一致 |
| 隔离性 | 完全隔离 | 基于全局锁隔离 | 基于资源预留隔离 | 无隔离 |
| 代码侵入 | 无 | 无 | 有,需编写三个接口 | 有,需编写状态机和补偿业务 |
| 性能 | 差 | 好 | 非常好 | 非常好 |
| 场景 | 对一致性、隔离性有高要求的业务 | 基于关系型数据库的大多数分布式事务场景 | 对性能要求较高的事务,<br>有非关系型数据库要参与的事务。 | 业务流程长或多,<br>参与者包含其他公司或遗留系统服务,无法提供 TCC模式的三个接口 |
2.3、事务分组机制
事务分组就是 seata 的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。
集群:seata-server服务端一个或多个节点组成的集群cluster。 应用程序(客户端)使用时需要指定 事务逻辑分组 与 Seata服务端集群(默认为default)的映射关系。
2.3.1、设置事务分组再映射到集群的原因
为啥要设计成通过事务分组再直接映射到集群?干嘛不直接指定集群呢?为什么是获取事务分组到映射集群的配置呢?
这样设计后,事务分组可以作为资源的逻辑隔离单位,出现某集群故障时可以快速 failover,只切换对应分组,可以把故障缩减到服务级别,但前提也是你有足够 server集群。
3、下载 Seata
Seata服务端下载:Releases · apache/incubator-seata · GitHub
源码:https://github.com/seata/seata/archive/refs/heads/develop.zip
3.1、部署
普通启动类修改环境变量是在 环境 -> 环境变量 写入 server.port=9999
Seata服务端支持本地部署或是基于注册发现中心部署(比如 Nacos、Eureka 等)。
本地部署的话不需要对 Seata 的配置文件做任何修改。
3.1.1、文件模式部署
下载完之后,可以直接双击 seata\seata-server-1.4.2\bin\seata-server.bat 运行。也可以放进 idea 中配置端口运行。
-
把文件夹放入 idea 中
-
下拉选择 Edit Configuration(编辑配置...),
-
点击 + 号,选择 Shell Script
-
名字改为 Seata Server;脚本路径选择 seata\seata-server-1.4.2\bin\seata-server.bat 的路径;脚本选项 -p 8868,默认端口是 8091;解释器路径 /bin/bash

3.1.2、nacos模式部署
-
Seata 需要在 nacos 的一个命名空间下,当然也可以为 Seata 专门创一个命名空间 seata
-
修改 seata\seata-server-1.4.2\conf\registry.conf,
# 注册
registry {
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "要填写的是命名空间ID"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
# 配置
config {
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "要填写的是命名空间ID"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
-
然后我们需要把配置导入到 nacos 中,方便做管理和更新。源码的 script/config-center/nacos 下有上传文件,但是没有 Windows 下的 .bat文件,Linux 下使用 .sh文件。tenant 就是 nacos 的命名空间ID。WIndows 下的配置上传 nacos 可以参考 seata 整合 nacos(windows)_seata整合nacos-CSDN博客
-
因为修改了 Seata的部署方式,所以需要修改微服务的配置文件。从上面的旧方式变成下面的新方式。

spring: cloud: nacos: discovery: server-addr: localhost:8848 seata: # 注册 registry: # 使用 Nacos,默认的是 file type: nacos nacos: # 使用 Seata 的命名空间,这样才能正确找到 Seata服务, # SEATA_GROUP,是服务端的配置文件中的默认值,就不用配了 # group: namespace: 89fc2145-4676-48b8-9edd-29e867879bcb username: nacos password: nacos # 配置 config: type: nacos nacos: namespace: 89fc2145-4676-48b8-9edd-29e867879bcb username: nacos password: nacos
-
因为 Seata客户端都添加了配置 service: vgroup-mapping,这是事务组和集群的映射,现在我们需要在 nacos 上添加对应的事务组映射配置,DataId 的格式为service.vgroupMapping.事务组名称。每一个微服务都需要添加一个单独的 nacos配置。

3.2、修改事务会话信息的存储方式
默认使用的是 file方式,会自动创建 file_store、sessionStore文件夹。
如果使用的是 nacos模式部署,就修改配置文件中的 store.mode、store.session.mode,配置内容都改成 db。
3.3、修改数据库信息的配置并创建数据库
3.3.1、修改配置
要修改 数据库驱动、数据库URL、数据库用户名密码,
如果使用的是 nacos模式部署,就修改配置文件中的:
store.db.driverClassName配置文件 配置内容:com.mysql.cj.jdbc.Driver store.db.url配置文件 配置内容:改成 Seata对应的数据库 store.db.user、store.db.password 这两个配置文件
3.3.2、创建库表
3.3.2.1、创建语句
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('HandleAllSession', ' ', 0);
3.3.2.2、修改字段长度
如果 Seata服务端出现报错,可能是我们自定义事务组的名称太长了,将 globle_table表的字段transaction_server_group 长度适当增加一下即可。
4、简单使用
4.1、实例1
这样的 Seata,在高并发下不安全。可以考虑加锁或使用消息中间件。
4.1.1、将各个服务作为 Seata 的客户端
在每个服务中都添加以下设置。
4.1.1.1、引入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
4.1.1.2、添加配置
引入依赖、添加配置之后,启动就可以让服务连接到 Seata服务器了,但是还没有开启分布式事务。
seata: # 自定义分组名称 # tx-server-group: service: vgroup-mapping: # 对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到 default集群 # 配置错误会找不到服务,所以要注意所在微服务项目的服务名称(应用名称) book-service-seata-service-group: default grouplist: default: localhost:8868
4.1.1.3、启动类添加注解
可以通过配置文件让 Seata 进行自动代理,但是好像写上了配置但不生效。
这样启动后,微服务的 Seata客户端就会产生 RM、TM 去注册到 Seata服务端的 TC 上。
@EnableAutoDataSourceProxy
@SpringBootApplication
public class BookApplication {
public static void main(String[] args) {
SpringApplication.run(BookApplication.class, args);
}
}
4.1.1.4、对业务方法添加注解,开启分布式事务
在 impl类的需要开启分布式事务的方法上,添加 @GlobalTransactional注解,那么就会保证这个方法的正确执行。RootContext.getXID() 可以得到一致的 XID。
@GlobalTransactional
@Override
public boolean doBorrow(int uid, int bid) {
// 这里打印一下 XID 看看,如果打印的是同一个XID,表示使用的就是同一个事务
System.out.println(RootContext.getXID());
// 业务处理,远程调用
// 若抛出异常,就会进行回滚
return true;
}
4.1.1.5、创建 undo_log数据表
因为默认使用 XA模式,Seata 会分析修改数据的 sql语句,同时生成对应的反向回滚SQL,这个回滚记录会存放在 undo_log表中。所以要求每一个 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;
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)