分布式事务 TCC 其实很简单

2019 Java 开发者跳槽指南.pdf (吐血整理)….>>>

假设业务采用微服务架构,有库存,订单两个服务,同时还有个 C 端服务,专门用来聚合其它各个服务方的功能,统一给到 C 端。


分布式事务 TCC 其实很简单


现在让你实现一个需求,对某商品下单时,要求只要下单成功,就必须占用一个库存,如果没有下单成功,则一定不能占用库存。


实现很简单,先调用库存接口,扣减库存,扣减成功,再调用订单接口创建订单。


分布式事务 TCC 其实很简单


如果调用创建订单接口失败,则再调用一次增加库存的接口,如果调用增加库存的接口失败就重试。


分布式事务 TCC 其实很简单


如果这些接口调用不会出现异常,那么上面这种做法貌似是可以工作的。


但接口调用不可能不出现异常,你总会碰到各种各样的异常情况,超时是最典型的一种。


现在假设调用扣减库存接口超时,你如何知道扣减库存成功没有呢?没办法知道!即使接口超时失败,也可能成功扣减了库存,只是给你返回结果的时候超时了。


分布式事务 TCC 其实很简单


在调用方看来一次失败的调用,在服务方看来是成功的。


这个情况很明显会导致多扣库存。不能满足我们前面提出的需求。


再看另一种情况。


扣减库存调用成功,创建订单接口超时,但实际订单创建成功了。调用方发现创建订单接口超时了,于是调用增加库存接口把库存又加回去了。相当于创建了一个订单却没有消耗任何库存,这显然也是不对的。


分布式事务 TCC 其实很简单


你会发现,上面的场景其实是个典型的事务场景,创建订单和扣减库存要么同时成功,要么同时失败。


只不过由于订单和库存服务分别是两个独立的服务,它们都有自己专属的数据库,所以通过在数据库层面来实现事务的方式无法应用在这里。


这就是一个典型的分布式事务的例子。


那分布式事务要怎么实现呢?这篇文章就用上面这个案例来讲一讲 TCC 的实现方法。


TCC 是 Try-Confirm-Cancel 的缩写,顾名思义,就是将接口分为 Try、Confirm、Cancel 三个步骤来完成。


原来由一个接口完成的功能,现在分为三个接口来完成,如果所有服务的 try 接口都成功,则再调用所有服务的 confirm 接口来确认提交。如果有任意一个服务的 try 接口失败,则调用对应服务的 cancel 接口来取消提交。


拿我们上面的实例来讲,具体如何将原来一个接口的功能,拆成 TCC 三个接口呢?


try 接口的实现要保证,只要 try 调用成功,那么随后的 confirm 也必须成功。注意,这里说的是逻辑上必须成功,不包括网络错误等异常情况。cancel 也一样。


扣减库存的 try 接口可以实现为锁定库存,如果有可用库存,则锁定一个库存,此时,前端展示的可用库存应该等于 所有库存 - 锁定库存


扣减库存的 confirm 接口将锁定的库存实实在在的扣减掉。


扣减库存的 cancel 接口将锁定的库存释放掉。


这样,只要扣减库存 try 成功,就代表我独占了一个库存,随后的 confirm 扣减这个独占库存是应该必须成功的。


创建订单的 try 接口可以实现为创建一个订单,只不过此时订单的状态可以设为一个较特殊的状态,此订单实实在在存在,但由于整个事务还未完成,库存只是锁定,并没有实际扣减掉,相当于下单只完成了一部分,没有全部完成,并不是一个有效的可以支付的订单。


创建订单的 confirm 接口实现就比较简单了,直接将订单状态改为待支付即可。此时订单是一个正常的可支付订单了。


创建订单的 cancel 接口也可以简单实现,直接将订单删除即可。


现在我们再来完成 C 端服务的逻辑,首先调用扣减库存的 try 接口,如果成功,则调用创建订单的 try 接口,如果也成功,则分别调用扣减库存和创建订单的 confirm 接口。


分布式事务 TCC 其实很简单


如果扣减库存的 try 接口失败,则调用扣减库存的 cancel 接口,流程结束。如果创建订单的 try 接口失败,则调用扣减库存的 cancel 接口把锁定的库存释放掉,再调用创建订单的 cancel 接口删除订单,流程结束。


分布式事务 TCC 其实很简单


如果 confirm 或者 cancel 失败怎么办?重试!


分布式事务 TCC 其实很简单


但重试过程中宕机了怎么办?上下文都没了,重启后还咋重试?


这个问题的本质是,分布式事务的状态没有持久化保存,因此,应对不了宕机之类的故障。


所以事务的状态我们得持久化保存起来,那么我们要保存事务的哪些状态呢?那得问我们自己,为了从故障中恢复事务,我们需要知道事务的哪些状态。


文章后面会逐步分析出来我们需要保存哪些状态,目前来看,最起码执行了哪些 try 接口是需要保存的。


