目 录CONTENT

文章目录

柔性事务

Administrator
2024-11-25 / 0 评论 / 0 点赞 / 7 阅读 / 0 字
温馨提示:
本文最后更新于2024-11-25,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

image-edce.png

柔性事务概念

柔性事务主要分为补偿型通知型

补偿型事务又分:TCC、Saga;

通知型事务分:MQ事务消息、最大努力通知型。

柔性事务有两个特性:基本可用和柔性状态。

  • 基本可用是指分布式系统出现故障的时候允许损失一部分的可用性。

  • 柔性状态是指允许系统存在中间状态,这个中间状态不会影响系统整体的可用性,比如数据库读写分离的主从同步延迟等。柔性事务的一致性指的是最终一致性。

通知型事务

通知型事务的主流实现是通过MQ(消息队列)来通知其他事务参与者自己事务的执行状态,引入MQ组件,有效的将事务参与者进行解耦,各参与者都可以异步执行,所以通知型事务又被称为异步事务

普通消息是无法解决本地事务执行和消息发送的一致性问题的。因为消息发送是一个网络通信的过程,发送消息的过程就有可能出现发送失败、或者超时的情况。超时有可能发送成功了,有可能发送失败了,消息的发送方是无法确定的,所以此时消息发送方无论是提交事务还是回滚事务,都有可能不一致性出现。

所以,通知型事务的难度在于: 投递消息和参与者本地事务的一致性保障

通知型事务主要适用于那些需要异步更新数据,并且对数据的实时性要求较低的场景,主要包含:异步确保型事务最大努力通知事务两种。

  • 异步确保型事务:主要适用于内部系统的数据最终一致性保障,因为内部相对比较可控,如订单和购物车、收货与清算、支付与结算等等场景;

  • 最大努力通知:主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商、支付对接等等跨网络系统级别对接;

异步确保型事务

指将一系列同步的事务操作修改为基于消息队列异步执行的操作,来避免分布式事务中同步阻塞带来的数据操作性能的下降。

MQ事务消息方案

基于MQ的事务消息方案主要依靠MQ的半消息机制来实现投递消息和参与者自身本地事务的一致性保障。半消息机制实现原理其实借鉴的2PC的思路,是二阶段提交的广义拓展。

  • 事务发起方首先发送半消息到MQ;

  • MQ通知发送方消息发送成功;

  • 在发送半消息成功后执行本地事务;

  • 根据本地事务执行结果返回commit或者是rollback;

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

  • 订阅方根据消息执行本地事务;

  • 订阅方执行本地事务成功后再从MQ中将该消息标记为已消费;

  • 如果执行本地事务过程中,执行端挂掉,或者超时,MQ服务器端将不停的询问producer来获取事务状态;

  • Consumer端的消费成功机制有MQ保证;

本地消息表方案

有时候我们目前的MQ组件并不支持事务消息,或者我们想尽量少的侵入业务方。这时我们需要另外一种方案“基于DB本地消息表“。

本地消息表最初由eBay 提出来解决分布式事务的问题。是目前业界使用的比较多的方案之一,它的核心思想就是将分布式事务拆分成本地事务进行处理。

发送消息方:

  • 需要有一个消息表,记录着消息状态相关信息。

  • 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。

  • 在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录

  • 消息会发到消息消费方,如果发送失败,即进行重试。

消息消费方:

  • 处理消息队列中的消息,完成自己的业务逻辑。

  • 如果本地事务处理成功,则表明已经处理成功了。

  • 如果本地事务处理失败,那么就会重试执行。

  • 如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

本地消息表优缺点:

优点:

  • 本地消息表建设成本比较低,实现了可靠消息的传递确保了分布式事务的最终一致性。

  • 无需提供回查方法,进一步减少的业务的侵入。

  • 在某些场景下,还可以进一步利用注解等形式进行解耦,有可能实现无业务代码侵入式的实现。

缺点:

  • 本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。

  • 本地消息表是基于数据库来做的,而数据库是要读写磁盘IO的,因此在高并发下是有性能瓶颈的

MQ事务消息 VS 本地消息表

二者的共性: 1、 事务消息都依赖MQ进行事务通知,所以都是异步的。 2、 事务消息在投递方都是存在重复投递的可能,需要有配套的机制去降低重复投递率,实现更友好的消息投递去重。 3、 事务消息的消费方,因为投递重复的无法避免,因此需要进行消费去重设计或者服务幂等设计。

