Seata简介
简介
Seata 是⼀款开源的分布式事务框架。致⼒于在微服务架构下提供⾼性能和简单易⽤的分布式事务服务。
在 Seata 开源之前,Seata 对应的内部版本在阿⾥经济体内部⼀直扮演着分布式⼀致性中间件的⻆⾊,帮助经济体平稳的度过历年的双11,对各业务单元业务进⾏了有⼒的⽀撑。经过多年沉淀与积累,商业化产品先后在阿⾥云、⾦融云进⾏售卖。
2019.1 为了打造更加完善的技术⽣态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。
Seata:seata.io/zh-cn/index…
seata的github地址:github.com/seata/seata
特色模块
-
微服务框架⽀持 ⽬前已⽀持 Dubbo、Spring Cloud、Sofa-RPC、Motan 和 grpc 等RPC框架,其他框架持续集成中
-
AT 模式 提供⽆侵⼊⾃动补偿的事务模式,⽬前已⽀持 MySQL、 Oracle 、PostgreSQL和 TiDB的AT模式
-
TCC 模式 ⽀持 TCC 模式并可与 AT 混⽤,灵活度更⾼
-
SAGA 模式 为⻓事务提供有效的解决⽅案
-
XA 模式 ⽀持已实现 XA 接⼝的数据库的 XA 模式
-
⾼可⽤ ⽀持基于数据库存储的集群模式,⽔平扩展能⼒强
Seata产品模块
Seata 中有三⼤模块,分别是 TM、RM 和 TC。其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在⼀起,TC 作为 Seata 的服务端独⽴部署。
TC (Transaction Coordinator) - 事务协调者 维护全局和分⽀事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器 定义全局事务的范围:开始全局事务、提交或回滚全局事务
RM (Resource Manager) - 资源管理器 管理分⽀事务处理的资源,与TC交谈以注册分⽀事务和报告分⽀事务的状态,并驱动分⽀事务提交或回滚。
在 Seata 中,分布式事务的执⾏流程:
- TM 开启分布式事务, TM会 向 TC 注册全局事务记录;
- 操作具体业务模块的数据库操作之前, RM 会向 TC 注册分⽀事务;
- 当业务操作完事后.TM会通知 TC 提交/回滚分布式事务;
- TC 汇总事务信息,决定分布式事务是提交还是回滚;
- TC 通知所有 RM 提交/回滚 资源,事务⼆阶段结束。
Seata-AT模式
案例引入及问题剖析
1、在这里我们模拟一个常见的商城下订单业务,首先构建一个小型工程,分别构建业务微服务,订单微服务,积分微服务,库存微服务
调用业务微服务时,通过feign分别发起远程调用,依次调用添加订单,添加积分,扣除库存操作
2、执⾏初始化SQL脚本,⾸先创建4个数据库
seata_bussiness/seata_order/seata_points/seata_storage,在各⾃数据库执⾏SQL脚本
订单表
CREATE TABLE `t_order` (
`id` bigint(20) NOT NULL COMMENT '订单id',
`goods_Id` int(11) DEFAULT NULL COMMENT '商品ID',
`num` int(11) DEFAULT NULL COMMENT '商品数量',
`money` decimal(10,0) DEFAULT NULL COMMENT '商品总金额',
`create_time` datetime DEFAULT NULL COMMENT '订单创建时间',
`username` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '用户名称',
`status` int(11) DEFAULT NULL COMMENT '\r\n订单状态-0不可用,事务未提交 , 1-可用,事务提交',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;
积分表
CREATE TABLE `t_points` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '积分ID',
`username` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '用户名',
`points` int(11) DEFAULT NULL COMMENT '用户积分',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;
库存表
CREATE TABLE `t_storage` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '库存ID',
`goods_id` int(11) DEFAULT NULL COMMENT '商品ID',
`storage` int(11) DEFAULT NULL COMMENT '库存量',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;
INSERT INTO `t_storage` VALUES (1, 1, 100);
3、案例测试 依次将4个服务启动.和nacos服务
访问路径为:http://localhost:8000/test1 正常访问数据分别⼊库
订单增加
积分增加
库存扣除
http://localhost:8000/test2 访问出错库存不⾜,导致服务调⽤失败.则观察数据库, 经发现订单与积分数据库都已改变,⽽库存数据库没有减少库存, 所以不满⾜事务的特性.
即使库存扣除失败,也是增加了一条订单信息
积分增加
库存因为抛出异常了,所以还是90
AT模式介绍
AT 模式是⼀种⽆侵⼊的分布式事务解决⽅案。在 AT 模式下,⽤户只需关注⾃⼰的“业务 SQL”,⽤户 的 “业务 SQL” 作为⼀阶段,Seata 框架会⾃动⽣成事务的⼆阶段提交和回滚操作。
AT模式原理
在介绍AT 模式的时候它是⽆侵⼊的分布式事务解决⽅案, 那么如何做到对业务的⽆侵⼊的呢?
-
- ⼀阶段
在⼀阶段,Seata 会拦截“业务 SQL”,⾸先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执⾏“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后⽣成⾏锁。以上操作全部在⼀个数据库事务内完成,这样保证了⼀阶段操作的原⼦性。
-
- ⼆阶段
- 提交 ⼆阶段如果是提交的话,因为“业务 SQL”在⼀阶段已经提交⾄数据库, 所以 Seata 框架只需将⼀阶段保存的快照数据和⾏锁删掉,完成数据清理即可。
- 回滚 ⼆阶段如果是回滚的话,Seata 就需要回滚⼀阶段已经执⾏的“业务 SQL”,还原业务数据。回滚⽅式便是⽤“before image”还原业务数据;但在还原前要⾸先要校验脏写,对⽐“数据库当前业务数据”和 “after image”,如果两份数据完全⼀致就说明没有脏写,可以还原业务数据,如果不⼀致就说明有脏写,出现脏写就需要转⼈⼯处理。
AT 模式的⼀阶段、⼆阶段提交和回滚均由 Seata 框架⾃动⽣成,⽤户只需编写“业务SQL”,便能轻松接⼊分布式事务,AT 模式是⼀种对业务⽆任何侵⼊的分布式事务解决⽅案。
AT模式改造案例
Seata Server - TC全局事务协调器
介绍了 seata 事务的三个模块:TC(事务协调器)、TM(事务管理器)和RM(资源管理器),其中 TM 和 RM 是嵌⼊在业务应⽤中的,⽽ TC 则是⼀个独⽴服务。
Seata Server 就是 TC,直接从官⽅仓库下载启动即可,下载地址:github.com/seata/seata…
这里我们下载 seata-server-1.3.0.zip
registry.conf
这里我们打开一下conf目录,重点看registry.conf文件
可以看到配置文件中包含两部分
- registry:服务中心的设置,可选方式有:nacos,eurake,redis,zk,consul等
- config:配置文件的设置,可选方式有:nacos,eurake,redis,zk,consul等
在这里,我们使用nacos注册中心,所以我们得在registry.conf文件中进行配置
-
选择使用nacos注册中心,并且填写用户名密码
-
选择使用nacos作为配置中心,并且填写用户名密码
file.config
file.conf文件,配置的是TC端,也就是事务协调器的数据存储模式,可选file,db,redis 这里我们选择使用db,这里我们配置好数据库的账号名,密码
向nacos中添加配置信息
后面在nacos中,需要配置seata的相关参数,seata的相关配置信息在config-center中
- 下载配置config.txt github.com/seata/seata…
- 针对每个⼀项配置,可以去seata官网查看一下 seata.io/zh-cn/docs/…
-
将config.txt⽂件放⼊seata⽬录下⾯
-
修改config.txt信息 Server端存储的模式(store.mode)现有file,db,redis三种。主要存储全局事务会话信息,分⽀事务信息, 锁记录表信息,seata-server默认是file模式。file只能⽀持单机模式, 如果想要⾼可⽤模式的话可以切换db或者redis. 为了⽅便查看全局事务会话信息本次采⽤db数据库模式,打开config.txt文件进行如下修改
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
-
创建seata数据库
-
需要创建global_table/branch_table/lock_table三张表 github.com/seata/seata…
-- the table to store GlobalSession data
-- the table to store GlobalSession data
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 CHARSET = utf8;
-- the table to store BranchSession data
-- 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 CHARSET = utf8;
-- the table to store lock data
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table` (
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`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 CHARSET = utf8;
我们准备好了配置信息后,如何批量上传到nacos中?这里官方提供了一个工具,可以把config.txt中的配置文件,全部上传到nacos中
-
下载地址:github.com/seata/seata… 下载nacos-config.sh文件,此文件⽤于向 Nacos 中添加配置
-
将nacos-config.sh放在seata/conf⽂件夹中
- 打开git bash here 执⾏nacos-config.sh,需要提前将nacos启动
sh nacos-config.sh -h 127.0.0.1
登录nacos查看配置信息
启动seata Server
双击seata-server.bat时,会一闪而过,如果使用命令行方式打开,可以看到如下错误:
这是因为seata是依赖于jdk1.8的,所以需要把环境切换成jdk.18
登录nacos查看服务列表信息
TM/RM整合Seata
UNDOLOG表
AT 模式在RM端【资源端】需要 UNDO_LOG 表,来记录每个RM的事务信息,主要包含数据修改前,后的相关信息,⽤于回滚处理,所以在所有数据库中分别执⾏
-- 注意此处0.3.0+ 增加唯⼀索引 ux_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的资源逻辑,类似于服务实例。在file.conf中的my_test_tx_group就是一个事务分组。
通过事务分组如何找到后端集群?
- 首先程序中配置了事务分组(GlobalTransactionScanner 构造方法的txServiceGroup参数)
- 程序会通过用户配置的配置中心去寻找service.vgroupMapping .[事务分组配置项],取得配置项的值就是TC集群的名称
- 拿到集群名称程序通过一定的前后缀+集群名称去构造服务名,各配置中心的服务名实现不同
- 拿到服务名去相应的注册中心去拉取相应服务名的服务列表,获得后端真实的TC服务列表
为什么这么设计,不直接取服务名?
这里多了一层获取事务分组到映射集群的配置。这样设计后,事务分组可以作为资源的逻辑隔离单位,出现某集群故障时可以快速failover,只切换对应分组,可以把故障缩减到服务级别,但前提也是你有足够server集群。
说了这么多究竟是什么意思呢?下面用图例解释下
假如这里有两个seata集群,并且进行了分组,group1,group2,当group2组所对应的集群发生故障时,可以直接改配置名字即可,这样系统就会根据配置直接使用新的集群,达到快速故障转移的效果。
还可以对同一集群下不同服务器节点进行分组隔离
TM/RM端整合seata
下面是整合的流程图
这里重点讲解一下两个配置文件
在工程中,需要添加seata事务分组配置,这里使用的名称为:my_test_tx_group
-
nacos的会默认有一条记录配置,以seata事务分组名为后缀,service.vgroupMapping.
my_test_tx_group,此配置文件里面的值为default -
另外一个配置文件,就以值为配置的Key,也就是service.
default.grouplist,在这个配置中,就是真正的seata地址127.0.0.1:8091
总而言之:在项目中添加seata依赖,添加conf文件,配置事务分组名称,在启动时,就会根据事务分组名称,在nacos绕一圈后,获得实际的seata信息,然后注册到TC中,在调用业务端增加@GlobalTransactional注解,在RM端设置数据源代理。
第一步:⼯程中添加Seata依赖
lagou_parent添加seata依赖管理,⽤于seata的版本锁定
<!--SCA -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--seata版本管理, ⽤于锁定⾼版本的seata -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.3.0</version>
</dependency>
在lagou_common_db⼯程添加seata依赖
<!--seata依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<!--排除低版本seata依赖-->
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加⾼版本seata依赖-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.3.0</version>
</dependency>
第二步:在common⼯程添加registry.conf依赖
其实就是把seata的config目录下的registry.conf文件,复制过来
第三步:添加公共配置
修改commondb模块的配置文件,先重命名为:application-seata.properties
然后增加配置,说明在seata中,使用的事务分组名称为:my_test_tx_group
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group
logging.level.seata=debug
第四步:在每个模块下引⼊公共配置⽂件
第五步:在TM端开启全局事务,添加注解@GlobalTransactional
在整个项目中,bussiness项目是业务端,所以是TM端,所以替换原先的@Transactional,使用@GlobalTransactional
第六步:编译数据源代理
- 在common_db中 新建一个DataSourceConfiguration对象
- 使用使⽤druid连接池注入datasource对象
- 使用seata的DataSourceProxy对象,对druid对象进行包装成为代理
@Configuration
public class DataSourceConfiguration {
/**
* 使⽤druid连接池
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
@Primary //设置⾸选数据源对象
@Bean("dataSource")
public DataSourceProxy dataSource(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
然后在每个项目的启动类中,都排除默认注入的DataSource,然后扫描指定的包
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class,
scanBasePackages = "com.lagou")
第七步:运行所有的微服务模块
在控制台中,我们可以看到一些关键信息,注册了nacos了,并且从nacos中,取出了seata的服务地址 端口8091
在注册的过程中,seata控制台中,可以看到注册的信息,首先进行的是RM的注册,每个微服务都作为一个资源管理服务,然后进行TM的注册
第八步:测试
1、先直接使用@Transactional,然后发起postman请求,看看是什么情况
发现,虽然最后扣除库存失败了,但是订单还是下了,积分还是给了
2、再次恢复一下原始数据,然后开启全局事务,然后发起postman请求,看看是什么情况
我们首先把断点断到库存减少的地方
当我们开启事务后,我们查看TC协调器的三张表的情况
- global_table 全局表
- 此表中生成了全局事务ID:xid
- aplicationId为 调用方服务名称
- 记录了事务分组名称
- branch_table
- 在此案例中,因为有两个RM资源端所以在有两条 branch_table记录
- resource_id 分别记录了 order和point,两个TM把自己注册到了TC端
-
lock_table
- lock_table也记录了两条记录,分别代表着order库,point库的某一条记录在全局被锁住了
-
此表是一张记录锁表的详情,row_key 是一个全局锁,详情说明seata_order库中的t_order表中的一条记录被锁住了不能操作
然后观察订单表,积分表的undo_log日志记录信息
其中rollback_info的内容为:
当库存服务报错时,此次事务全部回滚
库存微服务的控制台清晰的打印出,把当前事务进行回滚提交
Seata-TCC模式
TCC模式介绍
Seata 开源了 TCC 模式,该模式由蚂蚁⾦服贡献。TCC 模式需要⽤户根据⾃⼰的业务场景实现Try、Confirm 和 Cancel 三个操作;事务发起⽅在⼀阶段 执⾏ Try ⽅式,在⼆阶段提交执⾏ Confirm⽅法,⼆阶段回滚执⾏ Cancel ⽅法。
TCC 三个⽅法描述:
- Try:资源的检测和预留;
- Confirm:执⾏的业务操作提交;要求 Try 成功 Confirm ⼀定要能成功;
- Cancel:预留资源释放。
业务模型分 2 阶段设计:
⽤户接⼊ TCC ,最重要的是考虑如何将⾃⼰的业务模型拆成两阶段来实现。
以“扣钱”场景为例,在接⼊ TCC 前,对 A 账户的扣钱,只需⼀条更新账户余额的 SQL 便能完成;但是在接⼊ TCC 之后,⽤户就需要考虑如何将原来⼀步就能完成的扣钱操作,拆成两阶段,实现成三个⽅法,并且保证⼀阶段 Try 成功的话 ⼆阶段 Confirm ⼀定能成功。
Try ⽅法作为⼀阶段准备⽅法,需要做资源的检查和预留。在扣钱场景下,Try 要做的事情是就是检查账户余额是否充⾜,预留转账资⾦,预留的⽅式就是冻结 A 账户的 转账资⾦。Try ⽅法执⾏之后,账号 A 余额虽然还是 100,但是其中 30 元已经被冻结了,不能被其他事务使⽤。
⼆阶段 Confirm ⽅法执⾏真正的扣钱操作。Confirm 会使⽤ Try 阶段冻结的资⾦,执⾏账号扣款。Confirm ⽅法执⾏之后,账号 A 在⼀阶段中冻结的 30 元已经被扣除,账号 A 余额变成 70 元 。
如果⼆阶段是回滚的话,就需要在 Cancel ⽅法内释放⼀阶段 Try 冻结的 30 元,使账号 A 的回到初始状态,100 元全部可⽤。
⽤户接⼊ TCC 模式,最重要的事情就是考虑如何将业务模型拆成 2 阶段,实现成 TCC 的 3 个⽅法,并且保证 Try 成功 Confirm ⼀定能成功。相对于 AT 模式,TCC 模式对业务代码有⼀定的侵⼊性,但是 TCC 模式⽆ AT 模式的全局⾏锁,TCC 性能会⽐ AT 模式⾼很多。
TCC模式改造案例
RM端改造
针对RM端,实现起来需要完成try/commit/rollback的实现,所以步骤相对较多但是前三步骤和AT模式⼀样
- 修改数据库表结构,增加预留检查字段,⽤于提交和回滚
ALTER TABLE `seata_order`.`t_order` ADD COLUMN `status` int(0) NULL
COMMENT '订单状态-0不可⽤,事务未提交 , 1-可⽤,事务提交' ;
ALTER TABLE `seata_points`.`t_points` ADD COLUMN `frozen_points` int(0)
NULL DEFAULT 0 COMMENT '冻结积分' AFTER `points`;
ALTER TABLE `seata_storage`.`t_storage` ADD COLUMN `frozen_storage` int(0)
NULL DEFAULT 0 COMMENT '冻结库存' AFTER `goods_id`;
- lagou_order⼯程改造
修改orderService接口,在接口添加注解@LocalTcc,标志着此接口被Seata管理,根据事务状态完成事务的提交或者回滚
- @TwoPhaseBusinessAction:此注解代表着当前方法是两阶段业务,配置当前事务的名称,提交方法名称,回滚方法名称
- @BusinessActionContextParameter(paramName = "order"):代表着当前事务传输的对象名称是order
在预提交部分,创建一个订单,然后设置订单的状态为 try阶段
在提交部分,接收上下文中context中的order数据,并且修改状态,进行提交
在回滚部分,接收上下文中context中的order数据,并且进行删除
在这里测试一下,我们先注释积分和库存的微服务,保留订单微服务
使用postman发起销售流程
通过断点可以发现,订单分别经过了try阶段方法和commit阶段方法,通过控制台可以看出,RM端向TC端注册了事务branch,且返回了全局唯一ID,最后提交了请求
3、lagou_point⼯程改造,做法和订单类似
积分接口
@LocalTCC
public interface PointsService extends IService<Points> {
@TwoPhaseBusinessAction(name = "increaseTcc", commitMethod =
"increaseCommit"
, rollbackMethod = "increaseRollback")
void increase(@BusinessActionContextParameter(paramName = "username") String username,
@BusinessActionContextParameter(paramName = "points") Integer points);
boolean increaseCommit(BusinessActionContext context);
boolean increaseRollback(BusinessActionContext context);
}
积分实现
/**
* 会员积分服务
*/
@Slf4j
@Service
public class PointsServiceImpl extends ServiceImpl<PointsMapper, Points> implements PointsService {
@Autowired
PointsMapper pointsMapper;
/**
* 会员增加积分
*
* @param username 用户名
* @param points 增加的积分
* @return 积分对象
*/
public void increase(String username, Integer points) {
QueryWrapper<Points> wrapper = new QueryWrapper<Points>();
wrapper.lambda().eq(Points::getUsername, username);
Points userPoints = this.getOne(wrapper);
if (userPoints == null) {
userPoints = new Points();
userPoints.setUsername(username);
//userPoints.setPoints(points); 不直接增加积分
userPoints.setFrozenPoints(points);//设置冻结积分
this.save(userPoints);
} else {
userPoints.setFrozenPoints(points);//设置冻结积分
this.saveOrUpdate(userPoints);
}
}
@Override
public boolean increaseCommit(BusinessActionContext context) {
//查询⽤户积分
QueryWrapper<Points> wrapper = new QueryWrapper<Points>();
//从上下文信息中获取用户信息
wrapper.lambda().eq(Points::getUsername,
context.getActionContext("username"));
Points userPoints = this.getOne(wrapper);
if (userPoints != null) {
//增加⽤户积分
userPoints.setPoints(userPoints.getPoints() +
userPoints.getFrozenPoints());
//冻结积分清零,接口幂等性,这里必须设置为0,否则会引起错误
userPoints.setFrozenPoints(0);
this.saveOrUpdate(userPoints);
}
log.info("积分模块:--------->xid=" + context.getXid() + " 提交成功!");
return true;
}
@Override
public boolean increaseRollback(BusinessActionContext context) {
//查询⽤户积分
QueryWrapper<Points> wrapper = new QueryWrapper<Points>();
wrapper.lambda().eq(Points::getUsername,
context.getActionContext("username"));
Points userPoints = this.getOne(wrapper);
if (userPoints != null) {
//冻结积分清零,回滚操作只需把冻结积分清零即可
userPoints.setFrozenPoints(0);
this.saveOrUpdate(userPoints);
}
log.info("积分模块:--------->xid=" + context.getXid() + " 回滚成功!");
return true;
}
}
4、lagou_storage
库存接口
@LocalTCC
public interface StorageService extends IService<Storage> {
@TwoPhaseBusinessAction(
name = "decreaseTcc",
commitMethod = "decreaseCommit",
rollbackMethod = "decreaseRollback")
public void decrease(@BusinessActionContextParameter(paramName = "goodsId") Integer goodsId,
@BusinessActionContextParameter(paramName = "quantity") Integer quantity);
public boolean decreaseCommit(BusinessActionContext context);
public boolean decreaseRollback(BusinessActionContext context);
}
库存实现
@Slf4j
@Service
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements StorageService {
/**
* 减少库存
*
* @param goodsId 商品ID
* @param quantity 减少数量
* @return 库存对象
*/
public void decrease(Integer goodsId, Integer quantity) {
QueryWrapper<Storage> wrapper = new QueryWrapper<Storage>();
wrapper.lambda().eq(Storage::getGoodsId, goodsId);
Storage goodsStorage = this.getOne(wrapper);
if (goodsStorage.getStorage() >= quantity) {
//goodsStorage.setStorage(goodsStorage.getStorage() -quantity);
//设置冻结库存
goodsStorage.setFrozenStorage(quantity);
} else {
throw new RuntimeException(goodsId + "库存不⾜,⽬前剩余库存:"
+ goodsStorage.getStorage());
}
this.saveOrUpdate(goodsStorage);
}
@Override
public boolean decreaseCommit(BusinessActionContext context) {
QueryWrapper<Storage> wrapper = new QueryWrapper<Storage>();
wrapper.lambda().eq(Storage::getGoodsId,
context.getActionContext("goodsId"));
Storage goodsStorage = this.getOne(wrapper);
if (goodsStorage != null) {
//扣减库存
goodsStorage.setStorage(goodsStorage.getStorage() -
goodsStorage.getFrozenStorage());
//冻结库存清零
goodsStorage.setFrozenStorage(0);
this.saveOrUpdate(goodsStorage);
}
log.info("库存模块:--------->xid=" + context.getXid() + " 提交成功!");
return true;
}
@Override
public boolean decreaseRollback(BusinessActionContext context) {
QueryWrapper<Storage> wrapper = new QueryWrapper<Storage>();
wrapper.lambda().eq(Storage::getGoodsId,
context.getActionContext("goodsId"));
Storage goodsStorage = this.getOne(wrapper);
if (goodsStorage != null) {
//冻结库存清零
goodsStorage.setFrozenStorage(0);
this.saveOrUpdate(goodsStorage);
}
log.info("库存模块:--------->xid=" + context.getXid() + " 回滚成功!");
return true;
}
}
TM端改造
针对我们⼯程lagou_bussiness是事务的发起者,所以是TM端,其它⼯程为RM端. 所以我们只需要在lagou_common_db完成即可,因为lagou_bussiness⽅法⾥⾯没有对数据库操作.所以只需要将之前AT模式的代理数据源去掉即可.注意:如果lagou_bussiness也对数据库操作了.也需要完成try/commit/rollback的实现
整体流程如下:
事务发起者代码
测试
把四个服务都进行启动
nocas中看到服务已经启动了
发起test2请求时,由于库存不足,所以此次交易失败,全部回滚,我们可以看到相关的日志,由于回滚的时候,会调用多次回滚方法,所以这个方法必须是幂等的,而且根据断点调试得知,不同业务的提交顺序和业务的回滚顺序是相反的
提交时:订单-> 积分-> 库存
回滚时:库存->积分->订单
-
订单回滚
-
积分回滚
-
库存回滚
Seata-Sage模式
Saga模式简单介绍
Saga 模式是 Seata 开源的⻓事务解决⽅案,将由蚂蚁⾦服主要贡献。在 Saga 模式下,分布式事务内有多个参与者,每⼀个参与者都是⼀个冲正补偿服务,需要⽤户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执⾏过程中,依次执⾏各参与者的正向操作,如果所有正向操作均执⾏成功,那么分布式事务提交。如果任何⼀个正向操作执⾏失败,那么分布式事务会去退回去执⾏前⾯各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
适⽤场景:
- 业务流程⻓、业务流程多
- 参与者包含第三⽅公司或遗留系统服务,⽆法提供 TCC 模式要求的三个接⼝
- 典型业务系统:如⾦融⽹络(与外部⾦融机构对接)、互联⽹微贷、渠道整合等业务系统
三种模式对⽐
-
AT 模式是⽆侵⼊的分布式事务解决⽅案,适⽤于不希望对业务进⾏改造的场景,⼏乎0学习成本。
-
TCC 模式是⾼性能分布式事务解决⽅案,适⽤于核⼼系统等对性能有很⾼要求的场景。
-
Saga 模式是⻓事务解决⽅案,适⽤于业务流程⻓且需要保证事务最终⼀致性的业务系统,Saga 模式⼀阶段就会提交本地事务,⽆锁,⻓流程情况下可以保证性能,多⽤于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,⽆法进⾏改造和提供 TCC 要求的接⼝,也可以使⽤Saga 模式