分布式事务:TCC两阶段异步补偿型

VictorMontague 发布于1年前

TCC的作用主要是解决跨服务调用场景下的分布式事务问题,在本文中,笔者将先介绍一个跨服务的场景案例,并分析其中存在的分布式事务问题;然后介绍TCC的基本概念以及其是如何解决这个问题的。

1 TCC简介

关于 TCC(Try-Confirm-Cancel) 的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。在该论文中,TCC还是以Tentative-Confirmation-Cancellation命名。

正式以Try-Confirm-Cancel作为名称的是 Atomikos 公司,其注册了TCC商标。 Atomikos公司旗下最著名的产品就是事务管理器。产品分两个版本:

  • TransactionEssentials:开源的免费产品

  • ExtremeTransactions:上商业版,需要收费。

分布式事务:TCC两阶段异步补偿型

Atomikos公司在商业版本事务管理器ExtremeTransactions中提供了TCC方案的实现,但是由于其是收费的,限制了其的传播。

国内最早关于TCC的报道,应该是2008年 InfoQ上对阿里程立博士(花名:鲁肃)的一篇采访。同年底, 鲁肃在CSDN举办的软件开发2.0技术大会上,作了名为《大规模SOA系统中的分布式事务处理》的技术分享。经过鲁肃的传道之后, TCC在国内逐渐被大家广为了解并接受,因此相应的很多的开源实现方案也就涌现出来,如: tcc-transaction 、 ByteTCC 、 spring-cloud-rest-tcc 以及 最近格外火的 seata 等。

2 TCC场景案例

Atomikos官网上<<Composite Transactions for SOA>>一文中,以航班预定的案例,来介绍TCC要解决的事务场景。在这里笔者虚构一个完全相同的场景,把自己当做航班预定的主人公,来介绍这个案例。事实上,你可以把本案例当做官方文档案例的一个翻译,只不过把地点从Brussels-->Toronto-->Washington,改成从合肥-->昆明-->大理。

有一次,笔者买彩票中奖了(纯属虚构),准备从合肥出发,到云南大理去游玩,然后通过机票代理商(携程、美团等)来订机票。发现没有从合肥直达大理的航班,需要到昆明进行中转。如下图:

分布式事务:TCC两阶段异步补偿型

从图中我们可以看出来,从合肥到昆明乘坐的是四川航空,从昆明到大理乘坐的是东方航空。

由于通过机票代理商预定,当我选择了这种航班预定方案后,代理商就要去四川航空和东方航空各帮我购买一张票。如下图:

分布式事务:TCC两阶段异步补偿型

考虑最简单的情况:机票代理商先去川航帮我买票,如果买不到,那么东航也没必要买了。如果川航购买成功,再去东航购买另一张票。

现在问题来了:假设代理商先从川航成功买到了票,然后去东航买票的时候,因为天气问题,东航航班被取消了。那么此时,代理商必须取消川航的票,因为只有一张票是没用的,不取消就是浪费我的钱。那么如果取消会怎样呢?如果读者有取消机票经历的话,非正常退票,肯定要扣手续费的。在这里,川航本来已经购买成功,现在因为东航的原因要退川航的票,川航应该是要扣代理商的钱的。

那么机票代理商就要保证,如果任一航班购买失败,都不能扣钱,怎么做呢?

两个航空公司都为机票代理商提供以下3个接口:机票预留接口、确认接口、取消接口。机票代理商分2个阶段进行调用,如下所示: 

分布式事务:TCC两阶段异步补偿型

在第1阶段:

代理商分别请求两个航空公司预留机票,两个航空公司分别告诉代理商预留成功还是失败。航空公司需要保证,机票预留成功的话,之后一定能购买到。

在第2阶段:

1)如果两个航空公司都预留成功,则分别向两个公司发送确认购买请求。

2)如果两个航空公司任意一个预留失败,则对于预留成功的航空公司也要取消预留。这种情况下,对于之前预留成功机票的航班取消,也不会扣用户的钱,因为购买并没实际发生,之前只是请求预留机票而已。

通过这种方案,可以保证两个航空公司购买机票的一致性,要不都成功,要不都失败,即使失败也不会扣用户的钱。如果在两个航班都已经已经确认购买后,再退票,那肯定还是要扣钱的。

