分布式事务(4)---最终一致性方案之TCC
分布式事务(1)-理论基础
分布式事务(2)---强一致性分布式事务解决方案
分布式事务(3)---强一致性分布式事务Atomikos实战
强一致性分布式事务解决方案要求参与事务的各个节点的数据时刻保持一致,在高并发场景下,系统的性能可能收到影响。而最终一致性方案并不要求数据时刻一致,允许其存在中间状态,只要一段时间后数据能够最终一致即可。
所以基于BASE理论,提出了最终一致性解决方案,典型的有:TCC解决方案,可靠消息最终一致性方案,最大努力通知型解决方案。
其优点是:
1.性能比较高,不会因为长时间持有事务占用资源。
2.具备可用性。
3.适合高并发场景。
缺点是,因为数据的短暂不一致,所以会出现某一时刻数据的不一致。
对于一致性特别高的场景不适用。
我们实现最终一致性方案时,需要注意几个操作:
可查询操作:业务方需要提供可查询接口,来查询数据信息和状态,供其他服务知道数据状态。
幂等操作:同样的参数执行同一个方法,返回的结果都一样。在分布式环境,难免会出现数据的不一致,很多时候为了保证数据的一致,我们都会进行重试。如果不保证幂等,即使重试成功了,也无法保证数据的一致性。我们可以通过业务本身实现实现幂等,比如数据库的唯一索引来约束;也可以缓存(记录)请求和操作结果,当检测到一样的请求时,返回之前的结果。
补偿操作:某些数据存在不正常的状态,需要通过额外的方式使数据达到最终一致性的操作。
TCC
TCC解决方案主要包括三个阶段:try---尝试业务执行,confirm---确定业务执行,cancel---取消业务执行
try阶段完成所有业务的一致性检查,预留必要的业务资源。
confirm阶段,真正执行业务,因为try已经执行了资源预留,所以此阶段不会再检查数据,此阶段的操作需要满足幂等。
cancel阶段,释放try预留的业务资源,此阶段也需要满足幂等。
TCC主要用于跨服务调用下分布式事务问题,适用于具有强隔离性,又严格要求一致性的业务场景。
举个栗子
还是用下单扣库存为例
try阶段:创建订单,并将订单状态设置为待提交,调用库存服务预扣减库存。库存表中库存字段减去订单中的数量,同事在预扣减字段中增加订单中库存数量。以此来预留资源
confirm阶段:如果try全部成功,则进入confirm阶段。此阶段将订单状态修改为已提交,库存服务则将预扣减库存字段的数量减去订单中的数量,实现真正的减库存。
通常TCC方案我们都认为confirm阶段是不会出错的。就是说只要try成功了,那么confirm就一定会成功。如果confirm出错了,那么就需要引入补偿机制或者人工处理。
cancel阶段:try阶段失败或者出现异常,至此那个cancel,订单状态修改为已取消,库存服务将表中库存字段增加订单中的数量,预扣减字段减去订单中的数量,以此实现事务回滚。同样TCC中我们认为cancel阶段一定会执行成功,如果失败也需要引入重试或者人工处理。
TCC方案中锁定资源的粒度小,有利于提高系统性能;confirm和cancel阶段的幂等保证分布式事务执行完成后数据的一致性。由主业务放发起事务,无论是主业务还是分支业务都能集群部署,解决了XA规范的单点故障问题。但是它的代码需要耦合到业务中,参与分布式事务的每个业务方法都需要try,confirm,cancel阶段,增加开发成本。
TCC中需要注意的问题
1.空回滚
当一个分支事务所在的服务发生宕机或者网络异常导致调用失败,并未执行try方法,当恢复后事务执行回滚操作就会调用此分支事务的cancel方法,如果cancel方法不能处理此种情况就会出现空回滚。
是否出现空回滚,我们需要需要判断是否执行了try方法,如果执行了就没有空回滚。解决方法就是当主业务发起事务时,生成一个全局事务记录,并生成一个全局唯一ID,贯穿整个事务,再创建一张分支事务记录表,用于记录分支事务,try执行时将全局事务ID和分支事务ID存入分支事务表中,表示执行了try阶段,当cancel执行时,先判断表中是否有该全局事务ID的数据,如果有则回滚,否则不做任何操作。比如seata的AT模式中就有分支事务表。
2.幂等问题
由于服务宕机或者网络问题,方法的调用可能出现超时,为了保证事务正常执行我们往往会加入重试的机制,因此就需要保证confirm和cancel阶段操作的幂等性。
我们可以在分支事务记录表中增加事务执行状态,每次执行confirm和cancel方法时都查询该事务的执行状态,以此判断事务的幂等性。
3.悬挂问题
TCC中,在调用try之前会先注册分支事务,注册分支事务之后,调用出现超时,此时try请求还未到达对应的服务,因为调用超时了,所以会执行cancel调用,此时cancel已经执行完了,然而这个时候try请求到达了,这个时候执行了try之后就没有后续的操作了,就会导致资源挂起,无法释放。
执行try方法时我们可以判断confirm或者cancel方法是否执行,如果执行了那么就不执行try阶段。同样借助分支事务表中事务的执行状态。如果已经执行了confirm或者cancel那么try就执行。