本文转载自微信公众号《码猿科技专栏》,作者陈谋。转载本文请联系码猿科技专栏公众号。今天的文章介绍了Seata是如何实现TCC事务模式的。文章内容如下:什么是TCC模式?TCC(TryConfirmCancel)方案是在应用层侵入业务的两阶段提交。是目前最流行的灵活交易方案。它的核心思想是:对于每一个操作,都必须注册一个相应的确认和补偿(撤销)操作。TCC分为两个阶段,分别如下:第一阶段:尝试(try),主要是检测业务系统和预留资源(locking,lockingresources)第二阶段:这个阶段是根据第一阶段的结果阶段,决定是执行confirm还是cancelConfirm(确认):执行真正的业务(执行业务,释放锁)cancle(取消):取消预留资源(如果有问题,释放锁)TCC为了方便理解,下面以电商下载为例分析解决方案。在这里,整个过程简单分为两个步骤:扣减库存和创建订单。库存服务和订单服务分别在不同的服务器节点上。假设商品库存为100件,采购数量为2件,这里在查询更新库存的同时,冻结用户采购数量的库存,同时创建订单,订单状态为待确认。①Try阶段TCC机制中的Try只是一个初步的操作,连同后续的确认,才能真正构成一个完整的业务逻辑。该阶段主要完成:完成所有业务检查(一致性)。预留必要的业务资源(准隔离)。尝试尝试执行业务。Try阶段②Confirm/Cancel阶段根据Try阶段所有服务是否正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。Confirm和Cancel操作是幂等的。如果Confirm或Cancel操作失败,会不断重试,直到执行完成。Confirm:当Try阶段的所有服务都正常执行后,执行确认业务逻辑操作,业务如下:Try->Confirm这里使用的资源必须是Try阶段预留的业务资源。在TCC事务机制中,认为如果在Try阶段能够正常预留资源,那么Confirm一定是完整且正确提交的。Confirm阶段也可以看作是Try阶段的补充,Try+Confirm共同构成一个完整的业务逻辑。Cancel:当Try阶段有业务执行失败,进入Cancel阶段,业务如下:Try-CancelCancel取消执行,释放Try阶段预留的业务资源。在上面的例子中,Cancel操作会释放冻结的库存,并更新订单状态为cancelled。以上就是TCC模型的整个概念。这部分内容在陈老师之前的文章中也有详细介绍:相比7个分布式事务方案,我还是更喜欢阿里开源的Seata。类型?在行业实际生产中,对TCC模式进行了拓展,总结出以下三种类型。事实上,官方定义中并没有这样的表述,只是根据企业生产中的实际需求推导出来的三种方案。1.通用TCC方案通用TCC方案是最经典的TCC事务模型的实现。就像第一节介绍的模型一样,所有从服务都参与主服务的决策。通用型TCC应用场景:由于从业务服务是同步调用的,其结果会影响主业务服务的决策,所以通用型TCC分布式事务方案适用于执行时间确定且短的业务,比如电子商务系统三个核心服务:订单服务、账户服务、库存服务。所有三个服务要么同时成功,要么同时失败。当库存服务和账户服务的第二阶段调用完成后,整个分布式事务就完成了。2、异步保证TCC方案异步保证TCC方案的直接二次业务服务是可靠的消息服务,而真正的二次业务服务通过消息服务解耦,作为消息服务的消费者异步执行。异步保证可靠的消息服务需要提供三个接口:Try、Confirm和Cancel。Try接口预发送,只负责消息数据的持久化存储;Confirm接口确认发送,然后消息的实际传递开始;Cancel接口取消发送,删除消息数据。消息服务的消息数据独立存储,独立伸缩,降低业务服务与消息系统的耦合度,在消息服务可靠的前提下实现分布式事务的最终一致性。这种方案虽然增加了消息服务的维护成本,但是由于消息服务实现了TCC接口而不是从业务服务,从业务服务不需要做任何修改,接入成本非常低。适用场景:由于从业务服务消费消息是一个异步过程,执行时间不确定,可能会导致不一致的时间窗增加。因此异步有保证的TCC分布式事务方案只适用于一些对最终一致性时间不太敏感的被动业务(从业务服务的处理结果不影响主业务服务的决策,只被动接收主业务)业务服务决策的结果)。例如会员注册服务和邮件发送服务:3.补偿TCC方案补偿TCC方案在结构上与通用TCC方案类似,其副业务服务也需要参与主业务服务的活动决策。但不同的是,前者的slave业务服务只需要提供Do和Compensate两个接口,而后者需要提供三个接口。Do接口直接执行真正完整的业务逻辑,完成业务处理,业务执行结果对外可见;Compensate操作用于业务补偿,抵消或部分抵消前向业务操作的业务结果,Compensate操作必须满足幂等性。与通用方案相比,补偿型方案的二次业务服务不需要对原有业务逻辑进行改造,只需要增加额外的补偿回滚逻辑,业务改造量小。但需要注意的是,业务是在一个阶段执行整个业务逻辑,无法实现有效的事务隔离。当需要回滚时,可能会出现补偿失败,需要额外的异常处理机制,比如人工干预。适用场景:由于回滚补偿失败,补偿型TCC分布式事务方案只适用于一些并发冲突较少或需要外部交互的业务。这些外部业务不是被动业务,它们的执行结果会影响到主业务服务的决策。以上部分内容参考自:https://seata.io/zh-cn/blog/tcc-mode-applicable-scenario-analysis.html?utm_source=gold_browser_extensionTCC交易模式的实现上一篇,Seata的AT模式介绍不清楚的可以看:相比7种分布式事务方案,我还是更喜欢阿里开源的Seata。XA模式,下面我们整合TCC模式。1、演示场景以在电商系统下单为例。为了演示,直接去掉账户服务,引入订单服务和库存服务为例。具体逻辑如下:客户端调用下单接口,扣除库存,创建订单。根据以上逻辑,订单服务一定是主业务服务,交易的发起方,而库存服务是从业务服务,参与交易的决策。Seata的AT模式方案伪代码如下:@GlobalTransactionalpublicResultcreateOrder(LongproductId,Longnum,.....){//1、减少库存reduceStorage();//2、创建订单saveOrder();}@GlobalTransactional注解用于发起全局事务。但是,AT模式也有局限性,表现为:性能低下,锁定资源时间过长,无法解决跨应用事务。因此,对于对性能有要求的订单接口,可以考虑使用TCC模式,将其拆分为两个阶段,让整个流程锁定资源。时间会更短,性能会提高。此时TCC模式的拆分如下:1)一阶段Try操作TCC模式下的Try阶段实际上是预留资源。在此字段中维护一个冻结库存字段。伪代码如下:@Transactionalpublicbooleantry(){//冻结库存frozenStorage();//生成订单,状态待确认saveOrder();}》注:@Transactional开启本地事务,只要有是异常,本地事务会回滚,同时执行第二阶段的cancel操作。2)、第二阶段的confirm操作confirm操作在第一阶段try操作成功后提交事务,涉及的操作如下:释放try操作冻结的库存(frozeninventory-purchasedquantity)生成订单的伪代码如下:@Transactionalpublicbooleanconfirm(){//释放try操作保留的库存cleanFrozen();//修改订单,状态为完成updateOrder();returntrue;}注意:如果这里返回false,按照TCC规范,应该继续重试,直到确认完成。3)第二阶段的cancel操作cancel操作在第一阶段的try操作出现异常后执行,用于回滚资源。涉及的操作如下:恢复冻结库存(冻结库存-采购数量,库存+采购数量)删除订单伪代码如下:@Transactionalpublicbooleancancel(){//释放try操作保留的库存rollbackFrozen();//修改订单,状态完成delOrder();returntrue;}注意:如果这里返回false,按照TCC规范,应该不断重试,直到cancel完成。2.TCC事务模型的三个异常实现TCC事务模型涉及的三个异常是不可避免的,在实际生产中必须避免这三个异常。1)空回滚定义:当没有调用try方法或者try方法执行不成功时,执行cancel方法进行回滚。你怎么理解的?在不调用try方法的情况下执行cancel方法。这很容易理解。由于没有保留资源,因此不能回滚。try方法没有执行成功是什么意思?可以看到上一节第一阶段的try方法的伪代码。由于try方法启动的是本地事务,一旦try方法执行过程中出现异常,就会导致try方法的本地事务回滚(注意这里不是cancel方法的回滚,而是本地事务回滚try方法中的),这样try方法中的所有操作都会回滚,不需要调用cancel方法。但实际上,一旦try方法抛出异常,必须调用cancel方法回滚,导致空回滚。解决方案:解决逻辑很简单:在cancel方法执行操作之前,需要知道try方法是否执行成功。2)在幂等TCC模式的定义中提到:如果confirm或cancel方法执行失败,必须重试,直到成功。这里涉及到幂等性,confirm和cancel方法必须保证在同一个全局事务中的幂等性。解决方案:解决方案逻辑很简单:处理幂等性,自然是使用幂等标志来防止重操作。3)挂起的事务协调器调用TCC服务的第一阶段Try操作时,可能会因为网络拥塞而超时。此时事务管理器会触发二阶段回滚,调用TCC服务的Cancel操作,Cancel调用没有超时;之后,网络拥塞的第一阶段Try包被TCC服务接收,第二阶段Cancel请求先于第一阶段Try请求执行。TCC服务执行完lateTry后,再也不会收到第二阶段的Confirm或Cancel,导致TCC服务挂掉。解决方案:解决逻辑很简单:先判断cancel方法是否执行过,再执行try方法操作资源;同理,记录cancel方法执行后的执行状态。4)综上所述,针对以上三种异常的解决方案有很多,比如维护一个事务状态表,记录每个事务的所有执行阶段。幂等性:在执行confirm或cancel之前,根据事务状态表查询当前全局事务是否已经执行。空回滚:在执行cancel之前,根据事务状态表查看当前全局事务是否执行成功。try方法挂起:在执行try方法之前,根据事务状态表检查当前全局事务是否已经执行。cancel方法Seata集成了TCC来实现如何构建项目和添加依赖。我不会在这里详细介绍。不熟悉的可以看我之前的文章:对比7种分布式事务方案,我还是比较喜欢阿里开源的Seata。真香!(原理+实战)本节只介绍关键代码。毕竟空间有限。其他部分请下载源码。源码目录如下:源码目录下项目启动所需的相关文件如下:nacos目录下的SEATA_GROUP是Seata交易服务器和客户端需要的相关配置,直接导入nacos即可。seata目录下的conf是1.3.0版本服务器配置SQL目录,与几个数据库相关。1、TCC接口定义在order-boot模块中创建OrderTccService,代码如下:代码中的注释很全,介绍几个亮点:@LocalTCC:该注解启动TCC事务@TwoPhaseBusinessAction:该注解是标记在try方法上,三个属性如下:name:TCC事务的名称,必须是唯一的commitMethod:确认方法的名称,默认为commitrollbackMethod:取消方法的名称,默认为rollbackconfirm和cancel的返回值尤为重要,returnfalse会继续重试。2.TCC接口的实现定义已经建立,必须实现,如下:1).try方法①处的代码是为了防止挂起异常,从事务日志表中获取全局事务ID的状态。如果处于取消状态,则不会执行。②处的代码冻结库存。③处代码生成订单,状态为待确认。④处的代码为幂等工具类添加了标记。键是当前类和全局事务ID,值是当前时间戳。注意:必须打开本地事务。上述代码使用@Transactional开启本地事务2)、confirm方法confirm方法①处的代码根据当前类和全局事务ID从幂等工具类中获取值。由于try阶段执行成功,它会发送给Addingavalue如果confirm方法执行成功就会移除这个值。因此,在confirm开头判断值是否存在,具有幂等的作用,可以防止重试。⑥处的代码将幂等工具类的try方法中添加的值去掉。②处的代码是从BusinessActionContext中获取try方法中的入参。③处的代码是释放冻结的库存。④处的代码是修改订单状态为已完成。注意:1.开启本地事务2.注意返回值,返回false会重试3)、cancel方法cancel方法①处的代码是往事务日志记录表中插入一条数据,标记当前事务进入cancel方法,并使用来防止挂起,这个对应try方法中①处的代码。②处的代码是为了防止幂等和空回滚,因为只有try方法执行成功后,幂等工具类中对应的当前类和全局事务ID才会存储这个值。这可以防止幂等回滚和空回滚。③处的代码恢复冻结的库存。④处的代码删除了这个订单。⑤处的代码是去掉幂等工具类当前类对应的值和全局事务ID。3、如何预防TCC模型的三大异常?实现方法有很多种。在某些情况下,事务日志表用于记录当前状态,完美解决了幂等、空回滚、挂起等问题。Chen这里为了方便使用了两种方案,如下:1)、幂等,空回滚使用了一个幂等的工具类,它是一个Map,key是当前类和全局事务ID,value是时间戳。代码如下:思路如下:在try方法的最后,使用幂等工具类中的add方法进行加值。在confirm和cancel方法中,使用幂等工具类中的remove方法来移除值。在确认和取消方法中使用幂等工具类。get方法获取值。如果为空,则表示执行完毕,直接返回true,防止幂等和空回滚。2.暂停的实现依赖于事务日志表。表结构如下:CREATETABLE`transactional_record`(`id`bigint(11)NOTNULLAUTO_INCREMENT,`xid`varchar(100)NOTNULL,`status`int(1)DEFAULTNULLCOMMENT'1.try2commit3cancel',PRIMARYKEY(`id`)USINGBTREE)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;其中xid是全局事务ID,status是事务的状态。其他字段可以自行扩展解决挂起问题。逻辑如下:在cancel方法中,在事务日志表中记录当前全局事务ID,状态为cancel。try方法在执行资源操作前检查事务日志表中当前全局事务ID是否为cancel。Status4.创建订单的业务方法。以上只完成了TCC的三个方法。主营业务的发起人未提供。代码如下:@GlobalTransactional这个注解开启全局事务,是事务的发起者。内部直接调用的TCC的try方法。5.其他配置以上只列出关键步骤,其余配置根据案例源码完善,如下:接口测试集成nacos集成feign集成seata,TCC模式下配置同AT模式下的Seata配置注意:一定要配置Seata的事务组tx-service-group,配置方法见上篇文章。6.总结TCC交易模型比较简单。有兴趣的可以下载源码试试。