当然,实际情况肯定这里提到的肯定要复杂,通常航空公司在第一阶段,对于预留的机票,会要求在指定的时间必须确认购买(支付成功),如果没有及时确认购买,会自动取消。假设川航要求10分钟内支付成功,东航要求30分钟内支付成功。以较短的时间算,如果用户在10分钟内支付成功的话,那么代理商会向两个航空公司都发送确认购买的请求,如果超过10分钟(以较短的时间为准),那么就不能进行支付。

再次强调,这个案例,可以算是<<Composite Transactions for SOA>>中航班预定案例的汉化版。而实际机票代理商是如何实现这种需要中转的航班预定需求,笔者并不知情。 另外,注意这只是一个案例场景,实际情况中,你是很难去驱动航空公司进行接口改造的。

Whatever,这个方案提供给我们一种跨服务条用保证事务一致性的一种解决思路,可以把这种方案当做TCC的雏形。

3 TCC 的基本概念

上面的方案,是一种典型的通过TCC解决分布式事务的方案。下面对TCC( Try-Confirm-Cancel )的基本概念进行介绍:

Try阶段:

完成所有业务检查(一致性),预留业务资源(准隔离性)

回顾上面航班预定案例的阶段1,机票就是业务资源,所有的资源提供者(航空公司)预留都成功,try阶段才算成功

Confirm阶段:

确认执行业务操作,不做任何业务检查, 只使用Try阶段预留的业务资源。回顾上面航班预定案例的阶段2,代理商确认两个航空公司机票都预留成功,因此向两个航空公司分别发送确认购买的请求。

Cancel阶段:

取消Try阶段预留的业务资源。回顾上面航班预定案例的阶段2,如果某个业务方的业务资源没有预留成功,则取消所有业务资源预留请求。

敏锐的读者立马会想到,TCC与XA两阶段提交有着异曲同工之妙,下图列出了二者之间的对比:

分布式事务:TCC两阶段异步补偿型

1) 在阶段1:

在XA中,各个RM (prepare)预提交各自的事务分支,事实上就是准备提交资源的更新操作(insert、delete、update等);而在TCC中,是主业务活动请求(try)各个从业务服务预留资源。

2) 在阶段2:

XA根据第一阶段每个RM是否都prepare成功,判断是要提交还是回滚。如果都prepare成功,那么就commit每个事务分支,反之则rollback每个事务分支。

TCC中,如果在第一阶段所有业务资源都预留成功,那么confirm各个从业务服务,否则取消(cancel)所有从业务服务的资源预留请求。

4 TCC与XA 的区别

1、XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。

XA事务中的两阶段提交内部过程是对开发者屏蔽的,其内部会委派给TransactionManager进行真正的两阶段提交,因此开发者从代码层面是感知不到这个过程的。而事务管理器在两阶段提交过程中,从prepare到commit/rollback过程中,资源实际上一直都是被加锁的。如果有其他人需要更新这两条记录,那么就必须等待锁释放。

2、TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。

TCC中的两阶段提交并没有对开发者完全屏蔽,也就是说从代码层面,开发者是可以感受到两阶段提交的存在。因为业务需要接口进行改造。如上述航班预定案例:

在第一阶段,航空公司需要提供try接口(机票资源预留)。

在第二阶段,航空公司提需要提供confirm/cancel接口(确认购买机票/取消预留)。开发者明显的感知到了两阶段提交过程的存在。

另外,TCC第一阶段的try或者第二阶段的confirm/cancel在执行过程中,一般都会开启各自的本地事务,来保证方法内部业务逻辑的ACID特性。其中:

1、try过程的本地事务,是保证资源预留的业务逻辑的正确性。

2、confirm/cancel执行的本地事务逻辑确认/取消预留资源,以保证最终一致性,也就是所谓的 补偿型事务 (Compensation-Based Transactions)。

由于是多个独立的本地事务,因此不会对资源一直加锁。

另外,这里提到confirm/cancel执行的本地事务是补偿性事务, 关于什么事补偿性事务,atomikos 官网上有以下描述: 

分布式事务:TCC两阶段异步补偿型

红色框中的内容,是对补偿性事务的解释。大致含义是,"补偿是一个独立的支持ACID特性的本地事务,用于在逻辑上取消服务提供者上一个ACID事务造成的影响,对于一个长事务(long-running transaction),与其实现一个巨大的分布式ACID事务,不如使用基于补偿性的方案,把每一次服务调用当做一个较短的本地ACID事务来处理,执行完就立即提交”。

在这里,第二阶段confirm和cancel的本地事务就是补偿事务,用于取消try阶段本地事务造成的影响。因为第一阶段try只是预留资源,之后必须要明确的告诉服务提供者,这个资源你到底要不要,对应第二阶段的confirm/cancel。

