一、数据库中添加回滚日志表UNDO_LOG:
- UNDO_LOG必须在每个业务数据库中出现,用于保存回滚操作数据。
- 当全局事务提交时,对应的UNDO_LOG记录直接删除。
- 当全局事务回滚时,通过该表回滚到以前的数据,并删除UNDO_LOG记录。
Seata的UNDO_LOG表和数据库的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;
二、搭建Seata的TC
1.在官网下载seata-server包,解压。下载地址
2.tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,你需要提前创建好这些表,创建一个seata的数据库,新增如下表。
- global_table:全局事务表
- branch_table:分支信息表
- lock_table:加锁的表
-- 全局事务表--
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_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8;
-- 事务分支表 --
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 = utf8;
-- 锁定表--
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),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8;
-- seata新版本加的锁表--
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 ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
前面的UNDO_LOG表是加在业务数据库中的。每个业务数据库都要有UNDO_LOG
seata1.4.2之后,需要回滚的表日期类型不能使用datetime,可以使用timestamp
3.修改seata服务的配置文件file.conf(可以不做这一步,可以将这些配置放在远程配置中心)
4.修改配置文件registry.conf
这里的namespace和group项一定要和nacos的一致
5. 在registry.conf配置中还可以修改Seata-server的配置中心,默认是以file文件进行存储的,也就是我们上面设置的file.conf,现在我顺便把配置放到nacos上
#config部分
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "a971c0d9-771c-4c21-b4f3-3c04c499d831"
cluster = "default"
username = "nacos"
password = "nacos"
}
6.下载nacos-config脚本和config.txt,点击进入下载页,这两个就相当于seata的全部配置,比file.conf更全面。
-
nacos-config.sh和nacos-config.py选择一个:在seata目录下新建 script 目录,将
nacos-config.sh
放入script
目录下 -
config.txt:该文件存放在将seata目录下,与
conf、lib
目录同级,seata的非常全的配置内容,可通过nacos-config.sh
脚本推送到nacos
配置中心
-
修改
config.txt
的内容
- 打开
git bash
或linux
类命令行,执行sh脚本(注意脚本是否有执行的权限),导入config.txt的配置到nacos配置中心里
# -h 主机,你可以使用localhost,-p 端口号 ,-t 命名空间ID,-u 用户名,-w 密码
sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA-GROUP -t a971c0d9-771c-4c21-b4f3-3c04c499d831 -u nacos -w nacos
seata配置文件非常之多,建议新建一个命名空间单独存放
7.启动seata-server.bat,并查看nacos服务,若seata服务注册成功,表示注册中心和配置中心成功
三、微服务集成Seata-Server
我们的代码逻辑为:用户购买某件商品,首先查看库存是否充足,如果充足的话,再远程调用订单服务,在数据库中新建一个订单。
扣减库存的service代码如下:
创建订单的service代码如下:
Seata执行流程(AT模式)
事务执行成功的情况:
事务执行失败的情况:
要点说明:
1.每个RM使用DataSourceProxy连接数据库,其目的是使用ConnectionProxy,使用数据源和数据连接代理的目的就是在第一阶段将UNDO_LOG和业务数据放在一个本地事务提交,这样就保存了只要有业务操作就一定有UNDO_LOG。
2.在第一阶段UNDO_LOG中存放了数据修改前和修改后的值,为事务回滚作好准备,所以第一阶段完成就已经将分支事务提交,也就释放了锁资源。
3.TM开启全局事务开始,将XID全局事务id放在事务上下文中,通过feign调用也将XID传入下游分支事务,每个分支事务将自己的Branch ID分支事务ID与XID关联。
4.第二阶段全局事务提交,TC会通知各个分支参与者提交分支事务,在第一阶段就已经提交了分支事务,这里各参与者只需要删除UNDO_LOG即可,并且可以异步执行,第二阶段很快可以完成。
5.第二阶段全局事务回滚,TC会通知各个分支参与者回滚分支事务,通过XID和Branch lD找到相应的回滚日志,通过回滚日志生成反向的SQL并执行,以完成分支事务回滚到之前的状态,如果回滚失败则会重试回滚操作。
Seata工程构建
1.根据前面的步骤将我们的Seata-Server项目启动起来,也就是启动TC
2.在需要使用到分布式事务的微服务中引入Seata依赖,也就是注册TM和RM
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!-- 版本较低,排除-->
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 使用高版本 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
3.配置微服务配置文件,让微服务能通过注册中心找到seata-server
seata:
enabled: true
application-id: ${spring.application.name}
#事务群组(可以每个应用独立取名,也可以使用相同的名字),根据这个获取tc服务的cluster名称。事务组=全局事务
#"default_tx_group"是默认值,如果改了这里,server配置下的 `server.vgroupMapping.xxxx`也要跟着一起改,否则会注册不到 “seata-server”导致报错。
tx-service-group: default_tx_group
config:
type: nacos
#需要和server在同一个注册中心下
nacos:
namespace: a971c0d9-771c-4c21-b4f3-3c04c499d831
server-addr: 127.0.0.1:8848
#需要和server端的registry、config一致
group: SEATA-GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
#需要和server端保持一致,即server在nacos中的名称,默认为seata-server
application: seata-server
server-addr: 127.0.0.1:8848
group: DEFAULT_GROUP
namespace: public
username: nacos
password: nacos
4.TM方法上加上@GlobalTransactional注解。
测试
1.订单服务出错回滚全局事务
如果我们将订单服务代码改为:
那么只会扣减库存,而不会生成订单。
这两个不同的服务都存在本地事务,在本地事务中如果出现异常,那么就会回滚本地事务,且controller层会捕捉异常,不让异常抛给用户,所以订单服务即使发生异常回滚了,扣减库存的服务是感知不到的。
现在我们将更改扣减库存的服务,使得订单创建失败,也不会扣减库存
可以使用状态码来判断远程调用是否成功或失败,判断是否需要提交或回滚
测试后,库存和订单数据都一致。因为全局事务回滚了。
这个例子不是很明显,因为即使是本地事务回滚,也会将扣减库存的服务回滚,发挥不了分布式事务的优势。请看下面的例子。
2.扣减库存服务出错回滚订单创建
将创建订单的服务变为正常的:
扣减库存的服务代码最后会抛出一个异常:
如果没有分布式事务,那么就会发生库存没扣,但是订单却创建了的情况。但我们现在有全局事务,如果发生错误,TC会将所有的RM回滚,包括远端的服务。
如下这是订单服务的结果:
虽然它本来在本地已经创建了一个订单,且提交了,但是TC在检测到一个RM发生错误时就会通知它,让它根据UNDO_LOG进行回滚操作。
总结:
- 在需要发起全局事务的service方法上添加注解 @GlobalTransactional
- 使用Fiegn、restTemplate等方式发送请求,提供方只添加@Transactional保证本地事务
注意问题:
1.seata通过线程变量XID(RootContext.getXID() )判断TC与RM是否在同一事务下,现支持使用restTemplate与Feign方式发送请求自动携带xid到被调用方,使用其他方式可将xid放入请求头中,key为”TX_XID“,RM会在请求头中自动获取。若采用其他方式需自行保证xid的传递。
2.被调用方产生异常却没有回滚:当被调用方RM产生异常时,为了调用方TM可以正确接收到异常状态码,使Feign能抛出异常发起全局事务回滚,RM最好不要添加异常处理去拦截异常。
Seata的TCC模式
TCC模式主要是要注意幂等性、悬挂、空回滚这三个问题。使用TCC就需要改变我们的原先的代码逻辑,我们需要先构思出整体的流程:
扣减库存的服务:
try:
悬挂校验
校验库存
减库存
confirm:
无
cancel:
幂等校验
空回滚处理
加上库存
创建订单服务:
try:无
confirm:
幂等校验
创建订单
cancel:无
@LocalTCC
public interface ProductService {
@TwoPhaseBusinessAction(name = "buy", commitMethod = "confirm", rollbackMethod = "cancel")
boolean buy(@BusinessActionContextParameter(paramName = "id") Integer id);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
具体的流程比较复杂,可以新建几个表来解决幂等性、空回滚、悬挂问题