6.Seata分布式事务_05_TCC模式与SAGA模式
整理资料下载:链接: https://pan.baidu.com/s/16_LrfRPkbAjTMqaFNHz_lg?pwd=e3jy提取码: e3jy–来自百度网盘超级会员v6的分享前两种XA和AT都是加锁,加锁就会有性能的损耗,TCC模式不需要加锁TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复TCC模式通过将事务拆分为Try、Confirm和Canc
整理资料下载:
链接: https://pan.baidu.com/s/16_LrfRPkbAjTMqaFNHz_lg?pwd=e3jy
提取码: e3jy
–来自百度网盘超级会员v6的分享
一、TCC模式
前两种XA和AT都是加锁,加锁就会有性能的损耗,TCC模式不需要加锁
1、TCC模式
(1)、TCC模式简介
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复
TCC模式通过将事务拆分为Try、Confirm和Cancel三个阶段,使得每个参与者可以控制自己的操作和资源,从而实现了分布式事务的可靠性和一致性,它要求参与者实现相应的接口和逻辑,确保Try和Cancel操作是幂等的,以处理重试和故障恢复情况
需要实现三个方法
-
Try阶段(资源的检测和预留)
在这个阶段,参与者尝试预留或锁定资源并执行必要的前置检查
如果所有参与者的Try操作都成功,表示资源可用并进入下一阶段
如果有任何一个参与者的Try操作失败,表示资源不可用或发生冲突,事务将中止
-
Confirm阶段(完成资源操作业务)
在这个阶段参与者进行最终的确认操作,将资源真正提交或应用到系统中
如果所有参与者的Confirm操作都成功则事务完成,提交操作得到确认
如果有任何一个参与者的Confirm操作失败则事务将进入Cancel阶段
-
Cancel阶段(预留资源释放)
在这个阶段参与者进行回滚或取消操作,将之前尝试预留或锁定的资源恢复到原始状态
如果所有参与者的Cancel操作都成功则事务被取消,资源释放
如果有任何一个参与者的Cancel操作失败,可能需要进行补偿或人工介入来恢复系统一致性
(2)、TCC业务示例
假设账户A原来余额是100,需要余额扣减30元
-
阶段一(Try)
检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
初始余额

余额充足,可以冻结

此时
总金额 = 冻结金额 + 可用金额,数量依然是100不变事务直接提交无需等待其它事务
-
阶段二(Confirm)
假如要提交(Confirm),则冻结金额扣减30
确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了

此时总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
-
阶段二(Canncel)
如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30
需要回滚那么就要释放冻结金额,恢复可用金额

(3)、TCC优缺点
-
优点
- 一阶段完成直接提交事务,释放数据库资源,性能好
-
相比AT模型,无需生成快照,无需使用全局锁,性能最强
-
不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
-
缺点
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
-
软状态,事务是最终一致
-
需要考虑Confirm和Cancel的失败情况,做好幂等处理
(4)、TCC的空回滚
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作
在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚

执行cancel操作时应当判断try是否已经执行,如果尚未执行则应该空回滚
(5)、TCC的事务悬挂
对于已经空回滚的业务,之前被阻塞的try操作恢复,如果以后继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂
执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂

2、Seata的TCC模式
(1)、Seata的TCC模式简介
Seata的TCC模式是在TCC模式基础上进行了扩展和优化的实现
Seata引入了Seata Server作为事务协调器,集中管理分布式事务的控制逻辑
Seata的TCC模式还提供了分布式事务日志和分布式锁等功能,以增强事务的可靠性和性能
Seata的TCC模式可以更方便地集成到应用中,并提供了更好的事务管理和监控能力
-
Try(资源检查和预留)
判断是否有可用数据,足够则冻结可用数据
-
Confirm(业务执行和提交)
完成资源的操作业务:要求try成功则confirm一定要成功
-
Cancel(预留资源的释放)
预留资源释放,可以理解为try方向操作
Seata中的TCC模型依然延续之前的事务架构