小提示:到这里,读者应该明白为什么把TCC叫做两阶段异步补偿型事务了,提交过程分为2个阶段,第二阶段的confirm/cancel执行的事务属于补偿事务。 

5 TCC 与 DTP事务模型

在介绍完TCC的基本概念之后,我们再来比较一下 TCC事务模型DTP事务模型

首先对DTP(Distributed Transaction Processing)事务模型进行简单的介绍。 DTP模型是X/OPEN组织定义的分布式事务处理规范,规定了了构成DTP模型的5个基本元素。

  • 应用程序(Application Program ,简称AP): 用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作。

  • 资源管理器(Resource Manager,简称RM): 如数据库、文件系统等,并提供访问资源的方式。

  • 事务管理器(Transaction Manager ,简称TM): 负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。

  • 通信资源管理器(Communication Resource Manager,简称CRM) :控制一个TM域(TM domain)内或者跨TM域的分布式应用之间的通信。

  • 通信协议(Communication Protocol,简称CP)

为了简化问题,这里我们只关注AP、TM和RM。 DTP模型规定三者之间如何交互:

分布式事务:TCC两阶段异步补偿型

一句话总结三者之间的关系:AP通过TM来操作RM事务分支的提交或者回滚。

另外,需要注意的一点是,我们看到了XA两阶段提交协议只出现在RM和TM的连线上,也就是说,XA协议实际上规定的是RM和TM之间交互的接口 (XA Interface) ,对于应用AP来说,实际上是无感知的。

在之前的航班预定TCC案例中,机票代理商可以当做AP,航空公司作为资源提供者,可以当做RM,但是好像还少了个事务管理器TM。事实上,在TCC中,我们也需要有一个事务管理器。

下面通过一张图完整的比较TCC事务模型和DTP事务模型:

分布式事务:TCC两阶段异步补偿型

这两张图看起来差别较大,通过后面的介绍,读者会发现二者实际上是完全相同。(别怀疑我的眼神,我没瞎!!! 分布式事务:TCC两阶段异步补偿型 )

1) 事务参与者比较

  • TCC模型中的主业务服务 相当于 DTP模型中的AP

  • TCC模型中的从业务服务 相当于 DTP模型中的RM

在DTP模型中,应用AP操作多个资源管理器RM上的资源;而在TCC模型中,是主业务服务操作多个从业务服务上的资源。例如航班预定案例中,机票代理商就是主业务服务,而川航和东航就是从业务服务,主业务服务需要使用从业务服务上的机票资源。不同的是DTP模型中的资源提供者是类似于Mysql这种关系型数据库,而TCC模型中资源的提供者是其他业务服务。

2) 两阶段提交比较

  • TCC模型中,从业务服务提供的try、confirm、cancel接口 

  • 相当于 DTP模型中RM提供的prepare、commit、rollback接口

XA协议中规定了DTP模型中定RM需要提供prepare、commit、rollback接口给TM调用,以实现两阶段提交。

而在TCC模型中,从业务服务相当于RM,提供了类似的try、confirm、cancel接口。

3) 事务管理器比较

DTP模型和TCC模型中都有一个事务管理器。不同的是:

  • 在DTP模型中,阶段1的(prepare)和阶段2的(commit、rollback),都是由TM进行调用的。

  • 在TCC模型中,阶段1的try接口是主业务服务调用(绿色箭头),阶段2的(confirm、cancel接口)是事务管理器TM调用(红色箭头)。这就是 TCC 分布式事务模型的二阶段异步化功能,从业务服务的第一阶段执行成功,主业务服务就可以提交完成,然后再由事务管理器框架异步的执行各从业务服务的第二阶段。这里牺牲了一定的隔离性和一致性的,但是提高了长事务的可用性。

TCC事务的优缺点:

优点: XA两阶段提交资源层面的,而TCC实际上把资源层面二阶段提交上提到了业务层面来实现。有效了的避免了XA两阶段提交占用资源锁时间过长导致的性能地下问题。

缺点: 主业务服务和从业务服务都需要进行改造,从业务方改造成本更高。还是航班预定案例,原来只需要提供一个购买接口,现在需要改造成try、confirm、canel3个接口,开发成本高。

6 TCC For REST案例

通过前面的介绍,基本介绍了TCC的工作原理。在本节中,笔者借用Atomikos官网提供的<<Tcc For Rest>>进行TCC案例讲解,以加深理解。

