撸明白分布式事务(三)

前言

前面说的二阶段提交协议和三阶段提交协议很好的解决了分布式事务的问题,但是在极端情况下仍然存在数据的不一致性,此外它对系统的开销会比较大,引入事务管理者(协调者)后,比较容易出现单点瓶颈,以及在业务规模不断变大的情况下,系统可伸缩性也会存在问题。注意的是,它是同步操作,因此引入事务后,直到全局事务结束才能释放资源,性能可能是一个很大的问题。因此,在高并发场景下很少使用。因此,阿里提出了另外一种解决方案:TCC 模式。注意的是,很多读者把二阶段提交等同于二阶段提交协议,这个是一个误区,事实上,TCC 模式也是一种二阶段提交。

TCC 模式

TCC 模式将一个任务拆分三个操作:Try、Confirm、Cancel。假如,我们有一个 func() 方法,那么在 TCC 模式中,它就变成了 tryFunc()、confirmFunc()、cancelFunc() 三个方法。

tryFunc();
confirmFunc();
cancelFunc();

在 TCC 模式中,主业务服务负责发起流程,而从业务服务提供 TCC 模式的 Try、Confirm、Cancel 三个操作。其中,还有一个事务管理器的角色负责控制事务的一致性。例如,我们现在有三个业务服务:交易服务,库存服务,支付服务。用户选商品,下订单,紧接着选择支付方式进行付款,然后这笔请求,交易服务会先调用库存服务扣库存,然后交易服务再调用支付服务进行相关的支付操作,然后支付服务会请求第三方支付平台创建交易并扣款,这里,交易服务就是主业务服务,而库存服务和支付服务是从业务服务。

 我们再来梳理下,TCC 模式的流程。第一阶段主业务服务调用全部的从业务服务的 Try 操作,并且事务管理器记录操作日志。第二阶段,当全部从业务服务都成功时,再执行 Confirm 操作,否则会执行 Cancel 逆操作进行回滚。

现在,我们针对 TCC 模式说说大致业务上的实现思路。首先,交易服务(主业务服务)会向事务管理器注册并启动事务。其实,事务管理器是一个概念上的全局事务管理机制,可以是一个内嵌于主业务服务的业务逻辑,或者抽离出的一个 TCC 框架。事实上,它会生成全局事务 ID 用于记录整个事务链路,并且实现了一套嵌套事务的处理逻辑。当主业务服务调用全部的从业务服务的 try 操作,事务管理器利用本地事务记录相关事务日志,这个案例中,它记录了调用库存服务的动作记录,以及调用支付服务的动作记录,并将其状态设置成“预提交”状态。这里,调用从业务服务的 Try 操作就是核心的业务代码。那么, Try 操作怎么和它相对应的 Confirm、Cancel 操作绑定呢?其实,我们可以编写配置文件建立绑定关系,或者通过 Spring 的注解添加 confirm 和 cancel 两个参数也是不错的选择。当全部从业务服务都成功时,由事务管理器通过 TCC 事务上下文切面执行 Confirm 操作,将其状态设置成“成功”状态,否则执行 Cancel 操作将其状态设置成“预提交”状态,然后进行重试。因此,TCC 模式通过补偿的方式保证其最终一致性。

TCC 的实现框架有很多成熟的开源项目,例如 tcc-transaction 框架。(关于 tcc-transaction 框架的细节,可以阅读:https://github.com/changmingxie/tcc-transaction)tcc-transaction 框架主要涉及 tcc-transaction-core、tcc-transaction-api、tcc-transaction-spring 三个模块。其中,tcc-transaction-core 是 tcc-transaction 的底层实现,tcc-transaction-api 是 tcc-transaction 使用的 API,tcc-transaction-spring 是 tcc-transaction 的 Spring 支持。 tcc-transaction 将每个业务操作抽象成事务参与者,每个事务可以包含多个参与者。参与者需要声明 try / confirm / cancel 三个类型的方法。这里,我们通过 @Compensable 注解标记在 try 方法上,并定义相应的 confirm / cancel 方法。

// try 方法
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", 
transactionContextEditor = MethodTransactionContextEditor.class)
@Transactional
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}
 