(2)、Seata的TCC常用注解
-
@LocalTCC注解
@LocalTCC 适用于SpringCloud+Feign模式下的TCC
该注解需要添加到 Try 方法所在的接口上,表示实现该接口的类被 seata 来管理
seata 根据事务的状态,自动调用我们定义的方法
- 如果try()没问题则调用 commit() 方法
- 如果try()有问题调用 rollback() 方法
【示例】
@LocalTCC public interface AccountService { } -
@TwoPhaseBusinessAction注解
该注解用在接口的 Try 方法上,声明 TCC 需要执行方法
其中三个方法最重要
-
name()
用于指定当前业务的Try逻辑对应的方法名称
@TwoPhaseBusinessAction注解中的name属性要与当前方法名一致
全局唯一
-
commitMethod()
二阶段确认方法(默认方法名commit);
@TwoPhaseBusinessAction注解中的commitMethod属性要与提交方法名一致
-
rollbackMethod()
二阶段取消方法(默认方法名rollback);
@TwoPhaseBusinessAction注解中的rollbackMethod属性要与回滚方法名一致
import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface AccountService { /** * try逻辑 * * @param userId 用户ID * @param money 扣减金额 * @TwoPhaseBusinessAction注解中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法 * @TwoPhaseBusinessAction注解中的commitMethod属性要与提交方法名一致,用于指定提交逻辑对应的方法 * @TwoPhaseBusinessAction注解中的rollbackMethod属性要与回滚方法名一致,用于指定回滚逻辑对应的方法 */ @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel") void deduct(@BusinessActionContextParameter(paramName = "userId") Integer userId, @BusinessActionContextParameter(paramName = "productId") Integer productId, @BusinessActionContextParameter(paramName = "count") Integer count, @BusinessActionContextParameter(paramName = "money") Double money); /** * 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致 * * @param * @param context 上下文可以传递try方法里面的参数 * @return boolean 执行是否成功 */ boolean confirm(BusinessActionContext context); /** * 二阶段回滚方法,要保证与rollbackMethod一致 * * @param context * @return */ boolean cancel(BusinessActionContext context); } -
-
@BusinessActionContextParameter
该注解用来修饰 try() 方法的入参,被修饰的入参可以在 commit() 方法和 rollback() 方法中通过 BusinessActionContext 获取
-
BusinessActionContext
BusinessActionContext 是 seata tcc 的事务上下文,用于存放 tcc 事务的一些数据
在try阶段被BusinessActionContextParameter注解的参数都可以获取到
3、使用示例
(1)、业务说明
-
try业务
记录冻结金额和事务状态到account_freeze表
扣减account表可用金额
-
Confirm业务
根据xid删除account_freeze表的冻结记录
-
Cancel业务
修改account_freeze表,冻结金额为0,state为2
修改account表,恢复可用金额
-
如何判断是否空回滚
Cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
-
如何避免业务悬挂
Try业务中,根据xid查询account_freeze,如果已经存在则证明Cancel已经执行,拒绝执行try业务
(2)、增加数据表
为了实现空回滚、防止业务悬挂以及幂等性要求,必须在数据库记录冻结金额的同时,记录当前事务ID和执行状态,所以增加两张表
xid:是全局事务id
user_id:用来记录用户信息
freeze_money:用来记录用户冻结金额
freeze_num:用来记录产品剩余数量
state:用来记录事务状态
-
账户冻结表
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for account_freeze -- ---------------------------- DROP TABLE IF EXISTS `account_freeze`; CREATE TABLE `account_freeze` ( `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `user_id` int NOT NULL, `freeze_money` double UNSIGNED NOT NULL DEFAULT 0, `state` int NOT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel', PRIMARY KEY (`xid`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT; SET FOREIGN_KEY_CHECKS = 1; -
产品冻结表
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for product_freeze -- ---------------------------- DROP TABLE IF EXISTS `product_freeze`; CREATE TABLE `product_freeze` ( `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `user_id` int NOT NULL, `freeze_num` int UNSIGNED NOT NULL DEFAULT 0, `state` int NOT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel', PRIMARY KEY (`xid`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT; SET FOREIGN_KEY_CHECKS = 1;
(3)、父工程
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.8</version>
<relativePath/>
</parent>
<groupId>com.seata</groupId>
<artifactId>SeataTCCDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<!--1.打包方式设置为pom-->
<packaging>pom</packaging>
<!--2.配置子模块信息-->
<modules>
<module>account-service</module>
<module>product-service</module>
</modules>
<!--3.定义版本信息-->
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>2023.0.3</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.3.2</spring-cloud-alibaba.version>
</properties>
<!--4.这里定义公共的依赖-->
<dependencies>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.4.0</version>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
</dependencies>
<!--5.定义springcloud、springcloudalibba依赖管理-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!--6.引入构建工具-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
(4)、账户服务
-
pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.seata</groupId> <artifactId>SeataTCCDemo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <groupId>com.account</groupId> <artifactId>account-service</artifactId> <dependencies> <!--feign依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--负载均衡依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- nacos 服务注册--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- nacos 配置中心--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!-- seata --> <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>2.0.0</version> </dependency>--> <!-- seata kryo 序列化--> <!--<dependency> <groupId>io.seata</groupId> <artifactId>seata-serializer-kryo</artifactId> <version>2.0.0</version> </dependency>--> <!--mysql驱动--> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <!--mybatis依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.4</version> </dependency> <!--lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project> -
配置文件
重点就是seata客户端的配置
-
配置配置中心获取集群名称
-
配置注册中心,要与seata服务在一个命名空间以便服务发现
#配置使用哪个环境,在真实项目中,一般分为多个环境,此处指定使用的是dev环境 spring: profiles: active: dev --- ##指定本服务的端口号 server: port: 8081 spring: #设置当前应用的名称 application: name: account-service #指定此处配置为dev环境 profiles: active: dev #数据库连接配置 datasource: url: jdbc:mysql://localhost:3306/account?characterEncoding=UTF-8&useSSL=false&useUnicode=true&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver # 设置从nacos加载的配置文件 # 配置文件从"spring.cloud.nacos.config"模块里配置的nacos服务器的命名空间下读取 config: import: #这里表示读取命名空间名称为nacos的、组名为DEV_GROUP的account.yml配置文件 # 这里配置组名会覆盖掉"spring.cloud.nacos.config"里面设置的组名 - optional:nacos:account.yml?group=DEV_GROUP cloud: # nacos相关的配置 nacos: # 设置连接nacos的用户名、密码 username: nacos password: nacos # nacos的配置中心设置#(这里连接的是nacos命名空间) config: # 配置中心地址 # 注意config模块里面配置的地址与discovery模块里面配置的地址可以不相同,两者互不影响 server-addr: localhost:8848 # 配置文件后缀,即配置文件格式,目前只支持"properties"与"yml"两种格式 file-extension: yaml # 配置文件存放的命名空间ID,这里是从命名空间为nacos的命名空间读取配置 namespace: 231e01ec-912b-4529-9ff4-6bf792b38776 # 设置默认的组名 group: DEFAULT_GROUP # 配置注册中心(这里连接的是seata命名空间) discovery: # 配置注册中心地址 #注意config模块里面配置的地址与discovery模块里面配置的地址可以不相同,二者互不影响 server-addr: localhost:8848 # 获取服务的命名空间ID namespace: c511f4d4-0205-4316-909a-d0827ade5435 # 设置服务的分组名为SEATA_GROUP,因为seata的分组名也是SEATA_GROUP,需要保持一致 group: SEATA_GROUP # seata客户端配置 seata: # 设置seata使用哪种模式支持分布式事务(可选的值:XA、AT、TCC、SAGA) # data-source-proxy-mode: TCC # 应用ID(如果不配置则默认使用"spring.application.name"的值) # application-id: account-service enabled: true # 配置事务组名称,名字随便起 # 根据这个获取TC服务的cluster名称 tx-service-group: seata_demo # seata.service.vgroup-mapping下一定要有一个对"seata.tx-service-group: XXX"这个名字的映射 # 注意分两种情况 #情况1 # 当没有配置"seata.config.nacos"时,需要在本配置文件中配置"seata.service.vgroup-mapping.自定义事务组名: 集群名" # 这里的自定义事务组名称就是上面配置的"seata.tx-service-group"的值,集群名称就是"SeataServer"服务器中 # 的"seata.registry.nacos.cluster"中配置的集群名称 # 也就是需要添加下面的配置 service: vgroup-mapping: # 配置事务组与TC服务cluster的映射关系 # key:对应"tx-service-group"中定义的事务组名称 # value:对应SeataServer上的application.yml中seata.registry.nacos.cluster中配置的集群名称 seata_demo: default # 情况2: # 当配置了"seata.config.nacos"的配置后,则"seata.service.vgroup-mapping.自定义事务组名: 集群名"的值从nacos中获取 # 此时需要在对应的命名空间下(即"seata.config.nacos.namespace"中配置的命名空间)创建名称为"service.vgroup-mapping.自定义事务组名"的配置文件 # 文件的格式为"text"格式,值为"SeataServer"服务器中的"seata.registry.nacos.cluster"中配置的集群名称,比如下面的"default" # seata连接nacos的配置 config: # 配置seata的配置中心为nacos type: nacos #(这里连接的是nacos命名空间) nacos: # 这里设置seata服务需要从哪个nacos服务器上读取配置,可以与"spring.cloud.nacos.config"中配置的nacos地址不同 server-addr: localhost:8848 # 设置需要读取的命名空间的ID namespace: 231e01ec-912b-4529-9ff4-6bf792b38776 # 设置配置文件的分组 group: TEST_GROUP # 连接nacos的用户名 username: nacos # 连接nacos的密码 password: nacos # 配置注册项,配置内容与服务端保持一致(这里连接的是seata命名空间) registry: type: nacos nacos: # 设置连接的nacos服务器地址 # 这里必须与SeataServer服务器连接的nacos地址一致,测试服务器中配置的命名空间为seata,这里也连接为seata空间才能实现服务发现 server-addr: localhost:8848 # 设置命名空间ID:这里必须与SeataServer服务器配置的命名空间一致 namespace: c511f4d4-0205-4316-909a-d0827ade5435 # TC服务在nacos中注册的服务名 # 默认是seata-server,若没有修改则可以不配置 application: seata-server # 设置服务所在的分组,这里必须与SeataServer服务器配置的分组名一致,默认就是SEATA_GROUP group: SEATA_GROUP # 连接nacos的用户名 username: nacos # 连接nacos的密码 password: nacos ##关闭驼峰 #mybatis-plus: # configuration: # map-underscore-to-camel-case: true # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #mybatis mybatis: #所有pojo类所在包的路径 type-aliases-package: com.account.entity # xml映射文件所在的路径,一般用模糊匹配来指定最终的xml文件 mapper-locations: classpath:mapper/*.xml #mapper映射文件 configuration: #采用驼峰形式将数据表中以‘_’分隔的字段映射到java类的某个属性,比如表字段user_name可以映射为类里面的userName属性 map-underscore-to-camel-case: true #支持驼峰映射 -
-
控制器类
添加feign注解开启feign功能
package com.account; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients public class AccountApplication { public static void main(String[] args) { SpringApplication.run(AccountApplication.class, args); } } -
控制器类
定义一个方法,方法调用模拟的testService中的方法
package com.account.controller; import com.account.service.TestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/account") public class AccountController { @Autowired private TestService testService; /** * 修改用户账户信息 * * @return */ @RequestMapping("/deduct") public void deduct(int userId, int productId, int count, double money) { testService.deduct(userId, productId, count, money); } } -
业务类
-
接口:模拟业务服务
package com.account.service; public interface TestService { /** * 模拟业务Service * 1、扣减用户账户余额 * 2、扣减库存 * * @param userId 用户ID * @param productId 产品ID * @param count 购买产品数量 * @param money 消费金额 */ void deduct(Integer userId, Integer productId, Integer count, Double money); } -
实现类
该方法模拟业务中的逻辑
该方法是分布式事务的入口,需要在该方法上添加"@GlobalTransactional"开启全局事务
package com.account.service; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j public class TestServiceImpl implements TestService { @Autowired AccountService accountService; /** * 模拟业务Service * 1、扣减用户账户余额 * 2、扣减库存 * <p> * 在方法上添加注解"@GlobalTransactional"用来开启全局事务 * * @param userId 用户ID * @param productId 产品ID * @param count 购买产品数量 * @param money 消费金额 */ @GlobalTransactional public void deduct(Integer userId, Integer productId, Integer count, Double money) { // 调用用户账户服务来扣减金额 accountService.deduct(userId, productId, count, money); } }
-
-
业务事务类
-
接口
package com.account.service; import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface AccountService { /** * try逻辑 * * @param userId 用户ID * @param money 扣减金额 * @TwoPhaseBusinessAction注解中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法 * @TwoPhaseBusinessAction注解中的commitMethod属性要与提交方法名一致,用于指定提交逻辑对应的方法 * @TwoPhaseBusinessAction注解中的rollbackMethod属性要与回滚方法名一致,用于指定回滚逻辑对应的方法 */ @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel") void deduct(@BusinessActionContextParameter(paramName = "userId") Integer userId, @BusinessActionContextParameter(paramName = "productId") Integer productId, @BusinessActionContextParameter(paramName = "count") Integer count, @BusinessActionContextParameter(paramName = "money") Double money); /** * 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致 * * @param * @param context 上下文可以传递try方法里面的参数 * @return boolean 执行是否成功 */ boolean confirm(BusinessActionContext context); /** * 二阶段回滚方法,要保证与rollbackMethod一致 * * @param context * @return */ boolean cancel(BusinessActionContext context); } -
实现类
package com.account.service; import com.account.client.ProductService; import com.account.entity.Account; import com.account.entity.AccountFreeze; import com.account.mapper.AccountFreezeMapper; import com.account.mapper.AccountMapper; import io.seata.core.context.RootContext; import io.seata.rm.tcc.api.BusinessActionContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Date; @Service @RefreshScope public class AccountServiceImpl implements AccountService { @Value("${address.name}") private String name; @Autowired AccountMapper accountMapper; @Autowired AccountFreezeMapper accountFreezeMapper; @Autowired ProductService productService; /** * @param userId * @param money */ @Override @Transactional public void deduct(Integer userId, Integer productId, Integer count, Double money) { //1、获取事务id String xid = RootContext.getXID(); System.out.println("地址名称:" + name + ";事务ID:" + xid); //2、判断freeze中是否有冻结记录,如果有则一定是CANCEL执行过,需要要拒绝业务 AccountFreeze oldFreeze = accountFreezeMapper.selectById(xid); if (oldFreeze != null) { System.out.println("地址名称:" + name + ";CANCEL执行过,需要拒绝业务:"); //CANCEL执行过,需要拒绝业务 return; } //3、用户账户扣减可用余额 Account account = new Account(); account.setUserId(userId); account.setMoney(money); account.setUpdateDate(new Date()); accountMapper.deduct(account); //4、冻结表记录冻结金额、事务状态 AccountFreeze freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.TRY); freeze.setXid(xid); accountFreezeMapper.insert(freeze); System.out.println("地址名称:" + name + ";冻结完成"); productService.deduct(productId, userId, count); } /** * 提交操作 * * @param context 上下文可以传递try方法里面的参数 * @return */ @Override @Transactional public boolean confirm(BusinessActionContext context) { // 1、获取事务id String xid = context.getXid(); //2、根据id删除冻结记录 int count = accountFreezeMapper.deleteById(xid); System.out.println("地址名称:" + name + ";提交完成"); return count == 1; } /** * 回滚操作 * * @param context * @return */ @Override @Transactional public boolean cancel(BusinessActionContext context) { // 1、获取事务ID String xid = context.getXid(); //2、查询freeze表中是否有冻结记录 AccountFreeze freeze = accountFreezeMapper.selectById(xid); String userId = context.getActionContext("userId").toString(); //3、空回滚的判断:判断freeze是否为null,如果为null则证明try没有执行,需要空回滚 if (freeze == null) { //证明try没执行,需要空回滚 double money = 0l; freeze = new AccountFreeze(); freeze.setUserId(Integer.valueOf(userId)); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.CANCEL); freeze.setXid(xid); accountFreezeMapper.insert(freeze); return true; } //4、幂等判断 if (freeze.getState() == AccountFreeze.State.CANCEL) { //已经处理过一次CANCEL,无需重复处理 return true; } //5、恢复可用余额 Account account = new Account(); account.setUserId(Integer.valueOf(freeze.getUserId())); account.setMoney(freeze.getFreezeMoney()); account.setUpdateDate(new Date()); accountMapper.refund(account); //6、将冻结金额清零,状态改为CANCEL double money = 0; freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.CANCEL); int count = accountFreezeMapper.updateById(freeze); System.out.println("地址名称:" + name + ";回滚完成"); return count == 1; } }
-
-
实体类
-
账户类
package com.account.entity; import lombok.Data; import java.util.Date; @Data public class Account { private int id; private int userId; //用户ID private double money;//剩余金额 private Date updateDate;//更新时间 private Date createDate;//创建时间 } -
冻结类
package com.account.entity; import lombok.Data; @Data public class AccountFreeze { private String xid; private Integer userId; private Double freezeMoney; private Integer state; public static abstract class State { public final static int TRY = 0; public final static int CONFIRM = 1; public final static int CANCEL = 2; } }
-
-
mapper
-
账户mapper
package com.account.mapper; import com.account.entity.Account; import org.apache.ibatis.annotations.Mapper; @Mapper public interface AccountMapper { //扣减金额 int deduct(Account account); //恢复金额 int refund(Account account); } -
冻结mapper
package com.account.mapper; import com.account.entity.AccountFreeze; import org.apache.ibatis.annotations.Mapper; @Mapper public interface AccountFreezeMapper { //新增冻结记录 int insert(AccountFreeze accountFreeze); //根据id删除冻结记录 int deleteById(String xid); //修改冻结记录 int updateById(AccountFreeze accountFreeze); //查询freeze中是否有冻结记录 AccountFreeze selectById(String xid); }
-
-
feign配置类
package com.account.config; import feign.Retryer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import static java.util.concurrent.TimeUnit.SECONDS; /** * 自定义Feign配置类 */ @Configuration public class FeignConfig { @Bean public Retryer feignRetryer() { return new Retryer.Default(100, SECONDS.toMillis(1), 5); } } -
远程调用类
package com.account.client; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; /** * 用于访问Product服务的feign */ @FeignClient("product-service") public interface ProductService { @RequestMapping(value = "/product/deduct") String deduct(@RequestParam("productId") int productId,@RequestParam("userId") int userId, @RequestParam("productNum") int productNum); } -
xml文件
-
账户xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.account.mapper.AccountMapper"> <!--扣减金额--> <update id="deduct" parameterType="com.account.entity.Account"> update account set money=money - #{money,jdbcType=DOUBLE}, update_date=#{updateDate,jdbcType=TIMESTAMP} where user_id = #{userId,jdbcType=INTEGER}; </update> <!--恢复金额--> <update id="refund" parameterType="com.account.entity.Account"> update account set money=money + #{money,jdbcType=DOUBLE}, update_date=#{updateDate,jdbcType=TIMESTAMP} where user_id = #{userId,jdbcType=INTEGER}; </update> </mapper> -
账户冻结xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.account.mapper.AccountFreezeMapper"> <!--根据用户ID查询用户的账户信息--> <insert id="insert" parameterType="com.account.entity.AccountFreeze"> insert into account_freeze(XID, USER_ID, FREEZE_MONEY, STATE) values (#{xid,jdbcType=VARCHAR}, #{userId,jdbcType=INTEGER}, #{freezeMoney,jdbcType=DOUBLE}, #{state,jdbcType=INTEGER}) </insert> <!--根据id删除冻结记录--> <update id="deleteById" parameterType="java.lang.String"> delete from account_freeze where xid = #{xid,jdbcType=VARCHAR}; </update> <!--修改冻结记录--> <update id="updateById" parameterType="com.account.entity.AccountFreeze"> update account_freeze set freeze_money= #{freezeMoney,jdbcType=DOUBLE}, state= #{state,jdbcType=INTEGER}, where xid = #{xid,jdbcType=VARCHAR}; </update> <!--查询freeze中是否有冻结记录--> <select id="selectById" parameterType="java.lang.String" resultType="com.account.entity.AccountFreeze"> select * from account_freeze where xid = #{xid,jdbcType=VARCHAR}; </select> </mapper>
-
(5)、产品服务
-
pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.seata</groupId> <artifactId>SeataTCCDemo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <groupId>com.product</groupId> <artifactId>product-service</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- nacos 服务注册--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- nacos 配置中心--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!-- seata --> <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>2.0.0</version> </dependency>--> <!--mysql驱动--> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <!--mybatis依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.4</version> </dependency> <!--lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project> -
配置文件
#配置使用哪个环境,在真实项目中,一般分为多个环境,此处指定使用的是dev环境 spring: profiles: active: dev --- ##指定本服务的端口号 server: port: 8082 spring: #设置当前应用的名称 application: name: product-service #指定此处配置为dev环境 profiles: active: dev #数据库连接配置 datasource: url: jdbc:mysql://localhost:3306/product?characterEncoding=UTF-8&useSSL=false&useUnicode=true&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver # 设置从nacos加载的配置文件 # 配置文件从"spring.cloud.nacos.config"模块里配置的nacos服务器的命名空间下读取 config: import: #这里表示读取命名空间名称为nacos的、组名为DEV_GROUP的product.yml配置文件 # 这里配置组名会覆盖掉"spring.cloud.nacos.config"里面设置的组名 - optional:nacos:product.yml?group=DEV_GROUP cloud: # nacos相关的配置 nacos: # 设置连接nacos的用户名、密码 username: nacos password: nacos # nacos的配置中心设置#(这里连接的是nacos命名空间) config: # 配置中心地址 # 注意config模块里面配置的地址与discovery模块里面配置的地址可以不相同,两者互不影响 server-addr: localhost:8848 # 配置文件后缀,即配置文件格式,目前只支持"properties"与"yml"两种格式 file-extension: yaml # 配置文件存放的命名空间ID,这里是从命名空间为nacos的命名空间读取配置 namespace: 231e01ec-912b-4529-9ff4-6bf792b38776 # 设置默认的组名 group: DEFAULT_GROUP # 配置注册中心(这里连接的是seata命名空间) discovery: # 配置注册中心地址 #注意config模块里面配置的地址与discovery模块里面配置的地址可以不相同,二者互不影响 server-addr: localhost:8848 # 获取服务的命名空间ID namespace: c511f4d4-0205-4316-909a-d0827ade5435 # 设置服务的分组名为SEATA_GROUP,因为seata的分组名也是SEATA_GROUP,需要保持一致 group: SEATA_GROUP # seata客户端配置 seata: # 设置seata使用哪种模式支持分布式事务(可选的值:XA、AT、TCC、SAGA) # data-source-proxy-mode: TCC # 应用ID(如果不配置则默认使用"spring.application.name"的值) # application-id: product-service enabled: true # 配置事务组名称,名字随便起 # 根据这个获取TC服务的cluster名称 tx-service-group: seata_demo # seata.service.vgroup-mapping下一定要有一个对"seata.tx-service-group: XXX"这个名字的映射 # 注意分两种情况 #情况1 # 当没有配置"seata.config.nacos"时,需要在本配置文件中配置"seata.service.vgroup-mapping.自定义事务组名: 集群名" # 这里的自定义事务组名称就是上面配置的"seata.tx-service-group"的值,集群名称就是"SeataServer"服务器中 # 的"seata.registry.nacos.cluster"中配置的集群名称 # 也就是需要添加下面的配置 service: vgroup-mapping: # 配置事务组与TC服务cluster的映射关系 # key:对应"tx-service-group"中定义的事务组名称 # value:对应SeataServer上的application.yml中seata.registry.nacos.cluster中配置的集群名称 seata_demo: default # 情况2: # 当配置了"seata.config.nacos"的配置后,则"seata.service.vgroup-mapping.自定义事务组名: 集群名"的值从nacos中获取 # 此时需要在对应的命名空间下(即"seata.config.nacos.namespace"中配置的命名空间)创建名称为"service.vgroup-mapping.自定义事务组名"的配置文件 # 文件的格式为"text"格式,值为"SeataServer"服务器中的"seata.registry.nacos.cluster"中配置的集群名称,比如下面的"default" # seata连接nacos的配置 config: # 配置seata的配置中心为nacos type: nacos #(这里连接的是nacos命名空间) nacos: # 这里设置seata服务需要从哪个nacos服务器上读取配置,可以与"spring.cloud.nacos.config"中配置的nacos地址不同 server-addr: localhost:8848 # 设置需要读取的命名空间的ID namespace: 231e01ec-912b-4529-9ff4-6bf792b38776 # 设置配置文件的分组 group: TEST_GROUP # 连接nacos的用户名 username: nacos # 连接nacos的密码 password: nacos # 配置注册项,配置内容与服务端保持一致(这里连接的是seata命名空间) registry: type: nacos nacos: # 设置连接的nacos服务器地址 # 这里必须与SeataServer服务器连接的nacos地址一致,测试服务器中配置的命名空间为seata,这里也连接为seata空间才能实现服务发现 server-addr: localhost:8848 # 设置命名空间ID:这里必须与SeataServer服务器配置的命名空间一致 namespace: c511f4d4-0205-4316-909a-d0827ade5435 # TC服务在nacos中注册的服务名 # 默认是seata-server,若没有修改则可以不配置 application: seata-server # 设置服务所在的分组,这里必须与SeataServer服务器配置的分组名一致,默认就是SEATA_GROUP group: SEATA_GROUP # 连接nacos的用户名 username: nacos # 连接nacos的密码 password: nacos #关闭驼峰 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #mybatis mybatis: #所有pojo类所在包的路径 type-aliases-package: com.product.entity # xml映射文件所在的路径,一般用模糊匹配来指定最终的xml文件 mapper-locations: classpath:mapper/*.xml #mapper映射文件 configuration: #采用驼峰形式将数据表中以‘_’分隔的字段映射到java类的某个属性,比如表字段user_name可以映射为类里面的userName属性 map-underscore-to-camel-case: true #支持驼峰映射 -
控制器类
package com.product.controller; import com.product.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; /** * 根据产品ID更新产品信息 * * @param productId * @param productNum * @return */ @RequestMapping("/deduct") public void deduct(@RequestParam("productId") int productId, @RequestParam("userId") int userId, @RequestParam("productNum") int productNum) { System.out.println("ProductController 开始执行"); productService.deduct(productId, userId, productNum); } } -
实体类
-
产品实体
package com.product.entity; import lombok.Data; import java.util.Date; @Data public class Product { private int id;//产品ID private int productNum;//产品剩余数量 private String productDesc;//产品描述 private Date createDate;//创建时间 } -
产品冻结实体
package com.product.entity; import lombok.Data; import java.util.Date; @Data public class ProductFreeze { private String xid;//事务ID private Integer freezeNum;//冻结数量 private Integer userId;//用户ID private Integer state;//冻结状态 public static abstract class State { public final static int TRY = 0; public final static int CONFIRM = 1; public final static int CANCEL = 2; } }
-
-
mapper
-
产品mapper
package com.product.mapper; import com.product.entity.Product; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ProductMapper { //扣减金额 int deduct(Product account); //恢复金额 int refund(Product account); } -
冻结mapper
package com.product.mapper; import com.product.entity.ProductFreeze; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ProductFreezeMapper { //新增冻结记录 int insert(ProductFreeze accountFreeze); //根据id删除冻结记录 int deleteById(String xid); //修改冻结记录 int updateById(ProductFreeze accountFreeze); //查询freeze中是否有冻结记录 ProductFreeze selectById(String xid); }
-
-
service
-
产品service
package com.product.service; import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface ProductService { /** * try逻辑 * * @param productId 产品ID * @param userId 用户ID * @param productNum 扣减库存数量 * @TwoPhaseBusinessAction注解中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法 * @TwoPhaseBusinessAction注解中的commitMethod属性要与提交方法名一致,用于指定提交逻辑对应的方法 * @TwoPhaseBusinessAction注解中的rollbackMethod属性要与回滚方法名一致,用于指定回滚逻辑对应的方法 */ @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel") void deduct(@BusinessActionContextParameter(paramName = "productId") Integer productId, @BusinessActionContextParameter(paramName = "userId") Integer userId, @BusinessActionContextParameter(paramName = "productNum") Integer productNum); /** * 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致 * * @param * @param context 上下文可以传递try方法里面的参数 * @return boolean 执行是否成功 */ boolean confirm(BusinessActionContext context); /** * 二阶段回滚方法,要保证与rollbackMethod一致 * * @param context * @return */ boolean cancel(BusinessActionContext context); } -
冻结service
package com.product.service; import com.product.entity.Product; import com.product.entity.ProductFreeze; import com.product.mapper.ProductFreezeMapper; import com.product.mapper.ProductMapper; import io.seata.core.context.RootContext; import io.seata.rm.tcc.api.BusinessActionContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RefreshScope public class ProductServiceImpl implements ProductService { @Value("${product.name}") private String productName; @Autowired ProductMapper productMapper; @Autowired ProductFreezeMapper productFreezeMapper; /** * @param productId * @param freezeNum */ @Override @Transactional public void deduct(Integer productId, Integer userId, Integer freezeNum) { System.out.println("产品名称:" + productName); //1、获取事务id String xid = RootContext.getXID(); System.out.println("产品名称:" + productName + ";事务ID:" + xid); //2、判断freeze中是否有冻结记录,如果有则一定是CANCEL执行过,需要要拒绝业务 ProductFreeze oldFreeze = productFreezeMapper.selectById(xid); if (oldFreeze != null) { System.out.println("产品名称:" + productName + ";CANCEL执行过,需要拒绝业务" + xid); //CANCEL执行过,需要拒绝业务 return; } //3、商品库存扣减数量 Product account = new Product(); account.setProductNum(freezeNum); account.setId(productId); productMapper.deduct(account); if(1==1){ throw new RuntimeException("模拟异常"); } //4、冻结表记录冻结数量、事务状态 ProductFreeze freeze = new ProductFreeze(); freeze.setUserId(userId); freeze.setFreezeNum(freezeNum); freeze.setState(ProductFreeze.State.TRY); freeze.setXid(xid); productFreezeMapper.insert(freeze); System.out.println("产品名称:" + productName + ";冻结完成"); } /** * 提交操作 * * @param context 上下文可以传递try方法里面的参数 * @return */ @Override @Transactional public boolean confirm(BusinessActionContext context) { // 1、获取事务id String xid = context.getXid(); //2、根据id删除冻结记录 int count = productFreezeMapper.deleteById(xid); System.out.println("产品名称:" + productName + ";提交完成"); return count == 1; } /** * 回滚操作 * * @param context * @return */ @Override @Transactional public boolean cancel(BusinessActionContext context) { // 1、获取事务ID String xid = context.getXid(); //2、查询freeze表中是否有冻结记录 ProductFreeze freeze = productFreezeMapper.selectById(xid); String userId = context.getActionContext("userId").toString(); //3、空回滚的判断:判断freeze是否为null,如果为null则证明try没有执行,需要空回滚 if (freeze == null) { //证明try没执行,需要空回滚 freeze = new ProductFreeze(); freeze.setUserId(Integer.valueOf(userId)); freeze.setFreezeNum(0); freeze.setState(ProductFreeze.State.CANCEL); freeze.setXid(xid); productFreezeMapper.insert(freeze); return true; } //4、幂等判断 if (freeze.getState() == ProductFreeze.State.CANCEL) { //已经处理过一次CANCEL,无需重复处理 return true; } //5、恢复剩余数量 Product account = new Product(); account.setProductNum(freeze.getFreezeNum()); account.setId(Integer.valueOf(context.getActionContext("productId").toString())); productMapper.refund(account); //6、将冻结金额清零,状态改为CANCEL freeze.setFreezeNum(0); freeze.setState(ProductFreeze.State.CANCEL); int count = productFreezeMapper.updateById(freeze); System.out.println("产品名称:" + productName + ";回滚完成"); return count == 1; } }
-
-
xml
-
产品xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.product.mapper.ProductMapper"> <!--扣减金额--> <update id="deduct" parameterType="com.product.entity.Product"> update product set product_num=product_num - #{productNum,jdbcType=INTEGER} where id = #{id,jdbcType=INTEGER}; </update> <!--恢复金额--> <update id="refund" parameterType="com.product.entity.Product"> update product set product_num=product_num + #{productNum,jdbcType=INTEGER} where id = #{id,jdbcType=INTEGER}; </update> </mapper> -
冻结xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.product.mapper.ProductFreezeMapper"> <!--根据用户ID查询用户的账户信息--> <insert id="insert" parameterType="com.product.entity.ProductFreeze"> insert into product_freeze(XID, USER_ID, FREEZE_NUM, STATE) values (#{xid,jdbcType=VARCHAR}, #{userId,jdbcType=INTEGER}, #{freezeNum,jdbcType=INTEGER}, #{state,jdbcType=INTEGER}) </insert> <!--根据id删除冻结记录--> <update id="deleteById" parameterType="java.lang.String"> delete from product_freeze where xid = #{xid,jdbcType=VARCHAR}; </update> <!--修改冻结记录--> <update id="updateById" parameterType="com.product.entity.ProductFreeze"> update product_freeze set freezeNum= #{freezeNum,jdbcType=INTEGER}, state= #{state,jdbcType=INTEGER}, where xid = #{xid,jdbcType=VARCHAR}; </update> <!--查询freeze中是否有冻结记录--> <select id="selectById" parameterType="java.lang.String" resultType="com.product.entity.ProductFreeze"> select * from product_freeze where xid = #{xid,jdbcType=VARCHAR}; </select> </mapper>
-
(6)、数据库表
-
账户表
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for account -- ---------------------------- DROP TABLE IF EXISTS `account`; CREATE TABLE `account` ( `id` int NOT NULL AUTO_INCREMENT COMMENT 'id', `user_id` int NOT NULL COMMENT '用户ID', `money` double NULL DEFAULT NULL COMMENT '账户余额', `create_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', `update_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`, `user_id`) USING BTREE, CONSTRAINT `chech_money` CHECK (`money` >= 0) ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of account -- ---------------------------- INSERT INTO `account` VALUES (2, 10, 760, '2025-04-03 12:23:19', '2025-04-03 04:23:19'); SET FOREIGN_KEY_CHECKS = 1; -
产品表
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for product -- ---------------------------- DROP TABLE IF EXISTS `product`; CREATE TABLE `product` ( `id` int NOT NULL AUTO_INCREMENT, `product_num` int NOT NULL, `product_desc` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, `create_date` datetime NOT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of product -- ---------------------------- INSERT INTO `product` VALUES (1, 95, '格力空调', '2023-10-17 09:02:32'); INSERT INTO `product` VALUES (2, 30, '咖啡机', '2023-10-16 09:02:56'); INSERT INTO `product` VALUES (3, 4, '美的冰箱', '2023-10-04 09:03:28'); SET FOREIGN_KEY_CHECKS = 1;
(7)、测试
启动项目访问http://localhost:8081/account/deduct?userId=100&productId=1&count=5&money=5

4、使用步骤
(1)、引入依赖
-
旧版本中
所有事务相关的服务都需要添加如下依赖,先排除spring-cloud-starter-alibaba-seata包中的seata-spring-boot-starter依赖,再重新引入合适版本的依赖,因此seata-spring-boot-starter默认的版本可能不兼容
<!-- seata --> <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> </dependency> -
新版本中直接引入seata包即可,测试后是兼容的
<dependencies> <!-- seata --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> </dependencies>
(2)、添加配置
配置事务使用的模式、nacos使用的配置与服务的地址等信息
-
seata.data-source-proxy-mode
TCC中这一项不需要配置
-
seata.application-id
应用ID
如果不配置则默认使用"spring.application.name"的值
-
seata.tx-service-group(非必须)
配置事务组名称,名字随便起,但是同一个全局事务相关的事务组名称要一致
比如一个全局事务涉及到三个微服务,那么这三个微服务的事务组名称要一致
后面根据这个事务组名称获取TC服务器上配置的cluster名称
-
seata.service.vgroup-mapping(非必须)
seata.service.vgroup-mapping下一定要有一个对"seata.tx-service-group: 事务组名字"这个名字的映射
分两种情况
-
配置了"seata.config.nacos"的配置后
则"seata.service.vgroup-mapping.自定义事务组名: 集群名"的值从nacos中获取
此时需要在对应的命名空间下(即"seata.config.nacos.namespace"中配置的命名空间)创建名称为"service.vgroup-mapping.自定义事务组名"的配置文件
文件的格式为"text"格式,值为"SeataServer"服务器中的"seata.registry.nacos.cluster"中配置的集群名称,比如下面的"default"

-
没有配置"seata.config.nacos"
需要在本配置文件中配置"service.vgroup-mapping.自定义事务组名: 集群名"
这里的自定义事务组名称就是上面配置的"seata.tx-service-group"的值,集群名称是"SeataServer"服务器中的"seata.registry.nacos.cluster"中配置的集群名称
seata: #即添加下面的配置即可 service: vgroup-mapping: # 配置事务组与TC服务cluster的映射关系 # key:对应"tx-service-group"中定义的事务组名称 # value:对应SeataServer上的application.yml中seata.registry.nacos.cluster中配置的集群名称 seata_demo: default
-
-
seata.config.type
设置seata的配置中心为nacos
-
seata.config.nacos.server-addr
这里设置seata服务需要从哪个nacos服务器上读取配置
可以与"spring.cloud.nacos.config"中配置的nacos地址不同
-
seata.config.nacos.namespace
设置需要从哪个命名空间获取配置,这里设置的是命名空间的ID
-
seata.config.nacos.group
设置从哪个分组下
-
seata.config.nacos.username
连接nacos的用户名
-
seata.config.nacos.password
连接nacos的密码
-
seata.registry.type
设置seata服务注册到哪里
-
seata.registry.nacos.server-addr
设置连接的nacos服务器地址
这里必须与SeataServer服务器"seata.registry.nacos.server-addr"设置的命名空间一致
-
seata.registry.nacos.namespace
设置命名空间ID
这里必须与SeataServer服务器"seata.registry.nacos.namespace"设置的命名空间一致
-
seata.registry.nacos.application
SeataServer服务端在nacos中注册的服务名
默认是seata-server,若没有修改则可以不配置
-
seata.registry.nacos.group
设置服务所在的分组
这里必须与SeataServer服务器"seata.registry.nacos.group"设置的分组名一致
因为只有SeataServer服务端与SeataClient的服务在相同的nacos服务器集群下的相同的命名空间下的相同分组下,才能进行服务的发现与调用
-
seata.registry.nacos.username
连接nacos的用户名
-
seata.registry.nacos.password
连接nacos的密码
完整配置
# seata客户端配置
seata:
# 设置seata使用哪种模式支持分布式事务(可选的值:XA、AT、TCC、SAGA)
# data-source-proxy-mode: TCC
# 应用ID(如果不配置则默认使用"spring.application.name"的值)
# application-id: account-service
enabled: true
# 配置事务组名称,名字随便起
# 根据这个获取TC服务的cluster名称
tx-service-group: seata_demo
# seata.service.vgroup-mapping下一定要有一个对"seata.tx-service-group: XXX"这个名字的映射
# 注意分两种情况
#情况1
# 当没有配置"seata.config.nacos"时,需要在本配置文件中配置"seata.service.vgroup-mapping.自定义事务组名: 集群名"
# 这里的自定义事务组名称就是上面配置的"seata.tx-service-group"的值,集群名称就是"SeataServer"服务器中
# 的"seata.registry.nacos.cluster"中配置的集群名称
# 也就是需要添加下面的配置
service:
vgroup-mapping:
# 配置事务组与TC服务cluster的映射关系
# key:对应"tx-service-group"中定义的事务组名称
# value:对应SeataServer上的application.yml中seata.registry.nacos.cluster中配置的集群名称
seata_demo: default
# 情况2:
# 当配置了"seata.config.nacos"的配置后,则"seata.service.vgroup-mapping.自定义事务组名: 集群名"的值从nacos中获取
# 此时需要在对应的命名空间下(即"seata.config.nacos.namespace"中配置的命名空间)创建名称为"service.vgroup-mapping.自定义事务组名"的配置文件
# 文件的格式为"text"格式,值为"SeataServer"服务器中的"seata.registry.nacos.cluster"中配置的集群名称,比如下面的"default"
# seata连接nacos的配置
config:
# 配置seata的配置中心为nacos
type: nacos
#(这里连接的是nacos命名空间)
nacos:
# 这里设置seata服务需要从哪个nacos服务器上读取配置,可以与"spring.cloud.nacos.config"中配置的nacos地址不同
server-addr: localhost:8848
# 设置需要读取的命名空间的ID
namespace: 231e01ec-912b-4529-9ff4-6bf792b38776
# 设置配置文件的分组
group: TEST_GROUP
# 连接nacos的用户名
username: nacos
# 连接nacos的密码
password: nacos
# 配置注册项,配置内容与服务端保持一致(这里连接的是seata命名空间)
registry:
type: nacos
nacos:
# 设置连接的nacos服务器地址
# 这里必须与SeataServer服务器连接的nacos地址一致,测试服务器中配置的命名空间为seata,这里也连接为seata空间才能实现服务发现
server-addr: localhost:8848
# 设置命名空间ID:这里必须与SeataServer服务器配置的命名空间一致
namespace: c511f4d4-0205-4316-909a-d0827ade5435
# TC服务在nacos中注册的服务名
# 默认是seata-server,若没有修改则可以不配置
application: seata-server
# 设置服务所在的分组,这里必须与SeataServer服务器配置的分组名一致,默认就是SEATA_GROUP
group: SEATA_GROUP
# 连接nacos的用户名
username: nacos
# 连接nacos的密码
password: nacos
(3)、添加配置文件
在nacos上添加配置文件"service.vgroupMapping.事务组名",配置文件的内容为seata服务注册时设置的集群名

内容

(4)、添加注解
在入口方法添加注解"@GlobalTransactional"
(5)、开启TCC事务
在每个分支事务中使用@LocalTCC注解开启并提供对应的业务、提交、回滚方法
二、SAGA模式
1、Saga模式
(1)、Saga模式原理
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交
如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态

Saga也分为两个阶段
-
一阶段:直接提交本地事务
-
二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
(2)、执行过程
- 执行正向操作:按照事务的逻辑顺序,依次执行正向操作,每个正向操作都会记录事务的执行状态
- 如果所有的正向操作都成功执行,则事务提交完成
- 如果某个正向操作失败,将会触发相应的补偿操作,补偿操作会撤销或修复正向操作的影响
- 执行补偿操作:按照逆序依次执行已经触发的补偿操作,补偿操作应该具备幂等性,以便可以多次执行而不会造成副作用
- 如果所有的补偿操作都成功执行,则事务回滚完成
- 如果补偿操作也失败,需要人工介入或其他手段来解决事务的一致性问题
Saga模式适用于长时间运行的事务或跨多个服务进行的事务,可以降低分布式事务的复杂性,Saga模式的优点在于能够在发生故障或异常时进行局部回滚,而不需要回滚整个事务,然而,Saga模式也存在一些挑战,如补偿操作的实现和管理、事务执行的顺序控制等
(3)、优缺点
-
优点
事务参与者可以基于事件驱动实现异步调用,吞吐高
一阶段直接提交事务,无锁,性能好
不用编写TCC中的三个阶段,实现简单
-
缺点
软状态持续时间不确定,时效性差
没有锁,没有事务隔离,会有脏写
2、Seata的Saga模式
Seata的Saga模式通过Seata框架来管理和协调分布式事务,提供了对事务的编排和状态管理的支持
它与Seata的其他特性(如AT模式、TCC模式)结合在一起,构成了Seata全面的分布式事务解决方案
Seata的Saga模式相对于传统的Saga模式,具有以下特点
-
集成性
Seata的Saga模式与Seata框架紧密集成,可以与Seata的其他特性一起使用,如分布式事务日志和分布式锁等
-
强一致性
Seata的Saga模式提供了强一致性的事务支持,确保事务的执行顺序和一致性
-
可靠性
Seata的Saga模式在补偿操作的执行过程中,支持重试和恢复机制,提高了事务的可靠性和恢复能力
更多推荐



所有评论(0)