需要注意的是,这个案例的主要目的是说明,在使用基于HTTP的REST服务中,TCC模型中各个参与方的API接口应该如何设计。通常一个TCC方案是不会依赖于底层通信框架的,例如我们也可以使用业界比较火的spring cloud、dubbo等。这个时候,提供实现类似接口的功能就可以了。

首先我们总结一下TCC模型各个参与方需要提供的API:

Participant API: 从业务服务需要提供的API,其需要提供try接口供主业务服务调用,需要提供confirm、cancel接口供事务管理器调用。这里将从业务服务称之为Participant。

Transaction Coordinator API: 事务管理/协调器需要提供的API,其需要提供事务日志上报接口,让主业务活动上报try阶段各个从业务活动资源是否预留成功的信息

Application: 主业务服务,其不需要提供任何接口,只需要操作上述 Participant、 Transaction Coordinator提供的接口即可。

熟悉的配方,熟悉的味道,<<Tcc For Rest>>采用的依然航班预定案例,只不过是官方原版案例。如下图所示:

分布式事务:TCC两阶段异步补偿型

其中:

  • Booking Proccess是主业务活动,处理机票预定业务

  • Swiss和easyjet是从业务服务,可以理解为两个不同航空公司的机票预定系统

  • Transaction Coordinator是事务协调器,或者称之为事务管理器。

上图中描述的整体流程如下所示:


 

1 Booking Proccess接受到一个需要中转的航班预定请求(bookTrip)

 

-1.1:Booking Proccess向swiss发起机票预定请求 R1,其中/booking/A表示swis提供的预留机票资源的try接口

 

-1.2:Booking Proccess向easyjet发起机票预定请求 R2,其中/booking/B表示easyjet提供的预留机票资源的try接口

 

-1.3:Booking Proccess将请求1.1、1.2步骤中try的结果合并上报给Transaction Coordinator。

 

-1.3.1 Transaction Coordinator 向swiss发送确认执行业务操作的请求

 

-1.3.2 Transaction Coordinator 向easyjet发送确认执行业务操作的请求

6.1 Participant API

从业务服务需要提供try、confirm和cancel三个接口,其中try接口是给主业务服务调用的,confirm和cancel是给事务协调器调用的。

try接口

在1.1和1.2步骤中,Booking Proccess向swiss和easyjet提供的try接口分别发起机票预留请求,swiss和easyjet作为参与者,返回的响应格式如下所示

{"participantLink":

{"uri":"http://www.example.com/part/123",

"expires":"2014-01-11T10:15:54Z"

}

}

这里返回的是一个JSON格式,事实上返回的格式什么是无所谓的,不过Atomikos官方建议使用JSON格式。其中:

  • uri :表示的是稍后在Confirm确认执行业务操作时,需要调用的url

  • expires:表示截止时间,也就是说,如果超过这个时间依然没有确认购买,那么swiss和easyjet将会自动取消这个机票的预留

confirm接口

Transaction Coordinator判断资源都预留成功,解析出json格式中的uri部分,向swiss和easyjet发送确认执行请求(Confirm),请求格式如下:

PUT /part/123 HTTP/1.1

Host: www.example.com

Accept: application/tcc

注意请求头中Accept接受的MIME类型, 暗示了客户端的语义期望。 这个并不是强制的。如果swiss和easyjet都确认执行成功,应该返回204,表示执行成功

HTTP/1.1 204 No Content

需要注意的是,如果在截止时间(expires)后发送确认执行的请求,swiss和easyjet应该返回404

HTTP/1.1 404 Not Found

而Transaction Coordinator自身也应该有这种超时判断,以为较小的expires为准,当超过这个时间时,就不应该发送confirm确认执行的请求。

而在expires之前如果确认执行失败,Transaction Coordinator应该进行重试。

cancel接口(可选实现)

参与者可以选择是否显式的提供cancel接口,如果提供了。Transaction Coordinator应该发送DELETE请求,告诉参与者取消资源预留,格式如下

DELETE /part/123 HTTP/1.1

Host: www.example.com

Accept: application/tcc

如果取消成功,则返回

HTTP/1.1 204 No Content

如果取消失败,也不会影响结果。前面提到过,资源预留都有一个expires截止时间,超过这个截止时间,参与者就可以主动取消这个预留的资源。

如果是因为超时,参与者自行取消资源预留的情况下,应该返回

HTTP/1.1 404 Not Found