// confirm 方法
@Transactional
public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}
 
// cancel 方法
@Transactional
public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}

对于 tcc-transaction 框架的实现,我们来了解一些核心思路。tcc-transaction 框架通过 @Compensable 切面进行拦截,可以透明化对参与者 confirm / cancel 方法调用,从而实现 TCC 模式。这里,tcc-transaction 有两个拦截器,请参见图 6-10。

  • org.mengyun.tcctransaction.interceptor.CompensableTransactionInterceptor,可补偿事务拦截器。
  • org.mengyun.tcctransaction.interceptor.ResourceCoordinatorInterceptor,资源协调者拦截器。

这里,需要特别关注 TransactionContext 事务上下文,因为我们需要远程调用服务的参与者时通过参数的形式传递事务给远程参与者。在 tcc-transaction 中,一个事务org.mengyun.tcctransaction.Transaction可以有多个参与者org.mengyun.tcctransaction.Participant 参与业务活动。其中,事务编号 TransactionXid 用于唯一标识一个事务,它使用 UUID 算法生成,保证唯一性。当参与者进行远程调用时,远程的分支事务的事务编号等于该参与者的事务编号。通过事务编号的关联 TCC confirm / cancel 方法,使用参与者的事务编号和远程的分支事务进行关联,从而实现事务的提交和回滚。事务状态 TransactionStatus 包含 : 尝试中状态 TRYING(1)、确认中状态 CONFIRMING(2)、取消中状态 CANCELLING(3)。此外,事务类型 TransactionType 包含 : 根事务 ROOT(1)、分支事务 BRANCH(2)。当调用 TransactionManager#begin() 发起根事务时,类型为 MethodType.ROOT,并且事务 try 方法被调用。调用 TransactionManager#propagationNewBegin() 方法,传播发起分支事务。该方法在调用方法类型为 MethodType.PROVIDER 并且 事务 try 方法被调用。调用 TransactionManager#commit() 方法提交事务。该方法在事务处于 confirm / cancel 方法被调用。类似地,调用 TransactionManager#rollback() 方法,取消事务。

此外,对于事务恢复机制,tcc-transaction 框架基于 Quartz 实现调度,按照一定频率对事务进行重试,直到事务完成或超过最大重试次数。如果单个事务超过最大重试次数时,tcc-transaction 框架不再重试,此时需要手工介入解决。

这里,我们要特别注意操作的幂等性。幂等机制的核心是保证资源唯一性,例如重复提交或服务端的多次重试只会产生一份结果。支付场景、退款场景,涉及金钱的交易不能出现多次扣款等问题。事实上,查询接口用于获取资源,因为它只是查询数据而不会影响到资源的变化,因此不管调用多少次接口,资源都不会改变,所以是它是幂等的。而新增接口是非幂等的,因为调用接口多次,它都将会产生资源的变化。因此,我们需要在出现重复提交时进行幂等处理。那么,如何保证幂等机制呢?事实上,我们有很多实现方案。其中,一种方案就是常见的创建唯一索引。在数据库中针对我们需要约束的资源字段创建唯一索引,可以防止插入重复的数据。但是,遇到分库分表的情况是,唯一索引也就不那么好使了,此时,我们可以先查询一次数据库,然后判断是否约束的资源字段存在重复,没有的重复时再进行插入操作。注意的是,为了避免并发场景,我们可以通过锁机制,例如悲观锁与乐观锁保证数据的唯一性。这里,分布式锁是一种经常使用的方案,它通常情况下是一种悲观锁的实现。但是,很多人经常把悲观锁、乐观锁、分布式锁当作幂等机制的解决方案,这个是不正确的。除此之外,我们还可以引入状态机,通过状态机进行状态的约束以及状态跳转,确保同一个业务的流程化执行,从而实现数据幂等。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章