什么是TCC
TCC是try、confirm、cancel三个单词的缩写。
- try:预留和锁定业务需要的资源。
- conirm:业务确认,当所有的分支事务都成功时执行。
- cancel:撤销。如果有一个分支事务执行失败,执行。把预留阶段的资源撤销,相当于事务回滚
执行流程图如下:
执行流程说明:
- 全局事务发起者向申请开启一个全局事务,并生成一个代表该全局事务的全局事务ID。该全局事务ID会在整个分布式调用链中传递,用于记录事务上下文,追踪和记录状态。
- 全局事务发起者调用其他服务的try方法,预留资源
- 各个事务参与者将try的执行结果返回给全局事务发起者
- 全局事务发起者根据try的执行结果决定提交或回滚
- 如果是提交的话,事务管理者调用所有分支事务的confirm方法完成事务提交;反之,事务管理者调用所有分支事务的cancel方法完成事务回滚。
第一次接触的可能对预留资源不太理解,比如我要转给你100块,那我就得先从我的账户中扣除100块,这100块就是我们预留的资源,如果转账失败的话,cancel就会将100块(预留资源)返回给我们。说白了,cancel就是执行try的反向操作。
在SQL上的表现就是:
try: update bank set balance = balance + 100 where userId = 1
cancel:update bank set balance = balance - 100 where userId = 1
举个例子:
需求:账户A转账30元给账户B,假设A和B账户在不同的微服务上。
账户A需求分析:
- try:需要确保有30元可以转,所以在try中要预留30元
- confirm:30元在try中扣除了,所以confirm中什么也不需要干
- cancle:如果业务执行失败,cancel增加30元,实现事务回滚(撤销预留资源)
账户B需求分析:
- try:账户B不需要
- confirm:整个事务执行成功,账户B需要增加30元,所以confirm增加30元
- cancel:由于try啥也没干,所以cancel也啥也不用干
伪代码如下:
A:
try:
检查余额是否够30
扣减30元
confirm:
空
cancel:
增加30元
B:
try:
空
confirm:
增加30元
cancel:
正常流程:A.try => B.try => A.confirm => B.confirm
异常流程:A.try => B.try => A.cancel=> B.cancel (A或B其中一个在try阶段出现异常)
至此我们已经了解TCC的执行流程, 但是TCC还存在一些其他需要的问题。
TCC的三个注意问题
TCC认为cancel和confirm是一定要执行成功的,所以如果这俩个地方出现异常,它会有重试机制
空回滚
在没有调用try的情况下,调用了cancel,造成空回滚。
出现该原因的情况:当一个分支事务所在的服务宕机或异常,分支事务调用记录为异常,这个时候其实是没有执行try阶段的,当故障恢复后,分布式事务进行回滚会调用第二阶段的cancel方法,从而形成回滚。
解决思路:如果我们能知道第一阶段是否执行,我们就能判断出当前是否为空回滚,我们可以额外增加一张分支事务记录表,其中有全局事务ID和分支事务ID,在第一阶段try方法中给表插入一条记录,表示记录存在了,在cancel阶段的时候去查数据库,如果记录存在,则正常回滚,记录不存在则为空回滚。
幂等
由于TCC存在重试机制,如果我们没有做幂等性的话,会导致confirm和cancel方法执行多次,造成严重的数据不一致。同样的我们的try方法也应该做幂等性,因为try方法也可能被多次调用。
同样的我们可以添加一张事务记录表,在confirm阶段和cancel阶段分别往自己的事务记录表中插入进入,在执行前先去查记录是否存在,存在说明之前执行过,不在执行。
悬挂
悬挂指的是cancel阶段在try阶段之前执行,导致锁定的资源永远无法被释放。
出现该原因的情况,RPC在调用分支事务try方法时,出现网络拥堵,导致超时,超时后TM(事务管理者)就会通知RM执行cancel方法进行事务回滚,可能等完成回滚后,RPC请求才到达分支事务,执行try方法,这时候预留的资源就永远也无法被释放。
解决思路:在第二阶段执行后,就不应该执行第一阶段。在执行第一阶段前,先判断在"分支事务记录"表中是否存在第二阶段的事务记录,如果有则不执行try。
https://www.cnblogs.com/jajian/p/10014145.html作实现回滚。
所以说,之前转账的例子伪代码应该改为:
A:
try:
幂等检查处理
悬挂检查处理
检查余额是否够30
扣减30元
confirm:
空
cancel:
幂等检查处理
空回滚检查处理
增加30元
B:
try:
空
confirm:
幂等检查处理
增加30元
cancel:
空
使用Himily实现TCC事务
业务说明
模拟俩个账户转账交易过程,俩个账户分别在不同的银行(张三在bank1、李四在bank2),bank1、bank2是俩个微服务。交易过程是,张三给 李四转账指定金额。
上述交易步骤,要么一起成功,要么一起失败,必须是一个整体性的事务。
程序组成部分
数据库:MySQL-5.7.25
JDK:64位 jdk1.8.0_201
微服务:spring-boot-2.1.3、spring-cloud-Greenwich.RELEASE
Hmily:hmily-springcloud.2.0.4-RELEASE
微服务及数据库的关系 :
dtx/dtx-tcc-demo/dtx-tcc-demo-bank1 银行1,操作张三账户, 连接数据库bank1
dtx/dtx-tcc-demo/dtx-tcc-demo-bank2 银行2,操作李四账户,连接数据库bank2
服务注册中心:dtx/discover-server
环境准备
1、引入Himily maven依赖
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hmily‐springcloud</artifactId>
<version>2.0.4‐RELEASE</version>
</dependency>
2、配置Himily
2.1 编写配置文件
org:
dromara:
hmily :
serializer : kryo
recoverDelayTime : 30
retryMax : 30
scheduledDelay : 30
scheduledThreadMax : 10
repositorySupport : db
started: true
hmilyDbConfig :
driverClassName : com.mysql.jdbc.Driver
url : jdbc:mysql://localhost:3306/hmily?useUnicode=true
username : root
password : root
2.2 编写配置类
@Bean
public HmilyTransactionBootstrap hmilyTransactionBootstrap(HmilyInitService hmilyInitService){
HmilyTransactionBootstrap hmilyTransactionBootstrap = new HmilyTransactionBootstrap(hmilyInitService);
hmilyTransactionBootstrap.setSerializer(env.getProperty("org.dromara.hmily.serializer"));
hmilyTransactionBootstrap.setRecoverDelayTime(Integer.parseInt(env.getProperty("org.dromara.hmily.recoverDelayTime")));
hmilyTransactionBootstrap.setRetryMax(Integer.parseInt(env.getProperty("org.dromara.hmily.retryMax")));
hmilyTransactionBootstrap.setScheduledDelay(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledDelay")));
hmilyTransactionBootstrap.setScheduledThreadMax(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledThreadMax")));
hmilyTransactionBootstrap.setRepositorySupport(env.getProperty("org.dromara.hmily.repositorySupport"));
hmilyTransactionBootstrap.setStarted(Boolean.parseBoolean(env.getProperty("org.dromara.hmily.started")));
HmilyDbConfig hmilyDbConfig = new HmilyDbConfig();
hmilyDbConfig.setDriverClassName(env.getProperty("org.dromara.hmily.hmilyDbConfig.driverClassName"));
hmilyDbConfig.setUrl(env.getProperty("org.dromara.hmily.hmilyDbConfig.url"));
hmilyDbConfig.setUsername(env.getProperty("org.dromara.hmily.hmilyDbConfig.username"));
hmilyDbConfig.setPassword(env.getProperty("org.dromara.hmily.hmilyDbConfig.password"));
hmilyTransactionBootstrap.setHmilyDbConfig(hmilyDbConfig);
return hmilyTransactionBootstrap;
}
3、在启动类上添加@EnableAspectJAutoProxy
并将org.dromara.hmily
加入到扫描包中
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients(basePackages = {"cn.itcast.dtx.tccdemo.bank1.service.feign"})
@ComponentScan({"cn.itcast.dtx.tccdemo.bank1","org.dromara.hmily"})
public class Bank1TccServer {
public static void main(String[] args) {
SpringApplication.run(Bank1TccServer.class, args);
}
}
代码开工
dtx-tcc-demo-bank1
bank1是转账的角色,按照我们之前的分析,他应该是这样的。
try:
幂等检查处理
悬挂检查处理
检查余额是否足够
扣减金额
confirm:
空
cancel:
幂等检查处理
空回滚检查处理
增减扣减的金额
代码比较多,我就不全贴出来,只点几个比较重要的点:
service层:
//表明该方法是个try方法,并指定cancel和confirm方法
@Hmily(cancelMethod = "cancel",confirmMethod = "confirm")
@Transactional
public void updateAccountBalance(String accountNo, Double amount) {
//1.幂等处理
//2.悬挂处理
//3.执行业务代码(检查余额,然后扣钱)
//4.分支事务记录表中保存try的执行记录
}
@Transactional
public void cancel(String accountNo, Double amount) {
//1. 幂等性验证
//2. 空回滚
//3. 撤销预留资源
//4. 分支事务表中保存cancel执行记录
}
public void confirm(String accountNo, Double amount) {
//什么也不需要干
}
dtx-tcc-demo-bank2
bank2是收钱的角色,按照我们之前的分析,他应该这样:
try:
空
confirm:
幂等检查处理
增加金额
cancel:
空
service层:
@Hmily(confirmMethod = "confirmMethod",cancelMethod = "cancelMethod")
public void updateAccountBalance(String accountNo, Double amount) {
//啥也不做
}
@Transactional
public void confirmMethod(String accountNo, Double amount) {
//1.幂等验证
//2.执行业务代码
//3.添加confirm日志,用于幂等性判断
}
public void cancelMethod(String accountNo, Double amount) {
//啥也不干
}
如果你有兴趣去我的码云看TCC实现代码,你就会发现,我们真正的业务代码只有几行,但是TCC的代码就有十几行,由此可以看出TCC对于业务代码有较大的入侵,而且代码工作量巨大!
从代码中我们可以看出来,由于TCC要求每个分支事务都必须提供try、confirm、cancel三个方法,所以TCC对于业务代码有较大的入侵,而且代码工作量巨大!
当然,也正是因为他是在代码层面上实现的分布式事务,所以TCC的使用范围也会更大,可以实现跨数据库、跨不同的业务系统的分布式事务。
项目测试请求http://localhost:56081/bank1/transfer?amount=1
测试用例:
- 场景1:amount=1,bank1和bank2的try方法都执行成功,且confirm方法也执行成功。
- 场景2:amount=2,bank1的try方法在远程调用bank2之前执行失败。验证bank2是否会出现空回滚
- 场景3:amount=3,bank1的try远程调用bank2成功后,后续业务执行失败。验证bank2是否成功回滚
- 场景4:amount=4,bank1和bank2的try方法都执行成功,但是bank2的comfirm方法执行失败。验证confirm是否会重试(存在)
- 场景5:amount=5,bank1和bank2的try方法有一个失败,且bank1的cancel也失败。验证cancel是否存在重试机制。(存在)
TCC认为confirm和cancel方法是一定会成功的,如果出现异常,那么就是我们自己写的代码有问题!