另外,由于参与具备超时自动取消预留的功能,因此DELETE接口是可选的。如果参与者不提供DELETE接口来支持显式cancel,可以返回

HTTP/1.1 405 Method Not Allowed

不过笔者还是建议显式的提供cancel接口,例如,如果swiss预留成功,easyjet预留失败。对于预留失败的情况,其实我们已经没有必要进行cancel了。但是swiss预留成功了,如果等待超时自动取消,可能会比较耗时,通过显式提供cancel接口,来更快的取消预留的资源,将机票卖给其他客户。

6.2 Transaction Coordinator

TCC模型中,主业务服务需要将事务日志上报给事务管理器/协调器,然后由协调器来调用从业务服务的confirm或者cancel接口。因此事务管理器/协调器必须提供一个事务日志上报的接口。而本节就是介绍这个接口接受的参数类型和响应类型。

主业务服务在第一阶段,调用各个从业务服务的try接口,并且将响应合并起来上传报给Transaction Coordinator。考虑一下,这里应该分为2种情况:

  • 所有的try接口都调用成功了,因此主业务服务希望Transaction Coordinator向各个从业务服务进行confirm

  • try接口部分成功,部分失败。因此主业务服务希望Transaction Coordinator对已经try成功的从业务服务都进行cancel

因此对Transaction Coordinator来说,需要提供2个事务日志上报接口:confirm接口、cancel接口.

confirm接口

请求格式如下:

PUT /coordinator/confirm HTTP/1.1

Host: www.taas.com

Content-Type: application/tcc+json

{

"participantLinks": [

{

"uri": "http://www.example.com/part1",

"expires": "2014-01-11T10:15:54Z"

},

{

"uri": "http://www.example.com/part2",

"expires": "2014-01-11T10:15:54+01:00"

}

]

}

然后协调器会对参与者逐个发起Confirm请求, 如果一切顺利那么将会返回如下结果

HTTP/1.1 204 No Content

如果发起Confirm请求的时间太晚, 那么意味着所有被动方都已经进行了超时补偿

HTTP/1.1 404 Not Found

最糟糕的情况就是有些参与者确认了, 但是有些就没有. 这种情况就应该要返回409, 这种情况在Atomikos中定义为启发式异常

HTTP/1.1 409 Conflict

当然, 这种情况应该尽量地避免发生, 要求Confirm与Cancel实现幂等性, 出现差错时协调器可多次对参与者重试以尽量降低启发性异常发生的几率. 万一409真的发生了, 则应该由请求方主动进行检查或者由协调器返回给请求方详细的执行信息, 例如对每个参与者发起故障诊断的GET请求, 记录故障信息并进行人工干预.

cancel接口

一个cancel请求跟confirm请求类似, 都是使用PUT请求, 唯一的区别是URI的不同。

唯一可预见的响应就是

HTTP/1.1 204 No Content

因为当预留资源没有被确认时最后都会被释放, 所以参与者返回其他错误也不会影响最终一致性。

7 推荐阅读资料

7.1 开源TCC实现方案

  • tcc-transaction

  • ByteTCC

  • spring-cloud-rest-tcc

  • seata 

github上搜一下即可,事实上seata一出,谁与争锋,其他开源的tcc解决方案都黯然失色,只要看这个就行了,如果想深入理解,可以阅读我嘉扬的seata源码解析: http://t.im/gvjw

7.2  Atomikos的官方文档

  • Composite Transactions for SOA

  • Transaction management API for REST: TCC

  • Atomikos ExtremeTransactions Guide

官方文档实际上最能反映TCC最原始的面貌,现在各种开源实现方案,实际上都是在这个基础上的衍化。 找不到的话,关注公众号加我好友,备注TCC。

识别二维码关注我

分布式事务:TCC两阶段异步补偿型

往期精彩

分布式事务概述

异地多活场景下的数据同步之道

数据库中间件详解

深入理解数据库编程中的超时设置

详解HTTP 与TCP中Keep-Alive机制的区别

TCP粘包、拆包与通信协议详解

史上最详细mybatis与spring整合教程

源码剖析 Mybatis 映射器(Mapper)工作原理

剖析Spring多数据源

最后提示,可能有人看过这篇文章,这是我2018年2月份在我的博客上写的文章,现在搬到公众号上

查看原文: 分布式事务:TCC两阶段异步补偿型

  • greenostrich
  • crazyfish
  • silverpeacock
  • yellowostrich
  • goldentiger
  • greenkoala
  • organicwolf
  • bluebird