我不喜欢分布式的TCC模式。转载本文请联系程序员jinjunzhu公众号。在分布式事务解决方案中,TCC是比较经典的一种模式,它采用2-phasecommit的思想来实现分布式事务的最终一致性。但最近我有点不喜欢TCC模式。TCC回顾什么是TCC?以经典的电子商务系统为例,当客户购买商品时,系统需要三个服务来配合。订单服务增加订单,库存服务减少库存,账户服务减少金额。如下图所示:如果我们使用上图所示的方式,每个服务提交自己的事务,很有可能会出现数据不一致的情况。因为三个服务使用不同的数据库,所以不是原子操作。比如订单服务提交成功,账户服务失败,导致数据不一致。TCC的思想是使用两阶段提交。在try阶段,每个服务首先尝试预留资源。如果预留成功,则进入提交阶段提交事务。如果服务预订失败,则进入取消阶段以取消交易。这就需要增加一个协调节点,向三个服务下发命令,获取每个服务的分支事务执行结果。try阶段如下图所示:在try阶段,如果每个服务预留资源成功,协调节点会向每个服务下发commit命令,如下图:所有服务commit成功后,整个事务完成了。代码实现协调节点需要为每个分布式事务提供一个全局事务id,称为xid,用于与每个服务的本地事务进行绑定。我们以账户服务为例,我们看一下try/commit/cancel这三个阶段的代码:这段代码使用jdbc处理本地事务,在try阶段我们获取连接并保存在connectionMap中,key是xid,所以在commit/cancel阶段,从connectionMap中取出connection进行commit/rollback。存在问题上面TCC模式的代码实现有没有问题?服务集群如下图所示。如果订单服务集群部署在三台机器上,try请求发送给订单服务1,commit请求发送给订单服务2,connectionMap怎么会有xid=123的值呢?无法提交订单服务的本地交易。所以如果真的要通过保持连接的方式来提交事务,协调节点需要保证同一个xid对应的try/commit/cancel请求到的是同一台机器。肯定有办法,改造注册中心,或者协调节点自己维护服务列表。前者让注册中心耦合业务代码,后者相当于放弃注册中心。注册中心的空提交和协调节点的改造需要大量的工作。还有别的办法吗?让我们进行改进。这里orm框架使用mybatis。代码如下:在try阶段要预留资源。这段代码如果reserved资源预留成功,分支事务其实已经提交了,commit阶段只是空提交,没有实际作用。另一种方式是在try阶段直接返回true,在commit阶段真正提交事务。但这两种方式都违背了TCC的思路。幂等性如果协调节点设置了超时重试,出现下图所示的情况,订单服务1在执行try方法后失败,协调节点如果收不到成功的回复就会重试,这样订单服务就会反复执行try方法。为了避免这个问题,try/confirm/cancel方法必须加入幂等逻辑来记录本地事务对应的全局事务xid的执行状态。空回滚在使用框架实现TCC模式时,会出现空回滚的情况。如上图,因为订单服务1节点出现故障,try方法失败,但是全局事务已经启动,框架必须将这个全局事务推到结束状态,所以不得不调用订单服务cancel方法回滚,订单服务运行空一个cancel方法。为了解决这个问题,try阶段需要记录xid对应的分支事务的执行状态,cancel阶段根据这个记录进行判断。上面说了在使用seata的过程中会出现空回滚。如果发生空回滚,执行cancel方法后全局事务结束。但是由于网络问题,订单服务收到了一个尝试请求。执行try方法后,预回滚资源预留成功,但是这些资源无法释放。这个问题的解决方法是在cancel方法中记录xid对应的分支事务的执行状态。执行try阶段时,首先判断分支事务是否已经回滚。代码入侵try/commit/cancel高TCC入侵业务代码。如果再考虑幂等、空回滚、挂起等,代码入侵会更高。总结TCC是分布式事务中非常经典的一种模式,但是即使借助框架,代码实现也比较复杂。在实际使用中,需要考虑服务集群、空提交、幂等、空回滚、挂起等问题。对业务代码的侵入性很大。
