TCC事务介绍
TCC(Try-Confirm-Cancel)
是除可靠消息队列以外的另一种常见的分布式事务机制,它是由数据库专家帕特 · 赫兰德(Pat Helland
)在2007年撰写的论文《Life beyond Distributed Transactions: An Apostate’s Opinion》中提出的。正式以Try-Confirm-Cancel
作为名称的是Atomikos公司,其注册了TCC商标。
Atomikos公司在商业版本事务管理器ExtremeTransactions中提供了TCC方案的实现,但是由于其是收费的,因此相应的很多的开源实现方案也就涌现出来,如:tcc-transaction
、ByteTCC
、hmily
、spring-cloud-rest-tcc
。
上次我们分享的使用RocketMQ实现分布式事务,虽然它也能保证最终的结果是相对可靠的,过程也足够简单(相对于TCC来说),但可靠消息队列的整个实现过程完全没有任何隔离性可言。
如果业务需要隔离,我们通常就应该重点考虑TCC方案,它天生适合用于需要强隔离性的分布式事务中。
在具体实现上,TCC的操作有点复杂,它是一种业务侵入性较强的事务方案,要求业务处理过程必须拆分为“预留业务资源”和“确认/释放消费资源”两个子过程。另外,你看名字也能看出来,TCC的实现过程分为了三个阶段:
• Try: 尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好事务需要用到的所有业务资源(保障隔离性)。
• Confirm: 确认执行阶段,不进行任何业务检查,直接使用Try阶段准备的资源来完成业务处理。注意,Confirm
阶段可能会重复执行,因此需要满足幂等性。
• Cancel: 取消执行阶段,释放Try阶段预留的业务资源。注意,Cancel
阶段也可能会重复执行,因此也需要满足幂等性。
深入分析
我们用一个下单过程来说明TCC
的工作机制,正常的下单会包含创建订单,扣减库存,扣减账户余额,增加积分等功能,所以会调用这些子系统, 有的系统使用http进行远程调用,有的使用rpc进行调用,我们这个例子中有五个微服务,分别为业务微服务(BusinessMicroservice)
, 库存微服务(StockMicroservice)
,订单微服务(OrderMicroservice)
,积分微服务(IntegralMicroservice)
, 账户微服务(AccountMicroservice)
,在业务微服务中通过rpc统一调用其他微服务。
使用TCC
就需要设计三部分的业务代码,分别是Try
阶段,Confirm
阶段,Concel
阶段。
Try
从字面意思我们看出Try
是尝试的意思,这个阶段其实就是对资源进行预留,需要工程师自己去设计,在Try
阶段代码设计的好坏,在很大程度上影响 整个分布式事务的结果,所以对工程师的设计能力和思考能力有一定的考验。
-
预扣减库存
这一步并没有真正地扣减库存,所以叫做预扣减库存,首先先检查库存是否被扣减,比如我下单量为2,此时库存为1,那么显然不能扣减,如果库存为 10,那么证明可以扣减,但是此时并不会真正地扣减库存,我们需要对其进行一些设计,这需要根据自己的业务场景去设计,比如可以新建一个冻结库存 的表,因为下单量为2,库存为10,所以此时执行10-2,库存为8,我们就将库存更新为8,然后在库存冻结表中插入一条扣减记录,记录是某个用户的下单数量, ,那么扣减库存这一步就完成了。
-
预创建订单
预创建订单也不是真正地创建订单,我们可以将订单的状态改为创建中,这个状态值只是用来表示订单的状态,这个状态并非真实订单的状态,而是为了使用 分布式事务而使用的状态,并不是商品生命周期中的属性。
-
预增加积分
这里也不是真正地增加积分,我们可以在积分记录中添加一个冻结积分字段,例如,积分余额为100,需要增加的积分20,那么此时积分余额值就变为120(100+20), 冻结积分为20。
-
预扣减余额
扣减余额我们在账户表中添加一个冻结字段,例如,如果账户余额为1000,本次需要扣减200,那么此时余额就变为800(1000-200), 冻结金额为200。
![](https://i-blog.csdnimg.cn/direct/5dd8c874dd65453cad449dd290ce76d7.png)
Confirm
如果Try阶段所有的业务都成功地执行完毕,没有出现错误,那么在Confirm阶段就会执行所有分支事务,这个阶段唯一做的就是提交事务(完成我们自己定义的逻辑), 不会再去校验数据,比如库存是否充足等,因为第一阶段已经检验过并且通过了。
-
扣减库存
因为库存表中在Try阶段已经减过库存,只是扣减记录保存在了库存冻结表,所以这一步就需要删除冻结表的记录,通过用户Id去删除。
-
设置订单状态为已创建
Try阶段将订单状态设置为创建中,到了这里就需要将订单状态设置为已创建,代表订单事务已经完成。
-
增加积分
这里的增加积分在Try阶段其实已经做了,只是预留了一个冻结积分,所以这里就需要更新冻结积分,将其更新为0,代表增加积分这个事务已经完成。
-
扣减余额
这里的扣减余额在Try阶段已经做了,只是预留了一个冻结金额,所以这里就需要更新冻结金额,将其更新为0,代表扣减余额事务已经完成。
到这里TCC
的Confirm
就完成了,Confirm
阶段唯一做的事情就是执行任务,不做任何的数据校验。
Cancel
上面的Confirm
阶段是Try
阶段所有的操作都正常,没有出错,如果有一种的一个操作出现异常或资源出错,那么就会进入Cancel
阶段,Cancel
阶段会对Try
阶段的所有操作进行回滚,也就是将数据恢复到刚开始的时候。
-
恢复库存
恢复库存就是查询库存冻结表中的冻结库存,然后加上库存表中库存(库存表库存 = 库存表库存 + 冻结库存), 8 + 2 = 10 , 然后删除冻结库存记录, 代表事务回滚成功。
-
设置订单状态为取消
Try
阶段订单状态为创建中,那么因为在Try
阶段某个分支事务出错,所以需要将订单状态置为已取消(这个状态并不是订单生命周期中的状态), 而是为事务设计的状态,代表事务回滚成功。
-
恢复积分
用冻结积分加上积分余额(积分余额 = 积分余额 + 冻结积分),然后更新到积分余额上,随后将冻结积分更新为0,代表事务回滚成功。
-
恢复余额
用冻结余额加上余额(余额 = 余额+ 冻结余额),更新到余额上面,将冻结余额更新为0,代表事务回滚成功。
Cancel
阶段就完成了,Cancel
阶段主要是对各个事务进行恢复,他是基于Try
阶段的数据进行恢复。
TCC 模式存在的问题
seata 的 TCC 模式存在一些异常场景会导致出现空回滚、幂等、悬挂等问题。TCC 模式是分布式事务中非常重要的事务模式,但是幂等、悬挂和空回滚一直是TCC 模式需要考虑的问题,在1.5.1之前的版本需要我们自己加个表去解决,并且需要在编码阶段去处理,侵入性大并且也增加了开发难度。在 1.5.1 版本开始,seata帮我们解决了这些问题。方式就是添加 tcc_fence_log 事务控制表。
幂等性问题
TC执行confirm或cancel后,因为网络问题,没有收到RM返回的通知,TC会以为没有执行成功,这时候TC就会再次进行调用confirm或cancel,多次对数据做修改,导致幂等性问题。
同样的也是在 TCC 事务控制表中增加一个记录状态的字段 status,该字段有 3 个值,分别为:
1. tried:1
2. committed:2
3. rollbacked:3
二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。
悬挂问题
悬挂简单点理解就是 cancel 比 try 先执行,造成 try 的资源无法回滚。
场景:try 执行的比较慢,导致调用 try 的服务超时了,这时候TC就去调了 cancel,调完 cancel 后 try 执行成功了,这时候 try 的资源无法回滚。
如上图所示,在执行参与者 A 的一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。
解决方案:这种情况不能够让 try 执行成功,因为只要 try 执行成功了就没法回滚了。
空回滚问题
空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法。
解决方案:要想防止空回滚,那么必须在 Cancel 方法中识别这是一个空回滚,在二阶段执行回滚 rollback 的时候,需要先检查一阶段是否有执行过 try 方法,如果执行过才能执行回滚 rollback 方法,如果没有执行过就不任何操作,Seata 的做法是新增一个 TCC 事务控制表 tcc_fence_log,在 try 阶段执行成功后在 tcc_fence_log 表中插入一条记录,在 rollback 时去查询 tcc_fence_log 表是否有 try 阶段执行成功的记录,如果有,才会执行 rollback,如果不存在记录说明 Try 方法没有执行,则不再执行 rollback 方法。