Seata TCC模式 1)TCC介绍 TCC 基于分布式事务中的二阶段提交协议实现,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel) ,他们的具体含义如下:
Try:对业务资源的检查,确定资源足够操作并预留。
注意:一旦try成功,就一定会commit成功,try失败了就进行cancel回滚。
Confirm:对业务处理进行提交,即 commit 操作,只要 Try 成功,那么该步骤一定成功;
Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放。
1.1 TCC与XA的区别
XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。
在整个全局事务没结束之前,XA会一直持有数据库的锁,不会释放掉
TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。
TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂 ,但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理,更好地解决了在各种复杂业务场景下的分布式事务问题。
2)TCC 三个步骤 2.1 try-commit 尝试预留并提交 try 阶段首先进行检查资源是否足够操作,然后预留资源,然后在 commit 阶段扣除资源 。
注意:一旦try成功,就一定会commit成功,如果try失败了就会执行cancel回滚。
如下图:
2.2 try-cancel 尝试预留失败回滚 try 阶段首先进行预留资源,预留资源时扣减库存失败导致全局事务回滚,在 cancel 阶段释放资源。如下图:
3)TCC模式
TCC模式下一样有TC、TM、RM,而他们的执行流程如下:
RM告知TC发起全局事务,TC返回XID给 事务发起者
一阶段
RM向TC 发起Prepare 预备请求,注册本地事务到TC,注册成功后报告本地分支状态(Branch Status Report)
二阶段
TM判断所有的RM 第一阶段都提交成功的话,TM就会告知TC,让TC驱动所有RM对本地事务进行commit
如果TM判断第一阶段内有RM出异常了,TM就会告知TC,让TC驱动所有RM对本地事务进行rollback
在Seata中,AT模式与TCC模式事实上都是两阶段提交 的模式,他们的区别在于:
AT 模式基于 支持本地 ACID 事务的关系型数据库实现:
一阶段 prepare 行为:在本地事务中,提交数据更新同时也会更新回滚日志记录。
二阶段commit行为:commit成功后,通过异步批量清理回滚日志。
二阶段rollback行为:通过一阶段的回滚日志 完成数据回滚。
而TCC模式下的一阶段prepare、二阶段commit、rollback都需要自己通过业务代码去实现 ,无法在依赖于数据库和undo_log表。
UseTCCFence=true,该字段用于记录TCC全局事务执行到了哪个阶段。
4)TCC模式下常见的异常问题 在TCC模式下可能会出现各种异常问题,最常见的有 空回滚、幂等、空悬挂,而在seata框架在1.5版本完美解决这些问题。
4.1 空回滚 空回滚指的是在分布式事务中,因为某种原因导致参与者第一阶段try没有执行的情况下,TM驱动二阶段导致参与者调用了cancel方法。
4.1.1 空回滚是如何产生的
在全局事务开启的情况下,参与者A注册完本地分支后的就会进入一阶段的try,但是此时由于宕机等异常导致一阶段没有执行,而TM检测到参与者一阶段异常的话就会通知TC,由TC会驱动所有参与者进行cancel,从而导致参与者A的空回滚。
4.1.2 seata如何防止空回滚 如果想要防止空回滚,在cancel之前我们要先判断try是否正常执行,如果try没有执行的话,是不允许进行cancel的。
Seata 的做法是新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。
4.2 幂等问题 幂等问题指的是TC 重复进行二阶段提交,如Confirm/Cancel接口 都需要支持幂等,确保资源不会重复提交或释放。
4.2.1 幂等问题是如何产生的
当我们发起全局事务后,参与者A执行完二阶段后,由于网络或者其他问题,TC收不到参与者A二阶段的执行结果,TC会重复驱动参与者A进行二阶段操作,直到参与者返回二阶段执行成功。
4.2.2 seata如何解决幂等 我们在解决空回滚时,新增了一张TCC事务控制表,而解决幂等则是在该表中添加一个状态字段 status;
tried:1
committed:2
rollbacked:3
二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。
4.3 空悬挂问题 悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行 ,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。
4.3.1 空悬挂是如何产生的
参与者A在执行try 一阶段时超时了,而TM基于seata的超时机制,TM会通知TC,TC驱动所有参与者回滚。当参与者A回滚成功之后,之前的try才开始执行,就会导致资源被预留,从而导致悬挂。
4.3.2 seata如何防止空悬挂 TCC事务控制表的status字段中新增一个状态:
当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录 ,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行成功。
Spring Cloud Alibaba整合Seata TCC实战
本次实验主要模拟商城下单逻辑,由三个微服务组成:
订单服务:用户访问下单接口后,开启全局事务,try 创建订单,commit 修改订单状态为支付成功,rollback 订单状态为 支付失败
库存服务:订单服务通过feign调用库存扣减服务,try 扣减库存,commit 直接返回true,rollback 退回库存
账户服务:订单服务通过feign调用账户扣减服务,try 扣减余额,commit 直接返回true,rollback 退回库存
搭建流程:
搭建父工程,父工程指定springCloudAlibaba版本和springBoot版本,mysql和mybatis plus,seata 等依赖。
搭建订单微服务、账户微服务、库存微服务,并且在订单微服务上面创建账户、库存微服务对应的feign service
在订单、账户、库存微服务上面,定义TCC接口,接口包括:try、commit、rollback。
测试成功情况:try余额扣减成功,进行commit,commit成功。
测试失败情况:try余额扣减失败,进行rollback,rollback成功。
1)前置环境 1.1 父工程环境
实验工程Demo:https://files.javaxing.com/seata/spring-cloud-seata-demo.zip
实验用到的nacos,seata 版本和搭建方法 在下面我会贴出来,具体可以往下翻。
Spring Cloud Alibaba Version
Spring Cloud Version
Spring Boot Version
Seata Version
2.2.8.RELEASE
Spring Cloud Hoxton.SR12
2.3.12.RELEASE
1.5.1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 <?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 > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.3.12.RELEASE</version > <relativePath /> </parent > <groupId > com.javaxing</groupId > <artifactId > spring-cloud-seata-demo</artifactId > <version > 1.0-SNAPSHOT</version > <packaging > pom</packaging > <modules > <module > spring-cloud-seata-order</module > <module > spring-cloud-seata-account</module > <module > spring-cloud-seata-stock</module > </modules > <properties > <java.version > 1.8</java.version > <spring-cloud.version > Hoxton.SR12</spring-cloud.version > <spring-cloud-alibaba.version > 2.2.8.RELEASE</spring-cloud-alibaba.version > <seata.version > 1.5.1</seata.version > <mysql-jdbc.version > 8.0.29</mysql-jdbc.version > <mybatis.version > 2.1.1</mybatis.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.2.8</version > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-seata</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.3.1</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency > </dependencies > <dependencyManagement > <dependencies > <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 > 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 > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > <version > ${mysql-jdbc.version}</version > </dependency > <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > ${mybatis.version}</version > </dependency > </dependencies > </dependencyManagement > </project >
1.2 启动nacos nacos怎么安装,这里可以看之前的文章《Nacos配置中心使用之Nacos 微服务注册中心搭建》
地址:https://www.javaxing.com/2023/04/01/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E9%80%89%E5%9E%8B%E5%92%8CNacos%E9%9B%86%E7%BE%A4%E6%90%AD%E5%BB%BA/
1 [root@S1 bin]# sh startup.sh -m standalone
1.3 seata tc端 启动seata TC端,并且指定使用nacos作为配置中心和注册中心,具体的启动和配置过程,可以看本文章的《Seata AT模式实战之Seata Server(TC)环境搭建》。
配置中心
注册中心
2)订单微服务
order-pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?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.javaxing</groupId > <artifactId > spring-cloud-seata-demo</artifactId > <version > 1.0-SNAPSHOT</version > </parent > <artifactId > spring-cloud-seata-order</artifactId > <packaging > pom</packaging > <modules > <module > spring-cloud-seata-order-api</module > <module > spring-cloud-seata-order-biz</module > </modules > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > </project >
mysql数据库
订单表
1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE `order_tbl` ( `id` bigint NOT NULL, `user_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, `commodity_code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, `count` int DEFAULT '0', `money` decimal(10,2) DEFAULT '0.00', `status` int DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; INSERT INTO `tcc_account`.`account_tbl` (`id`, `user_id`, `money`) VALUES (1, '10000', 100.00);
TccFenceLog表
TCC 模式中存在的三大问题是幂等、悬挂和空回滚 。在 Seata1.5.1 版本中,增加了一张事务控制表,表名是 tcc_fence_log 来解决这个问题。在@TwoPhaseBusinessAction 注解中提到的属性 useTCCFence 就是来指定是否开启这个机制,这个属性值默认是 false。
1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE `tcc_fence_log` ( `xid` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'global id', `branch_id` bigint NOT NULL COMMENT 'branch id', `action_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'action name', `status` tinyint NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)', `gmt_create` datetime(3) NOT NULL COMMENT 'create time', `gmt_modified` datetime(3) NOT NULL COMMENT 'update time', PRIMARY KEY (`xid`,`branch_id`) USING BTREE, KEY `idx_gmt_modified` (`gmt_modified`) USING BTREE, KEY `idx_status` (`status`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
注意:未避免幂等、悬挂、空回滚,在每个微服务中,都要有一张这样的表,因为每个微服务都会涉及到try,commit,rollback。
2.1 order-api
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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.javaxing</groupId > <artifactId > spring-cloud-seata-order</artifactId > <version > 1.0-SNAPSHOT</version > </parent > <artifactId > spring-cloud-seata-order-api</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > </project >
entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.javaxing.order.entity;public enum OrderStatus { INIT(0 ), SUCCESS(1 ), FAIL(-1 ); private final int value; OrderStatus(int value) { this .value = value; } public int getValue () { return value; } }
vo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.javaxing.order.vo;import lombok.Data;import java.math.BigDecimal;@Data public class OrderVO { private Long orderId; private String userId; private String commodityCode; private Integer count; private BigDecimal money; }
2.2 order-biz pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > com.javaxing</groupId > <artifactId > spring-cloud-seata-order</artifactId > <version > 1.0-SNAPSHOT</version > </parent > <groupId > com.javaxing</groupId > <artifactId > spring-cloud-seata-order-biz</artifactId > <version > 0.0.1-SNAPSHOT</version > <name > spring-cloud-seata-order-biz</name > <description > spring-cloud-seata-order-biz</description > <properties > <java.version > 1.8</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > <dependency > <groupId > com.javaxing</groupId > <artifactId > spring-cloud-seata-order-api</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
微服务bootstrap.yml中添加seata配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 server: port: 8002 spring: application: name: order cloud: nacos: discovery: server-addr: 10.211 .55 .12 :8848 config: server-addr: 10.211 .55 .12 :8848 file-extension: yaml datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123123 url: jdbc:mysql://10.211.55.12:3306/tcc_order?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=America/Los_Angeles&allowMultiQueries=true&allowPublicKeyRetrieval=true mybatis-plus: mapper-locations: classpath:/mapperxml/*Mapper.xml seata: application-id: ${spring.application.name} tx-service-group: default_tx_group registry: type: nacos nacos: application: seata-server server-addr: 10.211 .55 .12 :8848 namespace: group: SEATA_GROUP config: type: nacos nacos: server-addr: 10.211 .55 .12 :8848 namespace: e56de5d4-a37e-49c0-b613-f85a7f67cb36 group: SEATA_GROUP data-id: seataServer.properties
注意:请确保client与server的注册中心和配置中心namespace和group一致。
controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.javaxing.order.controller;import com.javaxing.order.service.BussinessService;import com.javaxing.order.vo.OrderVO;import com.javaxing.order.entity.OrderTbl;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController @RequestMapping("/order") public class OrderController { @Resource private BussinessService bussinessService; @PostMapping("/addOrder") public OrderTbl addOrder (@RequestBody OrderVO vo) { return bussinessService.addOrder(vo); } }
entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.javaxing.order.entity;import lombok.Data;import java.io.Serializable;import java.math.BigDecimal;@Data public class OrderTbl implements Serializable { private Long id; private String userId; private String commodityCode; private Integer count; private BigDecimal money; private Integer status; private static final long serialVersionUID = 1L ; }
service
BussinessService:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.javaxing.order.service;import com.javaxing.order.entity.OrderTbl;import com.javaxing.order.vo.OrderVO;public interface BussinessService { OrderTbl addOrder (OrderVO vo) ; }
OrderTblService:
TCC相关注解如下:
@LocalTCC 适用于SpringCloud+Feign模式下的TCC ,@LocalTCC一定需要注解在接口上,此接口可以是寻常的业务接口,只要实现了TCC的两阶段提交对应方法便可
@TwoPhaseBusinessAction 注解try方法,其中name为当前tcc方法的bean名称,写方法名便可(全局唯一),commitMethod指向提交方法,rollbackMethod指向事务回滚方法 。
指定好三个方法之后,seata会根据全局事务的成功或失败,去帮我们自动调用提交方法或者回滚方法。
@BusinessActionContextParameter 注解可以将参数传递到二阶段(commitMethod/rollbackMethod)的方法。
BusinessActionContext 便是指TCC事务上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.javaxing.order.service;import com.javaxing.order.vo.OrderVO;import com.javaxing.order.entity.OrderTbl;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 OrderTblService { @TwoPhaseBusinessAction(name = "prepareSaveOrder", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true) OrderTbl prepareSaveOrder (OrderVO vo,@BusinessActionContextParameter(paramName = "orderId") Long orderId) ; boolean commit (BusinessActionContext actionContext) ; boolean rollback (BusinessActionContext actionContext) ; }
accountFeignService 账户feign接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.javaxing.order.feign;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import java.math.BigDecimal;@FeignClient(name = "account",path = "/account") public interface AccountFeignService { @GetMapping("/deduct") public void deduct (@RequestParam("userId") String userId, @RequestParam("money") BigDecimal money) ; }
stockFeignService 库存feign接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.javaxing.order.feign;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;@FeignClient(name = "stock",path = "/stock") public interface StockFeignService { @GetMapping("/deduct") public void deduct (@RequestParam("commodityCode") String commodityCode,@RequestParam("count") Integer count) ; }
serviceImpl
BusinessServiceImpl:
在全局事务发起者中添加@GlobalTransactional注解,该注解会让订单微服务开启全局事务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.javaxing.order.service.impl;import com.javaxing.order.entity.OrderTbl;import com.javaxing.order.feign.AccountFeignService;import com.javaxing.order.feign.StockFeignService;import com.javaxing.order.service.BussinessService;import com.javaxing.order.service.OrderTblService;import com.javaxing.order.vo.OrderVO;import io.seata.core.context.RootContext;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 BusinessServiceImpl implements BussinessService { @Autowired private OrderTblService orderTblService; @Autowired private AccountFeignService accountFeignService; @Autowired private StockFeignService stockFeignService; @Override @GlobalTransactional(name="addOrder",rollbackFor=Exception.class) public OrderTbl addOrder (OrderVO vo) { log.info("=============用户下单=================" ); log.info("当前 XID: {}" , RootContext.getXID()); Long orderId = System.currentTimeMillis(); OrderTbl order = orderTblService.prepareSaveOrder(vo,orderId); stockFeignService.deduct(vo.getCommodityCode(),vo.getCount()); accountFeignService.deduct(vo.getUserId(),vo.getMoney()); return order; } }
OrderTblServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package com.javaxing.order.service.impl;import java.util.List;import com.javaxing.order.entity.OrderStatus;import com.javaxing.order.feign.AccountFeignService;import com.javaxing.order.feign.StockFeignService;import com.javaxing.order.vo.OrderVO;import io.seata.rm.tcc.api.BusinessActionContext;import io.seata.rm.tcc.api.BusinessActionContextParameter;import io.seata.spring.annotation.GlobalTransactional;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import javax.annotation.Resource;import com.javaxing.order.entity.OrderTbl;import com.javaxing.order.mapper.OrderTblMapper;import com.javaxing.order.service.OrderTblService;import org.springframework.transaction.annotation.Transactional;@Service @Slf4j public class OrderTblServiceImpl implements OrderTblService { @Resource private OrderTblMapper orderTblMapper; @Override @Transactional(rollbackFor=Exception.class) public OrderTbl prepareSaveOrder (OrderVO vo, @BusinessActionContextParameter(paramName = "orderId") Long orderId) { OrderTbl orderTbl = new OrderTbl (); orderTbl.setId(orderId); orderTbl.setUserId(vo.getUserId()); orderTbl.setCount(vo.getCount()); orderTbl.setMoney(vo.getMoney()); orderTbl.setStatus(OrderStatus.INIT.getValue()); orderTbl.setCommodityCode(vo.getCommodityCode()); int i = orderTblMapper.insertSelective(orderTbl); log.info("保存订单{}" , i > 0 ? "成功" : "失败" ); return orderTbl; } @Override public boolean commit (BusinessActionContext actionContext) { orderTblMapper.updateStatusById(OrderStatus.SUCCESS.getValue(),Long.parseLong(actionContext.getActionContext("orderId" ).toString())); log.info("commit成功,订单状态为成功" ); return true ; } @Override public boolean rollback (BusinessActionContext actionContext) { orderTblMapper.updateStatusById(OrderStatus.FAIL.getValue(),Long.parseLong(actionContext.getActionContext("orderId" ).toString())); log.info("回滚成功,订单状态为失败" ); return true ; } }
值得注意的是,切勿在impl实现类里面调用本类的service接口来实现TCC,否则不会生效,如:在orderImpl实现类里面注入orderService调用TCC的try和commit、rollback。
3)账户微服务
account-pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?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.javaxing</groupId > <artifactId > spring-cloud-seata-demo</artifactId > <version > 1.0-SNAPSHOT</version > </parent > <artifactId > spring-cloud-seata-account</artifactId > <packaging > pom</packaging > <modules > <module > spring-cloud-seata-account-api</module > <module > spring-cloud-seata-account-biz</module > </modules > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > </project >
mysql数据库
账户表
1 2 3 4 5 6 CREATE TABLE `account_tbl` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, `money` decimal(10,2) DEFAULT '0.00', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3;
TccFenceLog表
TCC 模式中存在的三大问题是幂等、悬挂和空回滚 。在 Seata1.5.1 版本中,增加了一张事务控制表,表名是 tcc_fence_log 来解决这个问题。在@TwoPhaseBusinessAction 注解中提到的属性 useTCCFence 就是来指定是否开启这个机制,这个属性值默认是 false。
1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE `tcc_fence_log` ( `xid` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'global id', `branch_id` bigint NOT NULL COMMENT 'branch id', `action_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'action name', `status` tinyint NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)', `gmt_create` datetime(3) NOT NULL COMMENT 'create time', `gmt_modified` datetime(3) NOT NULL COMMENT 'update time', PRIMARY KEY (`xid`,`branch_id`) USING BTREE, KEY `idx_gmt_modified` (`gmt_modified`) USING BTREE, KEY `idx_status` (`status`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
注意:未避免幂等、悬挂、空回滚,在每个微服务中,都要有一张这样的表,因为每个微服务都会涉及到try,commit,rollback。
3.1 account-api pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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.javaxing</groupId > <artifactId > spring-cloud-seata-account</artifactId > <version > 1.0-SNAPSHOT</version > </parent > <artifactId > spring-cloud-seata-account-api</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > </project >
3.2 account-biz pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > com.javaxing</groupId > <artifactId > spring-cloud-seata-account</artifactId > <version > 1.0-SNAPSHOT</version > </parent > <groupId > com.javaxing</groupId > <artifactId > spring-cloud-seata-account-biz</artifactId > <version > 0.0.1-SNAPSHOT</version > <name > spring-cloud-seata-account-biz</name > <description > spring-cloud-seata-account-biz</description > <properties > <java.version > 1.8</java.version > </properties > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
微服务bootstrap.yml中添加seata配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 server: port: 8001 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123123 url: jdbc:mysql://10.211.55.12:3306/tcc_account?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=America/Los_Angeles&allowMultiQueries=true&allowPublicKeyRetrieval=true application: name: account cloud: nacos: discovery: server-addr: 10.211 .55 .12 :8848 config: server-addr: 10.211 .55 .12 :8848 file-extension: yaml mybatis-plus: mapper-locations: classpath:/mapperxml/*Mapper.xml seata: application-id: ${spring.application.name} tx-service-group: default_tx_group registry: type: nacos nacos: application: seata-server server-addr: 10.211 .55 .12 :8848 namespace: group: SEATA_GROUP config: type: nacos nacos: server-addr: 10.211 .55 .12 :8848 namespace: e56de5d4-a37e-49c0-b613-f85a7f67cb36 group: SEATA_GROUP data-id: seataServer.properties
注意:请确保client与server的注册中心和配置中心namespace和group一致。
controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.javaxing.account.controller;import com.javaxing.account.service.AccountTblService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import java.math.BigDecimal;@RestController @RequestMapping("/account") public class accountController { @Autowired private AccountTblService accountTblService; @GetMapping("/deduct") public void deduct (@RequestParam("userId") String userId,@RequestParam("money") BigDecimal money) { accountTblService.deduct(userId,money); } }
entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.javaxing.account.entity;import lombok.Data;import java.io.Serializable;import java.math.BigDecimal;@Data public class AccountTbl implements Serializable { private Integer id; private String userId; private BigDecimal money; private static final long serialVersionUID = 1L ; }
service
AccountTblService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.javaxing.account.service;import com.javaxing.account.entity.AccountTbl;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;import java.math.BigDecimal;@LocalTCC public interface AccountTblService { @TwoPhaseBusinessAction(name = "deduct", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true) public void deduct (@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money") BigDecimal money) ; boolean commit (BusinessActionContext actionContext) ; boolean rollback (BusinessActionContext actionContext) ; }
serviceImpl
AccountTblServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.javaxing.account.service.impl;import io.seata.core.context.RootContext;import io.seata.rm.tcc.api.BusinessActionContext;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;import javax.annotation.Resource;import com.javaxing.account.entity.AccountTbl;import com.javaxing.account.mapper.AccountTblMapper;import com.javaxing.account.service.AccountTblService;import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;@Service @Slf4j public class AccountTblServiceImpl implements AccountTblService { @Resource private AccountTblMapper accountTblMapper; @Override @Transactional public void deduct (String userId, BigDecimal money) { log.info("当前 XID: {}" , RootContext.getXID()); AccountTbl accountTbl = accountTblMapper.findFirstByUserId(userId); if (money.compareTo(accountTbl.getMoney()) == 1 ) { throw new RuntimeException ("余额不足" ); } log.info("余额扣减成功,扣减余额:{},用户ID:{}" , money, accountTbl.getUserId()); accountTblMapper.deductBalance(userId, money); log.info("余额扣减成功,剩余余额:{},用户ID:{}" , accountTbl.getMoney().subtract(money), accountTbl.getUserId()); } @Override public boolean commit (BusinessActionContext actionContext) { log.info("commit成功" ); return true ; } @Override public boolean rollback (BusinessActionContext actionContext) { String userId = actionContext.getActionContext("userId" ).toString(); BigDecimal money = new BigDecimal (actionContext.getActionContext("money" ).toString()); accountTblMapper.addBalance(userId, money); log.info("余额回滚成功" ); return true ; } }
值得注意的是,切勿在impl实现类里面调用本类的service接口来实现TCC,否则不会生效,如:在orderImpl实现类里面注入orderService调用TCC的try和commit、rollback。
mapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.javaxing.account.mapper;import org.apache.ibatis.annotations.Param;import com.javaxing.account.entity.AccountTbl;import org.apache.ibatis.annotations.Mapper;import java.math.BigDecimal;@Mapper public interface AccountTblMapper { AccountTbl findFirstByUserId (@Param("userId") String userId) ; int deductBalance (@Param("userId") String userId, @Param("money") BigDecimal money) ; int addBalance (@Param("userId") String userId, @Param("money") BigDecimal money) ; }
mapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <?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.javaxing.account.mapper.AccountTblMapper" > <resultMap id ="BaseResultMap" type ="com.javaxing.account.entity.AccountTbl" > <id column ="id" jdbcType ="INTEGER" property ="id" /> <result column ="user_id" jdbcType ="VARCHAR" property ="userId" /> <result column ="money" jdbcType ="DECIMAL" property ="money" /> </resultMap > <sql id ="Base_Column_List" > id, user_id, money </sql > <select id ="findFirstByUserId" resultMap ="BaseResultMap" > select <include refid ="Base_Column_List" /> from account_tbl where user_id=#{userId,jdbcType=VARCHAR} limit 1 </select > <update id ="deductBalance" > update account_tbl set money = money - #{money,jdbcType=DECIMAL} where user_id = #{userId,jdbcType=VARCHAR} </update > <update id ="addBalance" > update account_tbl set money = money + #{money,jdbcType=DECIMAL} where user_id = #{userId,jdbcType=VARCHAR} </update > </mapper >
4)库存微服务
stock-pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?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.javaxing</groupId > <artifactId > spring-cloud-seata-demo</artifactId > <version > 1.0-SNAPSHOT</version > </parent > <artifactId > spring-cloud-seata-stock</artifactId > <packaging > pom</packaging > <modules > <module > spring-cloud-seata-stock-api</module > <module > spring-cloud-seata-stock-biz</module > </modules > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > </project >
mysql数据库
库存表
1 2 3 4 5 6 7 8 9 CREATE TABLE `stock_tbl` ( `id` int NOT NULL AUTO_INCREMENT, `commodity_code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, `count` int DEFAULT '0', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `commodity_code` (`commodity_code`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3; INSERT INTO `tcc_stock`.`stock_tbl` (`id`, `commodity_code`, `count`) VALUES (1, '20230101', 100);
TccFenceLog表
TCC 模式中存在的三大问题是幂等、悬挂和空回滚 。在 Seata1.5.1 版本中,增加了一张事务控制表,表名是 tcc_fence_log 来解决这个问题。在@TwoPhaseBusinessAction 注解中提到的属性 useTCCFence 就是来指定是否开启这个机制,这个属性值默认是 false。
1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE `tcc_fence_log` ( `xid` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'global id', `branch_id` bigint NOT NULL COMMENT 'branch id', `action_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'action name', `status` tinyint NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)', `gmt_create` datetime(3) NOT NULL COMMENT 'create time', `gmt_modified` datetime(3) NOT NULL COMMENT 'update time', PRIMARY KEY (`xid`,`branch_id`) USING BTREE, KEY `idx_gmt_modified` (`gmt_modified`) USING BTREE, KEY `idx_status` (`status`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
注意:未避免幂等、悬挂、空回滚,在每个微服务中,都要有一张这样的表,因为每个微服务都会涉及到try,commit,rollback。
4.1 stock-api pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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.javaxing</groupId > <artifactId > spring-cloud-seata-stock</artifactId > <version > 1.0-SNAPSHOT</version > </parent > <artifactId > spring-cloud-seata-stock-api</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > </project >
4.2 stock-biz pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > com.javaxing</groupId > <artifactId > spring-cloud-seata-stock</artifactId > <version > 1.0-SNAPSHOT</version > </parent > <groupId > com.javaxing</groupId > <artifactId > spring-cloud-seata-stock-biz</artifactId > <version > 0.0.1-SNAPSHOT</version > <properties > <java.version > 1.8</java.version > </properties > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
微服务bootstrap.yml中添加seata配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 server: port: 8003 spring: application: name: stock cloud: nacos: discovery: server-addr: 10.211 .55 .12 :8848 config: server-addr: 10.211 .55 .12 :8848 file-extension: yaml datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123123 url: jdbc:mysql://10.211.55.12:3306/tcc_stock?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=America/Los_Angeles&allowMultiQueries=true&allowPublicKeyRetrieval=true mybatis-plus: mapper-locations: classpath:/mapperxml/*Mapper.xml seata: application-id: ${spring.application.name} tx-service-group: default_tx_group registry: type: nacos nacos: application: seata-server server-addr: 10.211 .55 .12 :8848 namespace: group: SEATA_GROUP config: type: nacos nacos: server-addr: 10.211 .55 .12 :8848 namespace: e56de5d4-a37e-49c0-b613-f85a7f67cb36 group: SEATA_GROUP data-id: seataServer.properties
注意:请确保client与server的注册中心和配置中心namespace和group一致。
controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.javaxing.stock.controller;import com.javaxing.stock.service.StockTblService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/stock") public class StockController { @Autowired private StockTblService stockTblService; @GetMapping("/deduct") public void deduct (@RequestParam("commodityCode") String commodityCode,@RequestParam("count") Integer count) { stockTblService.deduct(commodityCode,count); } }
entity
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.javaxing.stock.entity;import lombok.Data;import java.io.Serializable;@Data public class StockTbl implements Serializable { private Integer id; private String commodityCode; private Integer count; private static final long serialVersionUID = 1L ; }
service
StockTblService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.javaxing.stock.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 StockTblService { @TwoPhaseBusinessAction(name = "deduct", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true) void deduct (@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode, @BusinessActionContextParameter(paramName = "count") int count) ; boolean commit (BusinessActionContext actionContext) ; boolean rollback (BusinessActionContext actionContext) ; }
serviceImpl
StockTblServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package com.javaxing.stock.service.impl;import io.seata.core.context.RootContext;import io.seata.rm.tcc.api.BusinessActionContext;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;import javax.annotation.Resource;import com.javaxing.stock.entity.StockTbl;import com.javaxing.stock.mapper.StockTblMapper;import com.javaxing.stock.service.StockTblService;import org.springframework.transaction.annotation.Transactional;@Service @Slf4j public class StockTblServiceImpl implements StockTblService { @Resource private StockTblMapper stockTblMapper; @Transactional @Override public void deduct (String commodityCode, int count) { log.info("当前 XID: {}" , RootContext.getXID()); StockTbl stockTbl = stockTblMapper.findFirstByCommodityCode(commodityCode); if (stockTbl.getCount() < count){ throw new RuntimeException ("库存不足" ); } stockTblMapper.deductCount(commodityCode,count); log.info("库存扣减成功,产品编号:{},扣减数量:{}" ,commodityCode,count); } @Override public boolean commit (BusinessActionContext actionContext) { log.info("库存commit成功" ); return true ; } @Override public boolean rollback (BusinessActionContext actionContext) { String commodityCode = actionContext.getActionContext("commodityCode" ).toString(); int count = (int ) actionContext.getActionContext("count" ); stockTblMapper.addCount(commodityCode,count); log.info("库存回滚成功" ); return true ; } }
值得注意的是,切勿在impl实现类里面调用本类的service接口来实现TCC,否则不会生效,如:在orderImpl实现类里面注入orderService调用TCC的try和commit、rollback。
mapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.javaxing.stock.mapper;import org.apache.ibatis.annotations.Param;import com.javaxing.stock.entity.StockTbl;import org.apache.ibatis.annotations.Mapper;@Mapper public interface StockTblMapper { StockTbl findFirstByCommodityCode (@Param("commodityCode") String commodityCode) ; int deductCount (@Param("commodityCode") String commodityCode, @Param("count") Integer count) ; int addCount (@Param("commodityCode") String commodityCode, @Param("count") Integer count) ; }
mapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?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.javaxing.stock.mapper.StockTblMapper" > <resultMap id ="BaseResultMap" type ="com.javaxing.stock.entity.StockTbl" > <id column ="id" jdbcType ="INTEGER" property ="id" /> <result column ="commodity_code" jdbcType ="VARCHAR" property ="commodityCode" /> <result column ="count" jdbcType ="INTEGER" property ="count" /> </resultMap > <sql id ="Base_Column_List" > id, commodity_code, `count` </sql > <select id ="findFirstByCommodityCode" resultMap ="BaseResultMap" > select <include refid ="Base_Column_List" /> from stock_tbl where commodity_code=#{commodityCode,jdbcType=VARCHAR} limit 1 </select > <update id ="deductCount" > update stock_tbl set `count` = count - #{count,jdbcType=INTEGER} where commodity_code = #{commodityCode,jdbcType=VARCHAR} </update > <update id ="addCount" > update stock_tbl set `count` = count + #{count,jdbcType=INTEGER} where commodity_code = #{commodityCode,jdbcType=VARCHAR} </update > </mapper >
5)模拟成功实验
TCC执行流程
订单微服务(TM&RM)向TC注册全局事务,TC返回XID给TM,而账户、库存微服务(RM)向TC注册本地事务,TC返回BID给RM。
一阶段
订单微服务 在一阶段try 创建订单,并通过feign 调用库存、账户服务 ,在一阶段try 扣减库存和账户余额。
二阶段
TM如果检测到RM 在一阶段抛出异常的话,TM就会告知TC,由TC驱动所有RM对本地事务进行rollback
TM如果检测到RM 在一阶段都try成功,TM就会告知TC,由TC驱动所有RM对本地事务进行commit
实验开始之前,我们先来看一下数据库表数据:
账户表余额:100,库存表:100,订单表:无。
5.1 访问addOrder方法
通过接口下单时,扣除库存1,金额10,库存和金额都校验成功,扣减成功。
5.2 微服务日志 订单微服务
账户微服务
库存微服务
5.3 查看执行后的数据库数据
6)模拟失败实验 实验思路
我们此时余额只有90,但是我们一阶段try却强行扣除1000余额,账户微服务自然会抛出异常-余额不足,此时我们订单服务已经创建订单,status = 0 ,库存服务已经扣去了相应的库存。而TM在二阶段时检测到一阶段有RM执行异常了,就会通知TC 告知所有RM对本地事务进行回滚。
执行方法
订单服务
try成功了,库存服务也try成功,但 账户服务try失败了,所以TM发现有RM一阶段异常了,TM告知TC,由TC驱动所有RM,订单服务和库存服务进行rollback,而账户服务try失败了,直接本地事务回滚了,不需要再走rollback。
库存服务
账户服务
数据库结果
账户余额依旧是90,库存也是99,而订单我们在rollback环节,并没有删除订单,只是把status改成了-1 ,综上所述,实验圆满结束。