分布式事务–SEATA–AT和TCC模式
seata官网:seata.io
Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务
达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务,下
图是全局事务与分支事务的关系图:
Seata定义了三个组件来处理分布式事务:
Transaction Coordinator (TC): 事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运
行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚。
Transaction Manager ™: 事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终
向TC发起全局提交或全局回滚的指令。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分
支(本地)事务的提交和回滚。
1.下载、配置并启动TC
官方教程地址:https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html
seata的github地址https://github.com/seata/seata/tree/1.3.0,
TC下载地址:https://github.com/seata/seata/releases
首先把seata-server的启动脚本下载下来。
bin目录里是启动脚本,conf目录是一些配置文件。
Server端存储模式(store.mode)现有file、db、redis三种(后续将引入raft,mongodb),file模式无需改动,直接启动即可。
注: file模式为单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高;
db模式为高可用模式,全局事务会话信息通过db共享,相应性能差些;
redis模式Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,需要配置合适当前场景的redis持久化配置.
接下来细说db模式:https://github.com/seata/seata/tree/1.2.0/script/ 这里有三个目录,分别是client、config-center、server。
- client
存放client端sql脚本,参数配置
- config-center
各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件
- server
server端数据库脚本及各个容器配置
首先建表:全局事务会话信息由3块内容构成,全局事务–>分支事务–>全局锁,对应表global_table、branch_table、lock_table,对应的sql文件在server目录下的db文件夹中。
启动包: seata–>conf–>file.conf,修改store.mode=“db”
启动包: seata–>conf–>file.conf,修改store.db相关属性。
然后就是server的配置,有file 、nacos 、eureka、redis、zk、consul、etcd3、sofa供我们选择。
接下来以nacos讲解,server相关配置信息在config-center目录的config.txt中。其中具体的参数详解官网有介绍。 接下来我们需要把这个config拷贝到启动包的目录下并修改其中的一些配置。
下面是我修改完的。
然后修改启动包: seata–>conf–>registry.conf。下面是我修改完的
appliction是注册到nacos中的服务名。serverAddr是nacos的地址,然后是组名,命名空间等不过多赘述。
然后我们需要把config.txt中的内容导入到nacos中,官方提供了两个脚本。
windos环境下可以用git bash here。
然后我们就可以启动server了。启动包: seata–>bin–>seata-server.bat,
这样就启动成功了。启动之前别忘了启动nacos。我使用的nacos是1.3.0版本。
2.配置客户端:
我使用的seata1.3.0,相关版本说嘛:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
下面是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 https://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.2.RELEASE</version>
</parent>
<groupId>com.emrubik</groupId>
<artifactId>AT-TCC</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
</properties>
<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>
</dependencies>
</dependencyManagement>
<modules>
<module>order</module>
<module>account</module>
<module>storage</module>
</modules>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.3.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.3.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.14</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.3.2.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
以经典的,订单、库存、账户为例开发。
首先需要三个微服务。每个微服务的启动类都需要加上以下注解
@EnableDiscoveryClient
@EnableAutoDataSourceProxy //此注解是开启数据源自动代理
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
每个微服务都是用naocs作为注册配置中心。
所以需要把application.properties改为bootstrap.properties或bootstrap.yaml
下面是bootstrap.yaml 配置
#spring.application.name=provider-b-config
#spring.cloud.nacos.config.file-extension=yaml
#spring.cloud.nacos.config.server-addr=localhost:8848
#spring.cloud.alibaba.seata.tx-service-group=provider-b
spring:
application:
name: storage-config #naocs配置列表显示的名字是storage-config.yaml
cloud:
nacos:
config:
file-extension: yaml #naocs配置列表显示的名字的后缀,
server-addr: 10.10.20.51:8848
alibaba:
seata:
tx-service-group: provider-b #这个要全局唯一,这个是前面在config.txt中的service.vgroupMapping.my_test_tx_group=default 修改为service.vgroupMapping.provider-b=default。
下面是nacos中的配置信息:
spring:
application:
name: storage #这个是启动服务后注册到naocs中的服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848
datasource:
url: jdbc:mysql://localhost:3306/storage?serverTimezone=GMT%2B8
username: root
password: 1234
driver-class-name: com.mysql.jdbc.Driver
server:
port: 7002 #服务启动端口
management:
endpoints:
web:
exposure:
include: "*"
seata: #具体参数配置请查看官网
enabled: true
application-id: ${spring.application.name}
tx-service-group: provider-b
enable-auto-data-source-proxy: true #开启数据源自动代理
config: #配置中心
# 指明类型
type: nacos
nacos:
server-addr: "localhost:8848"
namespace: ""
group: "SEATA_GROUP"
username: ""
password: ""
registry: #注册中心
type: nacos
nacos:
application: "seata-server"
serverAddr: "localhost:8848"
group: "SEATA_GROUP"
namespace: ""
username: ""
password: ""
feign:
hystrix:
enabled: true
配置完成之后就要进行开发了。
3.业务开发
AT
//账户mapper
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
@Update("update account set money = money - #{money} where user_name = #{userName}")
void reduceMoney(String userName, BigDecimal money);
}
//库存mapper
@Mapper
public interface StorageMapper extends BaseMapper<Storage> {
@Update("update storage set commodity_count=commodity_count-#{count} where commodity_code=#{commodityCode}")
void reduceStorage(String commodityCode, Integer count);
}
//账户service
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
private AccountMapper accountMapper;
@Resource
private TccAccountService tccAccountService;
@Override
public void reduceMoney(String userName, BigDecimal money) {
log.info("扣减账户金额");
accountMapper.reduceMoney(userName,money);
if(0==money.compareTo(new BigDecimal(1000))){
throw new RuntimeException("模拟account异常"+ RootContext.getXID());
}
}
}
//库存service
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageMapper storageMapper;
@Resource
private TccStorageService tccStorageService;
@Override
public void reduceStorage(String commodityCode, Integer count) {
try {
Thread.sleep(6000);
}catch (Exception e){
throw new RuntimeException("模拟超时回滚");
}
log.info("扣减库存");
storageMapper.reduceStorage(commodityCode,count);
// if(count==2){
// throw new RuntimeException("模拟storage异常"+ RootContext.getXID());
// }
}
}
//订单service
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private AccountClient accountClient;
@Resource
private StorageClient storageClient;
@Resource
private TccOrderService ttcOrderService;
/**
* 使用了@GlobalTransactional注解要捕捉远程调用异常,如果使用了feign降级策略,还需判断远程调用是否被降级了,如果被降级了就要进行全局事务回滚。
* 可以像下面方法抛异常让TM自动回滚,也可以使用GlobalTransactionContext.reload(RootContext.getXID()).rollback();手动回滚。
* @param commodityCode
* @param commodityCount
* @param userName
* @param money
* @return
*/
@Override
@GlobalTransactional(timeoutMills = 5000,rollbackFor = Exception.class)
public String createOrder(String commodityCode, Integer commodityCount, String userName, BigDecimal money) {
log.info("创建订单");
log.info("order-XID:"+ RootContext.getXID());
OrderInfo order = new OrderInfo();
order.setCommodityCode(commodityCode);
order.setCommodityCount(commodityCount);
order.setOrderCode(UUID.randomUUID().toString());
order.setUserName(userName);
order.setMoney(money);
orderMapper.insert(order);
try {
log.info("扣减库存");
String s = storageClient.reduceStorage(commodityCode, commodityCount);
log.info("调用库存是否有异常"+s);
log.info("扣减账户金额");
accountClient.reduceMoney(userName, money);
}catch (Exception e){
throw new RuntimeException("微服务调用异常");
}
return RootContext.getXID();
}
}
我是用订单服务调用库存和账户服务。所以需要在订单的启动类加上@EnableFeignClients(basePackages = {“com.emrubik.order.client”})
//调用账户服务的client
@FeignClient(value = "account",fallback = AccountClientFallback.class)
public interface AccountClient {
@GetMapping("/account/reduce")
public String reduceMoney(@RequestParam(value = "userName") String userName,@RequestParam(value = "money") BigDecimal money) ;
@GetMapping("/account/tcc")
public String tccReduceMoney(@RequestParam(value = "userName") String userName,@RequestParam(value = "money") BigDecimal money);
}
//调用库存服务的client
@FeignClient(value = "storage")
public interface StorageClient {
@GetMapping("/storage/reduce")
public String reduceStorage(@RequestParam(value = "commodityCode") String commodityCode, @RequestParam(value = "commodityCount") Integer commodityCount) throws Exception;
@GetMapping("/storage/tcc")
public String tccReduceStorage(@RequestParam(value = "commodityCode") String commodityCode, @RequestParam(value = "commodityCount") Integer commodityCount) throws Exception;
}
controller代码很简单就不展示了。
TCC
TCC需要我们自己实现幂等性。
public class ResultHolder {
private static Map<Class<?>, Map<String, String>> map = new ConcurrentHashMap<Class<?>, Map<String, String>>();
public static void setResult(Class<?> actionClass, String xid, String v) {
Map<String, String> results = map.get(actionClass);
if (results == null) {
synchronized (map) {
if (results == null) {
results = new ConcurrentHashMap<>();
map.put(actionClass, results);
}
}
}
results.put(xid, v);
}
public static String getResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
return results.get(xid);
}
return null;
}
public static void removeResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
results.remove(xid);
}
}
}
账户的mapper
@Update("update account set money = money - #{money}, freeze_money = freeze_money + #{money} where user_name = #{userName}")
void prepareAccount(String userName, BigDecimal money);
@Update("update account set freeze_money = freeze_money - #{money} where user_name = #{userName}")
void commitAccount(String userName,BigDecimal money);
@Update("update account set money = money + #{money}, freeze_money = freeze_money - #{money} where user_name = #{userName}")
void rollbackAccount(String userName, BigDecimal money);
账户的TCC接口和实现类
@LocalTCC
public interface TccAccountService {
@TwoPhaseBusinessAction(name = "account",commitMethod = "commit",rollbackMethod = "rollback")
Boolean updata(BusinessActionContext businessActionContext, @BusinessActionContextParameter(paramName = "userName") String userName,
@BusinessActionContextParameter(paramName="money") BigDecimal money);
boolean commit(BusinessActionContext businessActionContext);
boolean rollback(BusinessActionContext businessActionContext);
@Component
@Slf4j
public class TccAccountServiceImpl implements TccAccountService {
@Resource
private AccountMapper accountMapper;
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean updata(BusinessActionContext businessActionContext, String userName, BigDecimal money) {
log.info("account准备"+businessActionContext.getXid());
accountMapper.prepareAccount(userName,money);
ResultHolder.setResult(getClass(),businessActionContext.getXid(),"order");
return true;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean commit(BusinessActionContext businessActionContext) {
if(ResultHolder.getResult(getClass(),businessActionContext.getXid())==null){
return true;
}
log.info("account提交"+businessActionContext.getXid());
String userName = businessActionContext.getActionContext("userName").toString();
BigDecimal money =new BigDecimal(businessActionContext.getActionContext("money").toString());
accountMapper.commitAccount(userName,money);
ResultHolder.removeResult(getClass(),businessActionContext.getXid());
return true;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
if(ResultHolder.getResult(getClass(),businessActionContext.getXid())==null){
return true;
}
log.info("account回滚"+businessActionContext.getXid());
String userName = businessActionContext.getActionContext("userName").toString();
BigDecimal money =new BigDecimal(businessActionContext.getActionContext("money").toString());
accountMapper.rollbackAccount(userName,money);
ResultHolder.removeResult(getClass(),businessActionContext.getXid());
return true;
}
}
库存的mapper
@Update("update storage set commodity_count=commodity_count-#{count}, freeze_count=freeze_count+#{count} where commodity_code=#{commodityCode}")
void prepareStorage(String commodityCode, Integer count);
@Update("update storage set freeze_count=freeze_count-#{count} where commodity_code=#{commodityCode}")
void commitStorage(String commodityCode, Integer count);
@Update("update storage set commodity_count=commodity_count+#{count}, freeze_count=freeze_count-#{count} where commodity_code=#{commodityCode}")
void rollbackStorage(String commodityCode, Integer count);
库存的TCC接口和实现类
@LocalTCC
public interface TccStorageService {
@TwoPhaseBusinessAction(name = "storage",commitMethod = "commit",rollbackMethod = "rollback")
Boolean updata(BusinessActionContext businessActionContext, @BusinessActionContextParameter(paramName = "commodityCode") String commodityCode,
@BusinessActionContextParameter(paramName="count") Integer count);
boolean commit(BusinessActionContext businessActionContext);
boolean rollback(BusinessActionContext businessActionContext);
}
@Component
@Slf4j
public class TccStorageServiceImpl implements TccStorageService {
@Resource
private StorageMapper storageMapper;
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean updata(BusinessActionContext businessActionContext, String commodityCode, Integer count) {
log.info("Storage准备阶段"+businessActionContext.getXid());
storageMapper.prepareStorage(commodityCode,count);
ResultHolder.setResult(getClass(),businessActionContext.getXid(),"storage");
return true;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean commit(BusinessActionContext businessActionContext) {
if(ResultHolder.getResult(getClass(),businessActionContext.getXid())==null){
return true;
}
log.info("Storage提交"+businessActionContext.getXid());
String commodityCode = businessActionContext.getActionContext("commodityCode").toString();
Integer count = Integer.parseInt(businessActionContext.getActionContext("count").toString());
storageMapper.commitStorage(commodityCode,count);
ResultHolder.removeResult(getClass(),businessActionContext.getXid());
return true;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
if(ResultHolder.getResult(getClass(),businessActionContext.getXid())==null){
return true;
}
log.info("Storage回滚"+businessActionContext.getXid());
String commodityCode = businessActionContext.getActionContext("commodityCode").toString();
Integer count = Integer.parseInt(businessActionContext.getActionContext("count").toString());
storageMapper.rollbackStorage(commodityCode,count);
ResultHolder.removeResult(getClass(),businessActionContext.getXid());
return true;
}
}
订单服务的mapper
@Update("update order_info set state= #{state} where order_code = #{orderCode}")
void commitOrder(String orderCode,String state);
@Delete("delete from order_info where order_code = #{orderCode}")
void rollbackOrder(String orderCode);
订单服务的TCC接口和实现类
@LocalTCC
public interface TccOrderService {
@TwoPhaseBusinessAction(name = "order",commitMethod = "commit",rollbackMethod = "rollback")
Boolean updata(BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "orderCode")String orderCode,
@BusinessActionContextParameter(paramName = "commodityCode")String commodityCode,
@BusinessActionContextParameter(paramName = "commodityCount")Integer commodityCount,
@BusinessActionContextParameter(paramName = "userName") String userName,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
boolean commit(BusinessActionContext businessActionContext);
boolean rollback(BusinessActionContext businessActionContext);
}
@Component
@Slf4j
public class TccOrderServiceImpl implements TccOrderService {
@Resource
private OrderMapper orderMapper;
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean updata(BusinessActionContext businessActionContext,String orderCode,String commodityCode, Integer commodityCount, String userName, BigDecimal money) {
log.info("order准备"+businessActionContext.getXid());
OrderInfo order = new OrderInfo();
order.setCommodityCode(commodityCode);
order.setCommodityCount(commodityCount);
order.setOrderCode(orderCode);
order.setUserName(userName);
order.setMoney(money);
orderMapper.insert(order);
ResultHolder.setResult(getClass(),businessActionContext.getXid(),"order");
return true;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean commit(BusinessActionContext businessActionContext) {
if(ResultHolder.getResult(getClass(),businessActionContext.getXid())==null){
return true;
}
log.info("order提交"+businessActionContext.getXid());
String orderCode = businessActionContext.getActionContext("orderCode").toString();
orderMapper.commitOrder(orderCode,"0");
ResultHolder.removeResult(getClass(),businessActionContext.getXid());
return true;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
if(ResultHolder.getResult(getClass(),businessActionContext.getXid())==null){
return true;
}
log.info("order回滚"+businessActionContext.getXid());
String orderCode = businessActionContext.getActionContext("orderCode").toString();
orderMapper.rollbackOrder(orderCode);
ResultHolder.removeResult(getClass(),businessActionContext.getXid());
return true;
}
}
然后在上面AT的service类中调用每个服务的第一阶段的方法。
@Override
public void tccReduceMonry(String userName, BigDecimal money) {
log.info("扣减账户金额");
tccAccountService.updata(null,userName,money);
if(0==money.compareTo(new BigDecimal(1000))){
throw new RuntimeException("模拟account异常"+ RootContext.getXID());
}
}
@Override
public void tccReduceStorage(String commodityCode, Integer count) {
// try {
// Thread.sleep(6000);
// }catch (Exception e){
// throw new RuntimeException("模拟超时回滚");
// }
log.info("扣减库存");
tccStorageService.updata(null,commodityCode,count);
if(count==2){
throw new RuntimeException("模拟storage异常"+ RootContext.getXID());
}
}
@GlobalTransactional(rollbackFor = Exception.class)
@Override
public void tccCreateOrder(String commodityCode, Integer commodityCount, String userName, BigDecimal money) {
log.info("创建订单");
log.info("order-XID:"+ RootContext.getXID());
ttcOrderService.updata(null,UUID.randomUUID().toString(),commodityCode,commodityCount,userName,money);
try {
log.info("扣减库存");
storageClient.tccReduceStorage(commodityCode,commodityCount);
log.info("扣减账户金额");
String s = accountClient.tccReduceMoney(userName, money);
if(s.equals("false")){
throw new RuntimeException("account失败");
}
}catch (Exception e){
throw new RuntimeException("微服务调用异常");
}
if(commodityCount==3){
throw new RuntimeException("模拟order异常");
}
}
同样就不展示controller了。
开发就到此结束了。
commodityCount, String userName, BigDecimal money) {
log.info("创建订单");
log.info("order-XID:"+ RootContext.getXID());
ttcOrderService.updata(null,UUID.randomUUID().toString(),commodityCode,commodityCount,userName,money);
try {
log.info("扣减库存");
storageClient.tccReduceStorage(commodityCode,commodityCount);
log.info("扣减账户金额");
String s = accountClient.tccReduceMoney(userName, money);
if(s.equals("false")){
throw new RuntimeException("account失败");
}
}catch (Exception e){
throw new RuntimeException("微服务调用异常");
}
if(commodityCount==3){
throw new RuntimeException("模拟order异常");
}
}
同样就不展示controller了。
开发就到此结束了。
代码地址:https://gitee.com/sunyuntian/seata1.3.0/tree/master/AT-TCC