二者的区别:

MQ事务消息:

  • 需要MQ支持半消息机制或者类似特性,在重复投递上具有比较好的去重处理;

  • 具有比较大的业务侵入性,需要业务方进行改造,提供对应的本地操作成功的回查功能;

DB本地消息表:

  • 使用了数据库来存储事务消息,降低了对MQ的要求,但是增加了存储成本;

  • 事务消息使用了异步投递,增大了消息重复投递的可能性;

最大努力通知

最大努力通知事务主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商、支付对接、商户通知等等跨平台、跨企业的系统间业务交互场景

异步确保型事务主要适用于内部系统的数据最终一致性保障,因为内部相对比较可控,比如订单和购物车、收货与清算、支付与结算等等场景。

MQ事务消息方案

要实现最大努力通知,可以采用 MQ 的 ACK 机制。

最大努力通知事务在投递之前,跟异步确保型流程都差不多,关键在于投递后的处理。

因为异步确保型在于内部的事务处理,所以MQ和系统是直连并且无需严格的权限、安全等方面的思路设计。最大努力通知事务在于第三方系统的对接,所以最大努力通知事务有几个特性:

  • 业务主动方在完成业务处理后,向业务被动方(第三方系统)发送通知消息,允许存在消息丢失。

  • 业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。

  • 业务被动方提供幂等的服务接口,防止通知重复消费。

  • 业务主动方需要有定期校验机制,对业务数据进行兜底;防止业务被动方无法履行责任时进行业务回滚,确保数据最终一致性。

  1. 业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失。

  2. 主动方可以设置时间阶梯型通知规则,在通知失败后按规则重复通知,直到通知N次后不再通知。

  3. 主动方提供校对查询接口给被动方按需校对查询,用于恢复丢失的业务消息。

  4. 业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务。

  5. 如果被动方没有正常接收,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息。

特点

  1. 用到的服务模式:可查询操作、幂等操作;

  2. 被动方的处理结果不影响主动方的处理结果;

  3. 适用于对业务最终一致性的时间敏感度低的系统;

  4. 适合跨企业的系统间的操作,或者企业内部比较独立的系统间的操作,比如银行通知、商户通知等;

本地消息表方案

要实现最大努力通知,可以采用 定期检查本地消息表的机制 。

发送消息方:

  • 需要有一个消息表,记录着消息状态相关信息。

  • 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。

  • 在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录

  • 消息会发到消息消费方,如果发送失败,即进行重试。

  • 生产方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

最大努力通知事务在于第三方系统的对接,所以最大努力通知事务有几个特性:

  • 业务主动方在完成业务处理后,向业务被动方(第三方系统)发送通知消息,允许存在消息丢失。

  • 业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。

  • 业务被动方提供幂等的服务接口,防止通知重复消费。

  • 业务主动方需要有定期校验机制,对业务数据进行兜底;防止业务被动方无法履行责任时进行业务回滚,确保数据最终一致性。

最大努力通知事务 VS 异步确保型事务

最大努力通知事务,其实是基于异步确保型事务发展而来适用于外部对接的一种业务实现。他们主要有的是业务差别,如下: • 从参与者来说:最大努力通知事务适用于跨平台、跨企业的系统间业务交互;异步确保型事务更适用于同网络体系的内部服务交付。 • 从消息层面说:最大努力通知事务需要主动推送并提供多档次时间的重试机制来保证数据的通知;而异步确保型事务只需要消息消费者主动去消费。 • 从数据层面说:最大努力通知事务还需额外的定期校验机制对数据进行兜底,保证数据的最终一致性;而异步确保型事务只需保证消息的可靠投递即可,自身无需对数据进行兜底处理。

补偿型事务

但是基于消息实现的事务并不能解决所有的业务场景,例如以下场景:某笔订单完成时,同时扣掉用户的现金。

这里事务发起方是管理订单库的服务,但对整个事务是否提交并不能只由订单服务决定,因为还要确保用户有足够的钱,才能完成这笔交易,而这个信息在管理现金的服务里。这里我们可以引入基于补偿实现的事务,其流程如下:

  • 创建订单数据,但暂不提交本地事务

  • 订单服务发送远程调用到现金服务,以扣除对应的金额

  • 上述步骤成功后提交订单库的事务

