3.4.3 TCC事务

TCC是另一种常见的分布式事务机制,它是“Try-Confirm-Cancel”三个单词的缩写,是由数据库专家Pat Helland在2007年撰写的论文“Life beyond Distributed Transactions:An Apostate’s Opinion”[1]中提出。

前面介绍的可靠消息队列虽然能保证最终结果的相对可靠性,过程也足够简单(相对于TCC来说),但整个过程完全没有任何隔离性可言,虽然在一些业务中隔离性是无关紧要的,但在有些业务中缺乏隔离性就会带来许多麻烦。譬如在本章的场景事例中,缺乏隔离性会带来的一个明显问题便是“超售”:如两个客户在短时间内都成功购买了同一件商品,而且他们各自购买的数量都不超过目前的库存,但他们购买的数量之和却超过了库存。如果这件事情属于刚性事务,且隔离级别足够时是可以完全避免的,譬如,以上场景就需要“可重复读”(Repeatable Read)的隔离级别,以保证后面提交的事务会因为无法获得锁而导致失败,但用可靠消息队列就无法保证这一点,这部分属于数据库本地事务方面的知识,可以参考前面的讲解。如果业务需要隔离,那架构师通常就应该重点考虑TCC方案,该方案天生适用于需要强隔离性的分布式事务中。

在具体实现上,TCC较为烦琐,它是一种业务侵入式较强的事务方案,要求业务处理过程必须拆分为“预留业务资源”和“确认/释放消费资源”两个子过程。如同TCC的名字所示,它分为以下三个阶段。

·Try:尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好全部需要用到的业务资源(保障隔离性)。

·Confirm:确认执行阶段,不进行任何业务检查,直接使用Try阶段准备的资源来完成业务处理。Confirm阶段可能会重复执行,因此本阶段执行的操作需要具备幂等性。

·Cancel:取消执行阶段,释放Try阶段预留的业务资源。Cancel阶段可能会重复执行,因此本阶段执行的操作也需要具备幂等性。

按照我们的场景事例,TCC的执行过程应该如图3-8所示。

1)最终用户向Fenix’s Bookstore发送交易请求:购买一本价值100元的《深入理解Java虚拟机》。

2)创建事务,生成事务ID,记录在活动日志中,进入Try阶段。

·用户服务:检查业务可行性,若可行,将该用户的100元设置为“冻结”状态,通知下一步进入Confirm阶段;若不可行,通知下一步进入Cancel阶段。

·仓库服务:检查业务可行性,若可行,将该仓库的1本《深入理解Java虚拟机》设置为“冻结”状态,通知下一步进入Confirm阶段;若不可行,通知下一步进入Cancel阶段。

·商家服务:检查业务可行性,不需要冻结资源。

3)如果第2步所有业务均反馈业务可行,将活动日志中的状态记录为Confirm,进入Confirm阶段。

图3-8 TCC的执行过程

·用户服务:完成业务操作(扣减那被冻结的100元)。

·仓库服务:完成业务操作(标记那1本冻结的书为出库状态,扣减相应库存)。

·商家服务:完成业务操作(收款100元)。

4)第3步如果全部完成,事务正常结束,如果第3步中任何一方出现异常,不论是业务异常还是网络异常,都将根据活动日志中的记录,重复执行该服务的Confirm操作,即进行最大努力交付。

5)如果第2步有任意一方反馈业务不可行,或任意一方超时,则将活动日志的状态记录为Cancel,进入Cancel阶段。

·用户服务:取消业务操作(释放被冻结的100元)。

·仓库服务:取消业务操作(释放被冻结的1本书)。

·商家服务:取消业务操作。

6)第5步如果全部完成,事务宣告以失败回滚结束,如果第5步中任何一方出现异常,不论是业务异常还是网络异常,都将根据活动日志中的记录,重复执行该服务的Cancel操作,即进行最大努力交付。

由上述操作过程可见,TCC其实有点类似2PC的准备阶段和提交阶段,但TCC是在用户代码层面,而不是在基础设施层面,这为它的实现带来了较高的灵活性,可以根据需要设计资源锁定的粒度。TCC在业务执行时只操作预留资源,几乎不会涉及锁和资源的争用,具有很高的性能潜力。但是TCC也带来了更高的开发成本和业务侵入性,即更高的开发成本和更换事务实现方案的替换成本,所以,通常我们并不会完全靠裸编码来实现TCC,而是基于某些分布式事务中间件(譬如阿里开源的Seata)去完成,尽量减轻一些编码工作量。

[1] 下载地址:https://www-db.cs.wisc.edu/cidr/cidr2007/papers/cidr07p15.pdf。