所以,每次调用 try 接口之前,得把调用的接口信息保存起来,表示本次事务的执行进度,我们把这种信息叫做 事务日志


为了唯一标示一个事务,我们用一个全局唯一 id 用来表示事务 id(全局唯一的 id 生成算法以前的文章介绍过)。事务的事务日志都通过事务 id 关联起来。


好,现在执行过程变成了这样,每次事务开始前,先生成一个唯一的事务 id,每次调用 try 接口之前,先将该 try 接口持久化到数据库中,用事务 id 作为 key 来保存。


分布式事务 TCC 其实很简单


这样,事务崩溃重启后,我可以知道,当前事务已经执行了哪些接口的 try(最后一个接口的 try 可能并没有执行,因为有可能刚写完事务日志,还没来得及调用 try 就崩溃了)。


那么事务崩溃重启后,应该怎么恢复呢?合理的做法应该是下面这样:


1. 如果事务在 try 阶段崩溃,则重启后取出事务日志的 try 接口调用记录,依次调用它们的 cancel 接口,相当于回滚事务。


2. 如果事务在 confirm 阶段崩溃,则重启后取出事务日志的 try 接口调用记录,依次调用它们的 confirm 接口,相当于提交事务。


3. 如果事务在 cancel 阶段崩溃,则重启后取出事务日志的 try 接口调用记录,依次调用它们的 cancel 接口,相当于回滚事务。


但我们怎么知道事务崩溃前处于哪个阶段呢?只能把这个阶段信息记录下来才行,我们把这个阶段信息叫做事务状态。


初始时事务状态为 try,当事务的所有 try 接口调用完后,事务状态转移到 confirm。如果 try 阶段有任何接口调用失败,则事务状态转移到 cancel。如果 confirm 或 cancel 完成,则状态转移到 end 。


然后我们再来看下不同情况下的事务日志和事务状态。


创建订单 try 失败,开始 cancel 前:


分布式事务 TCC 其实很简单


try 调用都成功,confirm 过程中:


分布式事务 TCC 其实很简单


confirm 完成:


分布式事务 TCC 其实很简单


创建订单 try 失败,cancel 过程中:


分布式事务 TCC 其实很简单


创建订单 try 失败,cancel 完成:


分布式事务 TCC 其实很简单


事务崩溃重启后,由 C 端服务读取事务崩溃前状态,来决定是回滚,还是提交事务。


我们再来看一下边界情况。


1. 当所有 try 接口调用完成,事务状态还没来得及改变为 confirm 时,此时事务状态处于 try 状态,崩溃重启后回滚事务。


2. 当所有 confirm 接口调用完成,事务状态还没来得及改变为 end 时,此时事务处于 confirm 状态,崩溃重启后继续调 confirm 提交事务。


3. 当所有 cancel 接口调用完成,事务状态还没来得及改变为 end 时,此时事务处于 cancel 状态,崩溃重启后继续调 cancel 回滚事务。


无论是哪种情况,事务崩溃重启后,经过重试,最终一定会达成一个一致的状态,也就是说最终事务要么全部成功,要么全部失败,是满足我们对事务的要求的。


上面的例子,事务发起方是 C 端服务,我们把这个发起方叫做分布式事务的 协调者,把参与事务的订单服务和库存服务叫做分布式事务的 参与者


分布式事务 TCC 其实很简单


分布式事务执行过程中崩溃后,是由协调者根据崩溃前的事务日志来恢复事务,恢复的手段就是不断重试,或者回滚,或者继续提交。那有人就问了,如果重试一直失败怎么办?


重试一直失败是有可能的,这种情况,除了人工介入,别无他法。不要去幻想有什么完美的机器解决方案,不存在的。


由于 confirm 和 cancel 接口都可能重试,因此,这俩接口必须是幂等的。


我们可以在调用时把唯一的事务 id 一块传到参与者,这样方便参与者实现接口的幂等。


好了,以上就是 TCC 的一个实例实现,相信看完上面这些内容,你应该知道 TCC 怎么玩了。


但不要高兴得太早,这还只是入门了而已,这篇文章举的例子实际上算是最简单的例子了,像库存这种东西不是加一就是减一,如果把库存替换为其他的服务,难度就会升级。另外 TCC 在实现上还有些细节你未必知道,让我出道题来“为难”下你吧!^_^


请听题!


前面我们知道如果 try 接口调用失败,应该调用对应的 cancel 接口回滚,但有这么一种特殊的情况,try 接口的网络请求包因为网络问题而延迟了,结果返回超时失败,然后协调者调用 cancel 回滚,结果 cancel 先于 try 到达参与者,这种情况该如何处理呢?


分布式事务 TCC 其实很简单

本篇文章就先写到这里,下一篇 TCC 的文章,我再一一讲解这些问题。


原文始发于微信公众号(野马的架构之路):分布式事务 TCC 其实很简单