以上这个是正常成功的流程,异常流程需要回滚的话,将额外发送远程调用到现金服务以加上之前扣掉的金额。

以上流程比基于消息队列实现的事务的流程要复杂,同时开发的工作量也更多:

  • 编写订单服务里创建订单的逻辑

  • 编写现金服务里扣钱的逻辑

  • 编写现金服务里补偿返还的逻辑

可以看到,该事务流程相对于基于消息实现的分布式事务更为复杂,需要额外开发相关的业务回滚方法,也失去了服务间流量削峰填谷的功能。但其仅仅只比基于消息的事务复杂多一点,若不能使用基于消息队列的最终一致性事务,那么可以优先考虑使用基于补偿的事务形态。

什么是补偿模式?

补偿模式使用一个额外的协调服务来协调各个需要保证一致性的业务服务,协调服务按顺序调用各个业务微服务,如果某个业务服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的业务服务。

补偿模式大致有TCC,和Saga两种细分的方案

TCC 事务模型

TCC 分布式事务模型包括三部分:

1.主业务服务:主业务服务为整个业务活动的发起方,服务的编排者,负责发起并完成整个业务活动。

2.从业务服务:从业务服务是整个业务活动的参与方,负责提供 TCC 业务操作,实现初步操作(Try)、确认操作(Confirm)、取消操作(Cancel)三个接口,供主业务服务调用。

3.业务活动管理器:业务活动管理器管理控制整个业务活动,包括记录维护 TCC 全局事务的事务状态和每个从业务服务的子事务状态,并在业务活动提交时调用所有从业务服务的 Confirm 操作,在业务活动取消时调用所有从业务服务的 Cancel 操作。

TCC的工作流程

TCC(Try-Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖资源管理器(RM)对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务

TCC 模型认为对于业务系统中一个特定的业务逻辑,其对外提供服务时,必须接受一些不确定性,即对业务逻辑初步操作的调用仅是一个临时性操作,调用它的主业务服务保留了后续的取消权。如果主业务服务认为全局事务应该回滚,它会要求取消之前的临时性操作,这就对应从业务服务的取消操作。而当主业务服务认为全局事务应该提交时,它会放弃之前临时性操作的取消权,这对应从业务服务的确认操作。每一个初步操作,最终都会被确认或取消。

因此,针对一个具体的业务服务,TCC 分布式事务模型需要业务系统提供三段业务逻辑: 初步操作 Try:完成所有业务检查,预留必须的业务资源。

确认操作 Confirm:真正执行的业务逻辑,不作任何业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务有且只能成功一次。

取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。

TCC 分布式事务模型包括三部分:

Try 阶段: 调用 Try 接口,尝试执行业务,完成所有业务检查,预留业务资源。

Confirm 或 Cancel 阶段: 两者是互斥的,只能进入其中一个,并且都满足幂等性,允许失败重试。

Confirm 操作: 对业务系统做确认提交,确认执行业务操作,不做其他业务检查,只使用 Try 阶段预留的业务资源。 Cancel 操作: 在业务执行错误,需要回滚的状态下执行业务取消,释放预留资源。

Try 阶段失败可以 Cancel,如果 Confirm 和 Cancel 阶段失败了怎么办?

TCC 中会添加事务日志,如果 Confirm 或者 Cancel 阶段出错,则会进行重试,所以这两个阶段需要支持幂等;如果重试失败,则需要人工介入进行恢复和处理等。

TCC事务案例

然而基于补偿的事务形态也并非能实现所有的需求,如以下场景:某笔订单完成时,同时扣掉用户的现金,但交易未完成,也未被取消时,不能让客户看到钱变少了。

这时我们可以引入TCC,其流程如下:

  • 订单服务创建订单

  • 订单服务发送远程调用到现金服务,冻结客户的现金

  • 提交订单服务数据

  • 订单服务发送远程调用到现金服务,扣除客户冻结的现金

以上是正常完成的流程,若为异常流程,则需要发送远程调用请求到现金服务,撤销冻结的金额。

以上流程比基于补偿实现的事务的流程要复杂,同时开发的工作量也更多:

  • 订单服务编写创建订单的逻辑

  • 现金服务编写冻结现金的逻辑

  • 现金服务编写扣除现金的逻辑

  • 现金服务编写解冻现金的逻辑

TCC实际上是最为复杂的一种情况,其能处理所有的业务场景,但无论出于性能上的考虑,还是开发复杂度上的考虑,都应该尽量避免该类事务。

TCC事务模型的要求:
  • 可查询操作:服务操作具有全局唯一的标识,操作唯一的确定的时间。

  • 幂等操作:重复调用多次产生的业务结果与调用一次产生的结果相同。一是通过业务操作实现幂等性,二是系统缓存所有请求与处理的结果,最后是检测到重复请求之后,自动返回之前的处理结果。

  • TCC操作:Try阶段,尝试执行业务,完成所有业务的检查,实现一致性;预留必须的业务资源,实现准隔离性。Confirm阶段:真正的去执行业务,不做任何检查,仅适用Try阶段预留的业务资源,Confirm操作还要满足幂等性。Cancel阶段:取消执行业务,释放Try阶段预留的业务资源,Cancel操作要满足幂等性。TCC与2PC(两阶段提交)协议的区别:TCC位于业务服务层而不是资源层,TCC没有单独准备阶段,Try操作兼备资源操作与准备的能力,TCC中Try操作可以灵活的选择业务资源,锁定粒度。TCC的开发成本比2PC高。实际上TCC也属于两阶段操作,但是TCC不等同于2PC操作。

  • 可补偿操作:Do阶段:真正的执行业务处理,业务处理结果外部可见。Compensate阶段:抵消或者部分撤销正向业务操作的业务结果,补偿操作满足幂等性。约束:补偿操作在业务上可行,由于业务执行结果未隔离或者补偿不完整带来的风险与成本可控。实际上,TCC的Confirm和Cancel操作可以看做是补偿操作。

TCC事务模型 VS DTP事务模型

DTP模型,全称X/Open Distributed Transaction Processing Reference Model,是一个由X/Open组织(现称为The Open Group)定义的分布式事务处理规范。这个模型主要使用了两阶段提交(2PC - Two-Phase-Commit)来保证分布式事务的完整性。DTP模型在分布式系统中非常重要,因为它允许跨越多个网络主机的事务处理。

在DTP模型中,有三个核心角色:

  1. AP(Application Program):应用程序,定义事务的边界,并在事务边界内对资源进行操作。

  2. TM(Transaction Manager):事务管理器,负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。

  3. RM(Resource Manager):资源管理器,通常是指数据库管理系统,负责管理资源(如数据库记录)并提供访问资源的方式。

DTP模型的执行流程大致如下:

  1. AP通过TM向TM注册一个全局事务,并告知TM需要操作哪些资源。

  2. TM通过XA接口通知每个管理对应资源域的RM开启一个子事务。

  3. AP通过RM对资源进行操作,并根据执行结果通知TM提交或回滚全局事务。

  4. 如果所有子事务全部执行成功,则TM提交全局事务;否则,TM通知所有RM回滚子事务。

DTP模型通过XA规范定义了TM和RM之间的接口,XA接口是双向的系统接口,在事务管理器和资源管理器之间形成通信桥梁。XA规范主要定义了全局事务管理器和局部资源管理器之间的接口,使得多个资源(如数据库、应用服务器、消息队列等)可以在一个事务中处理,同时跨应用满足ACID属性。

DTP模型的优点在于它提供了一种标准化的方式来处理分布式事务,但同时也存在一些局限性,比如性能问题。由于DTP模型通常采用基于锁的并发控制来保证事务的隔离性,这意味着资源将被锁定直至事务结束,这可能会严重影响系统的并发性能。此外,DTP模型主要适用于单服务、跨资源场景,对于跨服务、跨资源的分布式事务处理可能不够有效。

TCC事务模型(Try-Confirm-Cancel)是一种分布式事务解决方案,它通过将事务的执行过程分为三个阶段来实现分布式事务的原子性和一致性。这种模型不依赖于底层资源管理器(如数据库)对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。以下是TCC事务模型的三个核心阶段:

  1. Try阶段:这是事务的准备阶段,主要完成所有业务的检查和资源的预留。例如,在电商下单场景中,Try阶段会检查库存是否充足,并冻结相应的库存资源。

  2. Confirm阶段:如果Try阶段成功,那么在Confirm阶段会执行真正的业务操作,如真正地扣除库存、扣款等。这个阶段的操作必须满足幂等性,即无论执行多少次,结果都是一样的。

  3. Cancel阶段:如果Try阶段执行成功,但在后续过程中需要回滚事务(比如用户取消订单),则进入Cancel阶段,释放Try阶段预留的所有资源,如解冻库存等。Cancel操作也必须满足幂等性。

TCC模型的优势在于它提供了一种灵活的事务管理方式,允许业务系统根据具体业务逻辑来实现Try、Confirm和Cancel三个阶段的操作。这使得TCC模型能够适应复杂的业务场景,并且可以通过减少资源锁定的时间来提高系统的并发性能。

然而,TCC模型的缺点在于它对业务代码的侵入性较强,需要业务系统开发者实现Try、Confirm和Cancel三个接口,这增加了开发的复杂度和维护成本。此外,为了保证事务的最终一致性,开发者需要确保Confirm和Cancel操作的幂等性,这可能需要额外的设计和实现工作。

在实际应用中,TCC模型适用于那些对性能要求较高、业务逻辑复杂且需要细粒度控制事务的场景。例如,在互联网金融、电子商务等领域,TCC模型可以帮助实现跨服务、跨数据库的事务一致性

比较一下TCC事务模型和DTP事务模型,如下所示:

这两张图看起来差别较大,实际上很多地方是类似的!

1、TCC模型中的 主业务服务 相当于 DTP模型中的AP,TCC模型中的从业务服务 相当于 DTP模型中的RM

  • 在DTP模型中,应用AP操作多个资源管理器RM上的资源;而在TCC模型中,是主业务服务操作多个从业务服务上的资源。例如航班预定案例中,美团App就是主业务服务,而川航和东航就是从业务服务,主业务服务需要使用从业务服务上的机票资源。不同的是DTP模型中的资源提供者是类似于Mysql这种关系型数据库,而TCC模型中资源的提供者是其他业务服务。

2、TCC模型中,从业务服务提供的try、confirm、cancel接口 相当于 DTP模型中RM提供的prepare、commit、rollback接口

  • XA协议中规定了DTP模型中定RM需要提供prepare、commit、rollback接口给TM调用,以实现两阶段提交。

  • 而在TCC模型中,从业务服务相当于RM,提供了类似的try、confirm、cancel接口。

3、事务管理器

  • DTP模型和TCC模型中都有一个事务管理器。不同的是:

  • 在DTP模型中,阶段1的(prepare)和阶段2的(commit、rollback),都是由TM进行调用的。

  • 在TCC模型中,阶段1的try接口是主业务服务调用(绿色箭头),阶段2的(confirm、cancel接口)是事务管理器TM调用(红色箭头)。这就是 TCC 分布式事务模型的二阶段异步化功能,从业务服务的第一阶段执行成功,主业务服务就可以提交完成,然后再由事务管理器框架异步的执行各从业务服务的第二阶段。这里牺牲了一定的隔离性和一致性的,但是提高了长事务的可用性。

TCC与2PC对比

TCC其实本质和2PC是差不多的:
  • T就是Try,两个C分别是Confirm和Cancel。

  • Try就是尝试,请求链路中每个参与者依次执行Try逻辑,如果都成功,就再执行Confirm逻辑,如果有失败,就执行Cancel逻辑。

TCC与XA两阶段提交有着异曲同工之妙,下图列出了二者之间的对比

  1. 在阶段1:

  • 在XA中,各个RM准备提交各自的事务分支,事实上就是准备提交资源的更新操作(insert、delete、update等);

  • 而在TCC中,是主业务活动请求(try)各个从业务服务预留资源。

    1. 在阶段2:

  • XA根据第一阶段每个RM是否都prepare成功,判断是要提交还是回滚。如果都prepare成功,那么就commit每个事务分支,反之则rollback每个事务分支。

  • TCC中,如果在第一阶段所有业务资源都预留成功,那么confirm各个从业务服务,否则取消(cancel)所有从业务服务的资源预留请求。

TCC和2PC不同的是:
  • XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。基于数据库锁实现,需要数据库支持XA协议,由于在执行事务的全程都需要对相关数据加锁,一般高并发性能会比较差

  • TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁,性能较好。但是对微服务的侵入性强,微服务的每个事务都必须实现try、confirm、cancel等3个方法,开发成本高,今后维护改造的成本也高为了达到事务的一致性要求,try、confirm、cancel接口必须实现幂等性操作由于事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个TCC事务时间拉长

TCC它会弱化每个步骤中对于资源的锁定,以达到一个能承受高并发的目的(基于最终一致性)。

TCC 的使用场景

TCC是可以解决部分场景下的分布式事务的,但是,它的一个问题在于,需要每个参与者都分别实现Try,Confirm和Cancel接口及逻辑,这对于业务的侵入性是巨大的。

TCC 方案严重依赖回滚和补偿代码,最终的结果是:回滚代码逻辑复杂,业务代码很难维护。所以,TCC 方案的使用场景较少,但是也有使用的场景。

比如说跟钱打交道的,支付、交易相关的场景,大家会用 TCC方案,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。

SAGA长事务模型

SAGA可以看做一个异步的、利用队列实现的补偿事务。

Saga模型是把一个分布式事务拆分为多个本地事务,每个本地事务都有相应的执行模块和补偿模块(对应TCC中的Confirm和Cancel),当Saga事务中任意一个本地事务出错时,可以通过调用相关的补偿方法恢复之前的事务,达到事务最终一致性。

这样的SAGA事务模型,是牺牲了一定的隔离性和一致性的,但是提高了long-running事务的可用性。

Saga 模型由三部分组成:

  • LLT(Long Live Transaction):由一个个本地事务组成的事务链。

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

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

Saga的执行顺序有两种:

  • T1, T2, T3, ..., Tn

  • T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n

Saga 两种恢复策略:

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

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

显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务(最终)总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。

理论上补偿事务永不失败,然而,在分布式世界中,服务器可能会宕机,网络可能会失败,甚至数据中心也可能会停电。在这种情况下我们能做些什么? 最后的手段是提供回退措施,比如人工干预。

Saga的使用条件

Saga看起来很有希望满足我们的需求。所有长活事务都可以这样做吗?这里有一些限制:

  1. Saga只允许两个层次的嵌套,顶级的Saga和简单子事务

  2. 在外层,全原子性不能得到满足。也就是说,sagas可能会看到其他sagas的部分结果

  3. 每个子事务应该是独立的原子行为

  4. 在我们的业务场景下,各个业务环境(如:航班预订、租车、酒店预订和付款)是自然独立的行为,而且每个事务都可以用对应服务的数据库保证原子操作。

补偿也有需考虑的事项:

  • 补偿事务从语义角度撤消了事务Ti的行为,但未必能将数据库返回到执行Ti时的状态。(例如,如果事务触发导弹发射, 则可能无法撤消此操作)

但这对我们的业务来说不是问题。其实难以撤消的行为也有可能被补偿。例如,发送电邮的事务可以通过发送解释问题的另一封电邮来补偿。

对于ACID的保证:

Saga对于ACID的保证和TCC一样:

  • 原子性(Atomicity):正常情况下保证。

  • 一致性(Consistency),在某个时间点,会出现A库和B库的数据违反一致性要求的情况,但是最终是一致的。

  • 隔离性(Isolation),在某个时间点,A事务能够读到B事务部分提交的结果。

  • 持久性(Durability),和本地事务一样,只要commit则数据被持久。

SAGA模型的解决方案

SAGA模型的核心思想是,通过某种方案,将分布式事务转化为本地事务,从而降低问题的复杂性。

比如以DB和MQ的场景为例,业务逻辑如下:

  1. 向DB中插入一条数据。

  2. 向MQ中发送一条消息。

由于上述逻辑中,对应了两种存储端,即DB和MQ,所以,简单的通过本地事务是无法解决的。那么,依照SAGA模型,可以有两种解决方案。

方案一:半消息模式。

RocketMQ新版本中,就支持了这种模式。

首先,我们理解什么是半消息。简单来说,就是在消息上加了一个状态。当发送者第一次将消息放入MQ后,该消息为待确认状态。该状态下,该消息是不能被消费者消费的。发送者必须二次和MQ进行交互,将消息从待确认状态变更为确认状态后,消息才能被消费者消费。待确认状态的消息,就称之为半消息。

半消息的完整事务逻辑如下:

  1. 向MQ发送半消息。

  2. 向DB插入数据。

  3. 向MQ发送确认消息。

我们发现,通过半消息的形式,将DB的操作夹在了两个MQ操作的中间。假设,第2步失败了,那么,MQ中的消息就会一直是半消息状态,也就不会被消费者消费。

那么,半消息就一直存在于MQ中吗?或者是说如果第3步失败了呢?

为了解决上面的问题,MQ引入了一个扫描的机制。即MQ会每隔一段时间,对所有的半消息进行扫描,并就扫描到的存在时间过长的半消息,向发送者进行询问,询问如果得到确认回复,则将消息改为确认状态,如得到失败回复,则将消息删除。

半消息如上,半消息机制的一个问题是:要求业务方提供查询消息状态接口,对业务方依然有较大的侵入性。

方案二:本地消息表

在DB中,新增一个消息表,用于存放消息。如下:

  1. 在DB业务表中插入数据。

  2. 在DB消息表中插入数据。

  3. 异步将消息表中的消息发送到MQ,收到ack后,删除消息表中的消息。

本地消息表如上,通过上述逻辑,将一个分布式的事务,拆分成两大步。第1和第2,构成了一个本地的事务,从而解决了分布式事务的问题。

这种解决方案,不需要业务端提供消息查询接口,只需要稍微修改业务逻辑,侵入性是最小的。

SAGA的案例

SAGA适用于无需马上返回业务发起方最终状态的场景,例如:你的请求已提交,请稍后查询或留意通知 之类。

将上述补偿事务的场景用SAGA改写,其流程如下:

  • 订单服务创建最终状态未知的订单记录,并提交事务

  • 现金服务扣除所需的金额,并提交事务

  • 订单服务更新订单状态为成功,并提交事务

以上为成功的流程,若现金服务扣除金额失败,那么,最后一步订单服务将会更新订单状态为失败。

其业务编码工作量比补偿事务多一点,包括以下内容:

  • 订单服务创建初始订单的逻辑

  • 订单服务确认订单成功的逻辑

  • 订单服务确认订单失败的逻辑

  • 现金服务扣除现金的逻辑

  • 现金服务补偿返回现金的逻辑

Saga和TCC对比

但其相对于补偿事务形态有性能上的优势,所有的本地子事务执行过程中,都无需等待其调用的子事务执行,减少了加锁的时间,这在事务流程较多较长的业务中性能优势更为明显。同时,其利用队列进行进行通讯,具有削峰填谷的作用。

Saga相比TCC的缺点是缺少预留动作,导致补偿动作的实现比较麻烦:Ti就是commit,比如一个业务是发送邮件,在TCC模式下,先保存草稿(Try)再发送(Confirm),撤销的话直接删除草稿(Cancel)就行了。而Saga则就直接发送邮件了(Ti),如果要撤销则得再发送一份邮件说明撤销(Ci),实现起来有一些麻烦。

如果把上面的发邮件的例子换成:A服务在完成Ti后立即发送Event到ESB(企业服务总线,可以认为是一个消息中间件),下游服务监听到这个Event做自己的一些工作然后再发送Event到ESB,如果A服务执行补偿动作Ci,那么整个补偿动作的层级就很深。

不过没有预留动作也可以认为是优点:

  • 有些业务很简单,套用TCC需要修改原来的业务逻辑,而Saga只需要添加一个补偿动作就行了。

  • TCC最少通信次数为2n,而Saga为n(n=sub-transaction的数量)。

  • 有些第三方服务没有Try接口,TCC模式实现起来就比较tricky了,而Saga则很简单。

  • 没有预留动作就意味着不必担心资源释放的问题,异常处理起来也更简单。

Saga和TCC都是补偿型事务,他们的区别为:

劣势:

  • 无法保证隔离性;

优势:

  • 一阶段提交本地事务,无锁,高性能;

  • 事件驱动模式,参与者可异步执行,高吞吐;

  • Saga 对业务侵入较小,只需要提供一个逆向操作的Cancel即可;而TCC需要对业务进行全局性的流程改造;

总体的方案对比:

属性

2PC

TCC

Saga

异步确保型事务

尽最大努力通知

事务一致性

复杂性

业务侵入性

使用局限性

性能

维护成本

9:seata

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区