事务回滚什么意思try_分布式事务之解决⽅案(TCC)
4.分布式事务解决⽅案之TCC
4.1.什么是TCC事务
TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分⽀事务实现三个操作:预处理Try、确认Confirm、撤销Cancel。Try操
作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现⼀个与Try相反的操作既回滚操作。TM⾸先发起所有的分⽀事务的try操
作,任何⼀个分⽀事务的try操作执⾏失败,TM将会发起所有分⽀事务的Cancel操作,若try操作全部成功,TM将会发起所有分⽀事务的
Confirm操作,其中Confirm/Cancel操作若执⾏失败,TM会进⾏重试。
分⽀事务失败的情况:
TCC分为三个阶段:
阶段是做业务检查(⼀致性)及资源预留(隔离),此阶段仅是⼀个初步操作,它和后续的Confirm⼀起才能真正构成⼀个完整的
业务逻辑。
m阶段是做确认提交,Try阶段所有分⽀事务执⾏成功后开始执⾏Confirm。通常情况下,采⽤TCC则认为Confirm阶段是不
会出错的。即:只要Try成功,Confirm⼀定成功。若Confirm阶段真的出错了,需引⼊重试机制或⼈⼯处理。
阶段是在业务执⾏错误需要回滚的状态下执⾏分⽀事务的业务取消,预留资源释放。通常情况下,采⽤TCC则认为Cancel阶段
也是⼀定成功的。若Cancel阶段真的出错了,需引⼊重试机制或⼈⼯处理。
事务管理器
TM事务管理器可以实现为独⽴的服务,也可以让全局事务发起⽅充当TM的⾓⾊,TM独⽴出来是为了成为公⽤组件,是为了考虑结构
和软件复⽤。
TM在发起全局事务时⽣成全局事务记录,全局事务ID贯穿整个分布式事务调⽤链条,⽤来记录事务上下⽂,追踪和记录状态,由于
Confirm和Cancel失败需进⾏重试,因此需要实现为幂等性是指同⼀个操作⽆论请求多少次,其结果都相同。
解决⽅案
⽬前市⾯上的TCC框架众多⽐如下⾯这⼏种:
Seata也⽀持TCC,但Seata的TCC模式对SpringCloud并没有提供⽀持。我们的⽬标是理解TCC原理以及事务协调运作的过程,因此更
倾向于轻量级易于理解的框架。
Hmily是⼀个⾼性能分布式事务TCC开源框架。基于Java语⾔来开发(JDK1.8),⽀持Dubbo,SpringCloud等RPC框架进⾏分布式事
务。它⽬前⽀持以下特性:
⽀持嵌套事务(Nestedtransactionsupport)。
采⽤disruptor框架进⾏事务⽇志的异步读写,与RPC框架的性能毫⽆差别。
⽀持SpringBoot-starter项⽬启动,使⽤简单。
RPC框架⽀持:dubbo、motan、springcloud。
本地事务存储⽀持:redis、mongodb、zookeeper、file、mysql。
事务⽇志序列化⽀持:java、hessian、kryo、protostuff。
采⽤AspectAOP切⾯思想与Spring⽆缝集成,天然⽀持集群。
RPC事务恢复,超时异常恢复等。
Hmily利⽤AOP对参与分布式事务的本地⽅法与远程⽅法进⾏拦截处理,通过多⽅拦截,事务参与者能透明的调⽤到另⼀⽅的Try、
Confirm、Cancel⽅法;传递事务上下⽂;并记录事务⽇志,酌情进⾏补偿,重试等。
Hmily不需要事务协调服务,但需要提供⼀个数据库(mysql/mongodb/zookeeper/redis/file)来进⾏⽇志存储。
Hmily实现的TCC服务与普通的服务⼀样,只需要暴露⼀个接⼝,也就是它的Try业务。Confirm/Cancel业务逻辑,只是因为全局事
务提交/回滚的需要才提供的,因此Confirm/Cancel业务只需要被HmilyTCC事务框架发现即可,不需要被调⽤它的其他业务服务所
感知。
官⽹介绍:/website/zh-cn/docs/hmily/C需要注意三种异常处理分别是空回滚、幂等、悬TCC需要注意三种异常处理分别是空回滚、幂等、悬
挂:挂:空回滚空回滚:
在没有调⽤TCC资源Try⽅法的情况下,调⽤来⼆阶段的Cancel⽅法,Cancel⽅法需要识别出这是⼀个空回滚,然后直接返回成功。
出现原因是当⼀个分⽀事务所在服务宕机或⽹络异常,分⽀事务调⽤记录为失败,这个时候其实是没有执⾏Try阶段,当故障恢复后,
分布式事务进⾏回滚则会调⽤⼆阶段的Cancel⽅法,从⽽形成空回滚。
解决思路是关键就是要识别出这个空回滚。思路很简单就是需要知道⼀阶段是否执⾏,如果执⾏来,那就是正常回滚;如果没执⾏,那
就是空回滚。前⾯已经说过TM在发起全局事务时⽣成全局事务记录,全局事务ID贯穿整个分布式事务调⽤链条。再额外增加⼀张分⽀
事务记录表,其中有全局事务ID和分⽀事务ID,第⼀阶段Try⽅法⾥会插⼊⼀条记录,表⽰⼀阶段执⾏来。Cancel接⼝⾥读取该记录,
如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。幂等幂等:
通过前⾯介绍已经了解到,为了保证TCC⼆阶段提交重试机制不会引发数据不⼀致,要求TCC的⼆阶段Try、Confirm和Cancel接⼝保
证幂等,这样不会重复使⽤或者释放资源。如果幂等控制没有做好,很有可能导致数据不⼀致等严重问题。
解决思路在上述“分⽀事务记录”中增加执⾏状态,每次执⾏前都查询该状态。悬挂悬挂:
悬挂就是对于⼀个分布式事务,其⼆阶段Cancel接⼝⽐Try接⼝先执⾏。
出现原因是在RPC调⽤分⽀事务try时,先注册分⽀事务,再执⾏RPC调⽤,如果此时RPC调⽤的⽹络发⽣拥堵,通常RPC调⽤是有超
时时间的,RPC超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC请求才到达参与者真正执⾏,⽽⼀个Try⽅法预
留的业务资源,只有该分布式事务才能使⽤,该分布式事务第⼀阶段预留的业务资源就再也没有⼈能够处理了,对于这种情况,我们就
称为悬挂,即业务资源预留后⽆法继续处理。
解决思路是如果⼆阶段执⾏完成,那⼀阶段就不能再继续执⾏。在执⾏⼀阶段事务时判断在该全局事务下,“分⽀事务记录”表中是否
已经有⼆阶段事务记录,如果有则不执⾏Try。举例,场景为A转账30元给B,A和B账户在不同的服务。举例,场景为A转账30元给B,A和B账户在不同的服务。⽅案1⽅案1:
账户A
try:检查余额是否够30元扣减30元confirm:空cancel:增加30元
账户B
try:增加30元confirm:空cancel:减少30元
⽅案1说明:
1)账户A,这⾥的余额就是所谓的业务资源,按照前⾯提到的原则,在第⼀阶段需要检查并预留业务资源,因此,我们在扣钱TCC资源的
Try接⼝⾥先检查A账户余额是否⾜够,如果⾜够则扣除30元。Confirm接⼝表⽰正式提交,由于业务资源已经在Try接⼝⾥扣除掉
了,那么在第⼆阶段的Confirm接⼝⾥可以什么都不⽤做。Cancel接⼝的执⾏表⽰整个事务回滚,账户A回滚则需要把Try接⼝⾥扣除
掉的30元还给账户。
2)账号B,在第⼀阶段Try接⼝⾥实现给账户B加钱,Cancel接⼝的执⾏表⽰整个事务回滚,账户B回滚则需要把Try接⼝⾥加的30元再
减去。⽅案1的问题分析:⽅案1的问题分析:
1)如果账户A的try没有执⾏在cancel则就多加了30元。
2)由于try,cancel、confirm都是由单独的线程去调⽤,且会出现重复调⽤,所以都需要实现幂等。
3)账号B在try中增加30元,当try执⾏完成后可能会其它线程给消费了。
4)如果账户B的try没有执⾏在cancel则就多减了30元。问题解决:问题解决:
1)账户A的cancel⽅法需要判断try⽅法是否执⾏,正常执⾏try后⽅可执⾏cancel。
2)try、cancel、confirm⽅法实现幂等。
3)账户B在try⽅法中不允许更新账户⾦额,在confirm中更新账户⾦额。
4)账户B的cancel⽅法需要判断try⽅法是否执⾏,正常执⾏try后⽅可执⾏cancel。优化⽅案:优化⽅案:
账户A:
try:try幂等校验try悬挂处理检查余额是否够30元扣减30元confirm:空cancel:cancel幂等校验cancel空回滚处理增加可⽤余额30
元
账户B:
try:空confirm:confirm幂等校验正式增加30元cancel:空
实现TCC事务
4.3.1.业务说明
通过Hmily实现TCC分布式事务,模拟两个账户的转账交易过程。
两个账户分别在不同的银⾏(张三在bank1、李四在bank2),bank1、bank2是两个微服务。交易过程是,张三给李四转账制定⾦额。
上述交易步骤,要么⼀起成功,要么⼀起失败,必须是⼀个整体性事务。
4.3.2.程序组成部分
数据库:MySQL-5.7.25
JDK:64位jdk1.8.0_201微服务:spring-boot-2.1.3、EHmily:hmily-springcloud.2.0.4-
RELEASE
微服务及数据库的关系:
dtx/dtx-tcc-demo/dtx-tcc-demo-bank1银⾏1,操作张三账户,连接数据库bank1dtx/dtx-tcc-demo/dtx-tcc-demo-bank2银⾏
2,操作李四账户,连接数据库bank2
服务注册中⼼:dtx/discover-rver
4.3.3.创建数据库
创建hmily数据库,⽤于存储hmily框架记录的数据。
CREATEDATABASEhmilyCHARACTERSET‘utf8’COLLATE‘utf8_general_ci’;创建bank1库,并导⼊以下表结构和数据创建bank1库,并导⼊以下表结构和数据
(包含张三账户)
CREATEDATABASEbank1CHARACTERSET‘utf8’COLLATE‘utf8_general_ci’;创建bank2库,并导⼊以下表结构和数据创建bank2库,并导⼊以下表结构和数据
(包含李四账户)
CREATEDATABASEbank2CHARACTERSET‘utf8’COLLATE‘utf8_general_ci’;
DROPTABLEIFEXISTSaccount_info;CREATETABLEaccount_info(idbigint(20)NOTNULLAUTO_INCREMENT,account_name
varchar(100)CHARACTERSETutf8COLLATEutf8_binNULLDEFAULTNULLCOMMENT‘户主姓名’,account_no
varchar(100)CHARACTERSETutf8COLLATEutf8_binNULLDEFAULTNULLCOMMENT‘银⾏卡号’,account_password
varchar(100)CHARACTERSETutf8COLLATEutf8_binNULLDEFAULTNULLCOMMENT‘帐户密码’,account_balance
doubleNULLDEFAULTNULLCOMMENT‘帐户余额’,
PRIMARYKEY(id)USINGBTREE
)ENGINE=InnoDBAUTO_INCREMENT=5CHARACTERSET=utf8COLLATE=utf8_binROW_FORMAT=Dynamic;
INSERTINTOaccount_infoVALUES(2,‘张三的账户’,‘1’,‘’,10000);
每个数据库都创建try、confirm、cancel三张⽇志表:
CREATETABLE`local_try_log`(`tx_no`varchar(64)NOTNULLCOMMENT`create_time`datetimeDEFAULTNULL,
PRIMARYKEY(`tx_no`))ENGINE=InnoDBDEFAULTCHARSET=utf8CREATETABLE`local_confirm_log`(`tx_no`
varchar(64)NOTNULLCOMMENT`create_time`datetimeDEFAULTNULL)ENGINE=InnoDBDEFAULTCHARSET=utf8
CREATETABLE`local_cancel_log`(`tx_no`varchar(64)NOTNULLCOMMENT`create_time`datetimeDEFAULTNULL,
PRIMARYKEY(`tx_no`))ENGINE=InnoDBDEFAULTCHARSET=utf8
4.3.5⼯程dtx-tcc-demo
(1)引⼊maven依赖
RELEASE
(2)配置hmily
:
org:dromara:hmily:rializer:kryorecoverDelayTime:128retryMax:30scheduledDelay:128scheduledThreadMax:10
repositorySupport:dbstarted:truehmilyDbConfig:driverClassName:url:
jdbc:mysql://localhost:3306/bank?uUnicode=trueurname:rootpassword:root
新增配置类接收中的Hmily配置信息,并创建HmilyTransactionBootstrapBean:
@BeanpublicHmilyTransactionBootstraphmilyTransactionBootstrap(HmilyInitServicehmilyInitService){
HmilyTransactionBootstraphmilyTransactionBootstrap=newHmilyTransactionBootstrap(hmilyInitService);
ializer(perty("izer"));
overDelayTime(nt(perty("rDelayTime")));
ryMax(nt(perty("ax")));
eduledDelay(nt(perty("ledDelay")));
eduledThreadMax(nt(perty("ledThreadMax")));
ositorySupport(perty("torySupport"));
rted(oolean(perty("d")));HmilyDbConfig
hmilyDbConfig=newHmilyDbConfig();
verClassName(perty("ClassName"));
(perty(""));
rname(perty("me"));
sword(perty("rd"));
lyDbConfig(hmilyDbConfig);returnhmilyTransactionBootstrap;}
启动类增加@EnableAspectJAutoProxy并增加的扫描项:
@SpringBootApplication@EnableDiscoveryClient@EnableHystrix@EnableFeignClients(baPackages=
{""})@ComponentScan({"1",""})publicclass
Bank1HmilyServer{publicstaticvoidmain(String[]args){(,args);}}
4.3.7dtx-tcc-demo-bank1
dtx-tcc-demo-bank1实现try和cancel⽅法,如下:
try:try幂等校验try悬挂处理检查余额是够扣减⾦额扣减⾦额confirm:空cancel:cancel幂等校验cancel空回滚处理增加可⽤余额
@Mapper@ComponentpublicinterfaceAccountInfoDao{@Update("updateaccount_infot
account_balance=account_balance-#{amount}whereaccount_balance>=#{amount}andaccount_no=#{accountNo}")int
subtractAccountBalance(@Param("accountNo")StringaccountNo,@Param("amount")Doubleamount);@Update("update
account_infotaccount_balance=account_balance+#{amount}whereaccount_no=#{accountNo}")int
addAccountBalance(@Param("accountNo")StringaccountNo,@Param("amount")Doubleamount);/***增加某分⽀事务try执
⾏记录*@paramlocalTradeNo本地事务编号*@return*/@Inrt("inrtintolocal_try_logvalues(#{txNo},now());")int
addTry(StringlocalTradeNo);@Inrt("inrtintolocal_confirm_logvalues(#{txNo},now());")intaddConfirm(String
localTradeNo);@Inrt("inrtintolocal_cancel_logvalues(#{txNo},now());")intaddCancel(StringlocalTradeNo);/***查询分
⽀事务try是否已执⾏*@paramlocalTradeNo本地事务编号*@return*/@Select("lectcount(1)fromlocal_try_logwhere
tx_no=#{txNo}")intisExistTry(StringlocalTradeNo);/***查询分⽀事务confirm是否已执⾏*@paramlocalTradeNo本地事务编
号*@return*/@Select("lectcount(1)fromlocal_confirm_logwheretx_no=#{txNo}")intisExistConfirm(String
localTradeNo);/***查询分⽀事务cancel是否已执⾏*@paramlocalTradeNo本地事务编号*@return*/@Select("lectcount(1)
fromlocal_cancel_logwheretx_no=#{txNo}")intisExistCancel(StringlocalTradeNo);}
2)try和cancel⽅法
3)feignClient
@FeignClient(value="ata-demo-bank2",fallback=)publicinterfaceBank2Client{
@GetMapping("/bank2/transfer")@HmilyBooleantransfer(@RequestParam("amount")Doubleamount);}
ller
@RestControllerpublicclassBank1Controller{@AutowiredprivateAccountInfoServiceaccountInfoService;
@RequestMapping("/transfer")publicStringtest(@RequestParam("amount")Doubleamount){
AccountBalance("1",amount);return"bank1"+amount;}}
4.3.8dtx-tcc-demo-bank2
dtx-tcc-demo-bank2实现如下功能:
try:空confirm:confirm幂等校验正式增加⾦额cancel:空
1)Dao
@Component@MapperpublicinterfaceAccountInfoDao{@Update("updateaccount_infot
account_balance=account_balance+#{amount}whereaccount_no=#{accountNo}")int
addAccountBalance(@Param("accountNo")StringaccountNo,@Param("amount")Doubleamount);/***增加某分⽀事务try执
⾏记录*@paramlocalTradeNo本地事务编号*@return*/@Inrt("inrtintolocal_try_logvalues(#{txNo},now());")int
addTry(StringlocalTradeNo);@Inrt("inrtintolocal_confirm_logvalues(#{txNo},now());")intaddConfirm(String
localTradeNo);@Inrt("inrtintolocal_cancel_logvalues(#{txNo},now());")intaddCancel(StringlocalTradeNo);/***查询分
⽀事务try是否已执⾏*@paramlocalTradeNo本地事务编号*@return*/@Select("lectcount(1)fromlocal_try_logwhere
tx_no=#{txNo}")intisExistTry(StringlocalTradeNo);/***查询分⽀事务confirm是否已执⾏*@paramlocalTradeNo本地事务编
号*@return*/@Select("lectcount(1)fromlocal_confirm_logwheretx_no=#{txNo}")intisExistConfirm(String
localTradeNo);/***查询分⽀事务cancel是否已执⾏*@paramlocalTradeNo本地事务编号*@return*/@Select("lectcount(1)
fromlocal_cancel_logwheretx_no=#{txNo}")intisExistCancel(StringlocalTradeNo);}
2)实现confirm⽅法
3)Controller
@RestControllerpublicclassBank2Controller{@AutowiredprivateAccountInfoServiceaccountInfoService;
@RequestMapping("/transfer")publicBooleantest2(@RequestParam("amount")Doubleamount){
AccountBalance("2",amount);returntrue;}}
3.3.9测试场景
张三向李四转账成功。
李四事务失败,张三事务回滚成功。
张三事务失败,李四分⽀事务回滚成功。
分⽀事务超时测试。
4.4.⼩结
如果拿TCC事务的处理流程与2PC两阶段提交做⽐较,2PC通常都是在跨库的DB层⾯,⽽TCC则在应⽤层⾯的处理,需要通过业务逻辑来
实现。这种分布式事务的实现⽅式的优势在于,可以让应⽤⾃⼰定义数据操作的粒度,使得降低锁冲突、提⾼吞吐量成为可能应⽤⾃⼰定义数据操作的粒度,使得降低锁冲突、提⾼吞吐量成为可能。
⽽不⾜之处则在于对应⽤的侵⼊性⾮常强,业务逻辑的每个分⽀都需要实现try、confirm、cancel三个操作。此外,其实现难度也⽐较
⼤,需要按照⽹络状态、系统故障等不同的失败原因实现不同的回滚策略。
本文发布于:2023-01-03 12:21:13,感谢您对本站的认可!
本文链接:http://www.wtabcd.cn/fanwen/fan/90/84392.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |