暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

分布式事务算法Saga和TCC介绍

牧羊人的方向 2022-06-27
2454

前文介绍了分布式一致性算法,在分布式架构下很多时候是无法满足ACID,因此需要在应用开发上对分布式事务设计不同的实现方案,最终达到数据的一致性。本文主要介绍了几种常用的分布式事务算法,Saga、TCC、本地消息表和一致性消息。


1、分布式事务介绍

随着业务的快速发展,单体的应用和单体的数据库已无法满足需求,越来越多的应用会使用分库技术和微服务架构来替代传统的技术架构。随之而来的是分布式架构带来的挑战,传统的数据库事务已无法处理跨库和跨应用的数据库事务,因此需要对各应用库和各应用进行事务的协调管理,最终达到数据的一致性。分布式事务的架构模型很多,常用的是开源分布式事务解决方案Seata,其中分布式事务有三种不同的角色,负责不同的事务工作:
  1. TC (Transaction Coordinator):事务协调者,负责维护全局和分支事务的状态,协调事务处理,驱动全局事务提交或回滚。

  2. TM (Transaction Manager) :事务管理器,负责定义全局事务的范围,通过与TC通讯交互,发起全局事务的注册、提交或回滚请求

  3. RM (Resource Manager) :资源管理器,负责管理分支事务处理的资源,通过与TC通讯交互进行分支事务注册、汇报分支事务的状态;同时接受TC的协调事件,驱动分支事务提交或回滚。

分布式事务算法的实现有很多种,下面主要介绍下Saga、TCC、本地事务表和一致性消息等。

2、分布式事务算法

2.1 Saga模式

Saga模式属于长事务解决方案,其核心思想把一个分布式事务拆分为多个本地事务,每个本地事务都有相应的执行模块和补偿模块,当Saga事务中任意一个本地事务出错时,可以通过调用相关的补偿方法恢复之前的事务,达到事务最终一致性。

Saga模式由三部分组成:
  1. LLT(Long Live Transaction):由一个个本地事务组成的事务链。

  2. 本地事务:事务链由一个个子事务(本地事务)组成,LLT = T1+T2+T3+...+Ti。

  3. 补偿:每个本地事务 Ti 有对应的补偿 Ci。

在业务流程中,正常情况下每个参与者都在一阶段提交本地事务,按照T1->T2->T3->…->Ti的顺序执行。当出现异常时,则会发起补偿,将之前提交的事务回滚,执行顺序变成T1->T2->T3->C3->C2->C1。如下图所示:

Saga模式的恢复其实有两种:向后恢复(Backward Recovery)和向前恢复(Forward Recovery)
  • 向后恢复(Backward Recovery):撤销掉之前所有成功子事务。如果任意本地子事务失败,则补偿已完成的事务。如异常情况的执行顺序T1,T2,T3,..Ti,Ci,...C3,C2,C1。

  • 向前恢复(Forward Recovery):即重试失败的事务,适用于必须要成功的场景,该情况下不需要Ci。执行顺序:T1,T2,...,Tj(失败),Tj(重试),...,Ti。

Saga模式满足事务的ACD三个特性:
  • 原子性:Saga协调器协调事务链中的本地事务要么全部提交,要么全部回滚

  • 一致性:Saga事务可以实现最终一致性

  • 持久性:基于本地事务,所以这个特性可以很好实现

但是缺乏隔离性会引发脏读、幻读和不可重复读等问题,因此需要在业务设计上去解决这个问题,通常在应用层面通过逻辑锁或者串行化操作来确保读取数据的准确性。

实现Saga的注意事项:

(1) TiCi必须是幂等的。如向后恢复和向前恢复时候如果不是幂等操作会导致数据不一致。

(2) Ci必须是能够成功的,如果无法成功则需要人工介入。

(3) Ti->CiCi->Ti的执行结果必须是一样的。


复制
2.2 TCC模式
TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的论文《Life beyond Distributed Transactions:an Apostate’s Opinion》中提出。TCC采用补偿机制,其核心思想是针对每个操作,都要注册一个与其对应的确认和补偿,通过对资源的预留尽早释放对资源的加锁,提交则完成资源的确认,回滚则释放预留资源。TCC要求应用的每个服务提供 try、confirm、cancel三个接口,这些完全由业务实现,对业务侵入较大。
  • Try阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)

  • Confirm阶段:确认执行业务,不作任何业务检查,只使用Try阶段预留的业务资源Confirm操作要求具备幂等设计,Confirm失败后需要进行重试。

  • Cancel阶段:取消执行,释放Try阶段预留的业务资源。Cancel阶段的异常和Confirm阶段异常处理方案基本上一致,要求满足幂等设计。

2.2.1 TCC模式处理流程

上图是一个典型的TCC分布式事务模型,包括三部分:

  1. 主业务程序:负责发起并完成整个业务活动,在图中由它向TM注册TCC事务并开启分布式事务,执行业务

  2. 分支服务:整个业务活动的参与方,实现Try、Confirm和Cancel动作,供主业务程序调用。对应图中的微服务A和微服务B,执行Try执行,并根据TM返回的结果执行Confirm或Cancel

  3. 事务管理器:管理整个业务活动,包括记录事务状态,调用分支服务的Confirm/Cancel 操作

TCC模式相较于Saga模式来说应用可以自定义数据操作的粒度,降低了锁冲突,提升业务并发度;但是对应用侵入较强,开发量较大,需要提供Try/Confirm/Cancel接口

2.2.2 TCC模式注意点
1)允许空回滚

空回滚出现的原因是Try超时或者丢包,导致TCC分布式事务二阶段的回滚,触发Cancel操作,此时事务参与者未收到Try,但是却收到了Cancel请求,如下图所示:


所以cancel接口在实现时需要允许空回滚,也就是Cancel执行时如果发现没有对应的事务xid或主键时,需要返回回滚成功,让事务管理器认为已回滚。

2)防悬挂控制

悬挂指的是二阶段的Cancel比一阶段的Try操作先执行,出现该问题的原因是Try由于网络拥堵而超时,导致事务管理器生成回滚,触发Cancel接口,但之后拥堵在网络的Try操作又被资源管理器收到了,但是Cancel比Try先到。但按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功,所以此时应该拒绝执行空回滚之后到来的Try操作,否则会产生数据不一致。因此可以在Cancel空回滚返回成功之前,先记录该条事务xid或业务主键,标识这条记录已经回滚过,Try接口执行前先检查这条事务xid或业务主键是否已经标记为回滚成功,如果是则不执行Try的业务操作。

3)幂等控制

由于网络原因或者重试操作都有可能导致“Try-Confirm-Cancel”3个操作的重复执行,所以使用TCC时需要注意这三个操作的幂等控制,通常可以使用事务xid或业务主键判重来控制。RM对已执行过回滚的事务,不会再次调起回滚的逻辑,RM自动返回回滚成功,保证幂等。

2.3 本地消息表
本地消息表的核心思想是将分布式事务拆分成本地事务进行处理,本质上是一个消息异步处理的过程,其处理流程如下:
  1. 事务发起方在同一个事务里同时写业务表和消息表

  2. 事务发起方本地有个定时任务,定时查询消息表的状态,将未处理的消息通过消息中间件发送到事务消费方进行处理。

  3. 事务消费方读取消息队列中的消息,并写入本地的业务表中

  4. 消息正常处理完成后,事务消费方会通过消息中间件,通知事务发起方消息已处理

  5. 事务发起方在接收到结果后,会更新消息表中的状态为已处理

从上述流程上看,本地消息表方案是基于消息中间件的可靠性来达到事务的最终一致性。在这个过程中,当中间出现一些异常时,这里进一步分析:
  • 当①和②处理出错,由于写业务数据和写消息表仍然在本地事务中,直接回滚即可

  • 当③处理出错,发送消息失败,由于消息数据在本地消息表中有保存,只需要通过轮询任务定时发送到事务消费方,重新读取消息处理业务即可

  • 如果是业务上⑥执行失败,事务消费方需发消息到事务发起方主动回滚事务

  • 如果是事务发起方回滚事务但是消息已经被消费了,则需要通知到事务消费方发起回滚

总之,本地消息表方案整体简单易于实现,从应用设计开发的角度实现了消息数据的可靠性。但是本地消息表有以下缺点:
  • 消息数据和业务数据耦合,占用业务库资源

  • 消息表需要根据具体的业务场景制定,不具备通用性

  • 性能上受限于数据库的性能,高并发场景下会有性能瓶颈

  • 适用于最终一致性的场景

2.4 一致性消息
基于MQ的一致性消息方案本质上是对本地消息表的封装,整体流程与本地消息表一致,不同之处是依靠MQ的半消息机制来实现事务发起方和事务消费方本地事务的一致性。整体流程如下:
  1. 事务发起方首先发送半消息到MQ

  2. MQ Server将消息持久化成功之后,向发送方ack确认消息已经发送成功

  3. 在发送半消息成功后,发送方执行本地事务;

  4. 发送方根据本地事务执行结果向MQ Server提交二次确认,commit或者rollback;

  5. 如果消息是rollback, MQ将丢弃该消息不投递;如果是commit,MQ将会消息发送给消息订阅方;

  6. 事务消费方根据消息执行本地事务;

  7. 消费方执行本地事务成功后再发送执行结果到MQ server将该消息标记为已消费

在断网或者应用异常重启等异常情况下,commit/rollback的结果没有正常的发送到MQ Server,MQ Server对该消息发起消息回查:
  1. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果

  2. 发送方根据检查得到的本地事务的最终状态再次提交二次确认

  3. MQ Server基于commit/rollback对消息进行投递或者删除

一致性MQ消息方案对比本地消息表,将消息数据由MQ进行管理,降低了业务系统和消息系统之间的耦合性,同时使用MQ消息队列后业务的并发高于本地消费表写数据库的方案。但是一致性MQ需要和MQ两次交互,增加了网络开销,同时对业务侵入较大,需要单独开发事务状态的回查接口。

2.5 不同分布式事务算法对比

上述四种分布式事务的方案,在事务一致性、实现复杂性、对业务的侵入性、使用局限性、性能和维护成本等角度,总结如下表:

属性TCCSaga本地消息表一致性消息

事务一致性

复杂性

业务侵入性

使用局限性

性能

维护成本

不同模式有不同的的使用场景,如下所示:
  • Saga模式:由于Saga事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga由于缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。所以,Saga事务较适用于补偿动作容易处理的场景。

  • TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如金融类交易和支付类业务

  • 本地消息表/一致性MQ事务:适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到最终一致性,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。

3、分布式事务场景介绍

上面介绍了几种分布式事务算法和实现方案,接下去以几个场景具体看下分布式事务是如何实现的。

3.1 基于Saga的交易场景


以金融放款业务为例,业务流程上包括生成放款明细、结算账户入账、核算入账处理以及登记金融交易流水。上图是使用Saga模式实现分布式放款交易场景:
  1. 放款交易发起时,TM向分布式事务协调中心申请注册开启全局事务,返回一个全局事务ID,在整个交易处理过程中作为事务的唯一标识

  2. RM向分布式事务协调中心请求注册本地分支事务,作为全局事务的分支,并且和全局事务ID关联

  3. 如果放款交易正常完成,事务管理器会通知分布式事务协调中心进行全局事务提交,更新事务状态并返回,本次全局事务结束

  4. 如果放款过程中出现异常,TM则通知全局事务回滚,分布式事务协调中心将根据全局事务ID将成功注册的分支事务依次进行回滚。当所有分支事务回滚成功后,本次全局事务结束

  5. 如果放款交易过程中出现超时,则分布式事务协调中心自动将全局事务置为回滚状态,使新的分支注册失败,已经成功注册的分支倒序的方式进行回滚

3.2 一致性MQ流程

一致性MQ是通过消息队列异步消费的方式实现分布式事务的最终一致性,适用于可无限重试的业务场景。比如业务活动充值送积分,充值成功后根据充值额度添加相应的积分,在设计上可以先处理实时性高的充值交易,充值成功后通过MQ消息再为客户添加积分。


上图是一致性MQ处理的机制,其实还是本地消息表的方式,主要流程如下:
  1. 业务操作和写入消息在同一个事务中处理同时入库,利用数据库事务保证业务操作与消息内容的原子性

  2. 在发送方利用定时任务轮询扫描消息库中未发送的消息,更新状态为发送中,将消息入列等到发送

  3. 在发送方利用定时任务定时从队列中获取待发送的消息到MQ,发送成功则更新消息状态为成功

  4. 在发送方利用定时任务轮询扫描消息库中状态为发送中超过一定时间的消息,将其置为待发送,等待下次重新发送

  5. 消费方读取MQ中的消息进行消费,直至成功

上述一致性MQ的处理流程在消费端没有将消息的执行情况返回给发送方更新入库,而是采用定时轮询的方式在发送方更新消息库中的发送状态,因此在消费端存在重复消费的情况,需要消费端做好幂等的逻辑处理。

4、分布式事务容错处理

4.1 超时处理机制

全局事务超时处理机制如上图所示:

  • 服务端处理过程

    • 设置定时任务查询状态为开始但是已经超时的全局事务

    • 将超时的事务状态设置为timeout

    • 根据查询到的超时事务想客户端发起全局事务状态回查

    • 根据回查到的结果决定将事务状态设置为提交中或者回滚中

    • 最终由提交重试任务或者回滚重试任务进行二阶段操作的发起

  • 客户端处理过程

    • 客户端接收到回查请求后,根据全局事务对应的请求流水号进行日志的回查

    • 判断Log的状态,若状态为success或cancel则返回全局提交;若状态为fail、check或auth则返回全局回滚;其它状态则返回未知

4.2 提交重试机制

提交重试机制是指全局事务提交时,分支的二阶段提交出现失败,为保证最终一致性,分布式事务协调中心会对失败的分支进行重试,直至提交成功或重试次数耗尽为止。


具体流程如下:
  1. 任务执行器接收到重试的请求,开始任务处理

  2. 查询待重试事务,查询所有状态为提交重试、剩余执行次数大于0且当前时间大于下次执行时间的数据

  3. 遍历待重试提交的事务,执行处理逻辑

  4. 更新剩余重试的次数以及下次重试的时间

  5. 所有事务都处理完成后,任务结束

4.3 回滚重试机制

回滚重试机制是指全局事务回滚时,分支的二阶段回滚出现失败,为保证最终一致性,分布式事务协调中心会对失败的分支进行重试,直至回滚成功或回滚次数耗尽为止。

具体流程如下:

  1. 任务执行器接收到重试的请求,开始任务处理

  2. 查询待重试事务,查询所有状态为回滚重试、剩余执行次数大于0且当前时间大于下次执行时间的数据

  3. 遍历待重试的事务,执行处理逻辑

  4. 更新剩余重试的次数以及下次重试的时间

  5. 所有事务都处理完成后,任务结束

5、总结

本文简要介绍了分布式事务中几种实现模式,包括Saga、TCC、本地事务表和一致性消息,并结合实际应用场景分析了各种模式的使用。在分布式架构下,如何保证和实现分布式事务的一致性,需要在应用设计上更多的考虑以及一定的性能牺牲,比如幂等、防重机制等,对业务开发来说面临更大的挑战。


参考资料:

  1. https://seata.io/zh-cn/

  2. https://blog.csdn.net/wsdc0521/article/details/108223310

  3. https://blog.csdn.net/u012811805/article/details/121313006

  4. https://blog.csdn.net/qq_41824825/article/details/123413868

  5. https://blog.csdn.net/a745233700/article/details/12240230

文章转载自牧羊人的方向,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论