【分布式事务】分布式事务

一、什么是分布式事务

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据存储的数据一致性。

二、分布式事务产生的原因

2.1 数据库分别库分表

当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。

2.2 服务SOA化

所谓的SOA化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。

三、分布式系统基础

从上面来看分布式事务是随着互联网高速发展应运而生的,传统的数据库的ACID四大特性,已经无法满足我们分布式事务,这个时候又有一些大佬提出一些新的理论。

3.1 CAP理论

CAP定理,又被叫作布鲁尔定理,CAP理论指出,分布式系统不可能同时满足一致性(C)、可用性(A)和分区容错性(P),最多同时满足其中二者。

C (一致性):对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致。分布式系统一般都会放弃强一致性,追求最终一致性。

A (可用性):在某个考察时间,系统能够正常运行的概率或时间占有率期望值。业界通常使用几个9来描述系统的可用性,所谓1个9是指90%,2个9是指99%,3个9是指99.9%,依次类推。

P (分区容错性):当出现网络分区后,系统能够继续工作。打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。

在分布式系统中,网络无法100%可靠,分区其实是一个必然现象,如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构。

对于CP来说,放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致。

对于AP来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的BASE也是根据AP来扩展。

3.2 BASE理论

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展。

基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。

软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。

最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

四、分布式事务架构设计哲学

分布式系统可以是AP模型、也可以是CP模型,那么分布式事务到底属于CP还是AP呢?看看如下两个业务场景。

场景一:拼多多购买商品,首先选择商品,然后点击立即购买,最后进行支付:

场景二:朋友圈发送信息、小红点提示:

分布式事务具体是AP模型还是CP模型,取决于业务容忍度。对于场景一,需要有很强的一致性,点击立即购买之后要求立即创建订单,属于CP模型;对于场景二,一致性要求不那么强,但是对吞吐量要求很高,属于AP模型。

另外,通常情况下,对于一个业务场景而言,可能既有同步场景,也有异步场景。还是以拼多多购买商品为例:

同步场景:

  • 商品减库存
  • 建立订单
  • 前台支付

异步场景(前台超时未支付):

  • 删除订单
  • 恢复商品库存

从以上来看,分布式事务要么是AP模型,要么是CP模型;从业务场景来看,要么是同步场景,要么是异步场景。但是AP、CP,同步和异步都可能会结合,因此,我们要寻找分布式事务架构设计与实践的普适方法论,这就是架构设计哲学。对于分布式事务,其普适方法论就是拆分和补偿。

拆分:架构分为服务和数据,所有的架构问题,其核心问题都是耦合性问题,只要是耦合问题,都可以进行拆分。对于分布式事务而言,可以将一个设计多个数据存储的长事务,拆分为多个本地事务的短事务。

补偿:考虑如下服务调用链:A->B->C,若A、B执行成功,C执行失败,那么补偿B、A即可。所谓补偿,就是执行逆向操作,比如征信操作是insert,那么其补偿方法对应delete。

五、分布式事务解决方案

5.1 业务场景

考虑如下业务场景:在拼多多购物时,首先选择商品,点击立即购买,跳转到支付页面,用户可以选择立即支付,也可以暂不支付。如果超时未支付,订单会自动失效。

在这个业务场景中,既有同步场景,也有异步场景。

同步场景:

  • 商品减库存
  • 创建订单
  • 前台支付

异步场景:

  • 前台超时未支付,删除订单、恢复商品库存

对应的流程图如下:

5.2 异步场景下的分布式事务

上述业务场景中,用户点击立即购买,系统扣减商品库存、生成订单并保存到订单库,这是一个同步场景,我们稍后讨论;用户下单的同时会发送一个消息到MQ中,这个消息是延时消息,会在订单超时未支付时删除订单、恢复商品库存,这是异步场景。

对于异步场景,有以下两个步骤:

  • 下订单
  • 发送订单未支付延时消息

这里,如何保证下单成功,并且延时消息一定会发送到MQ呢?

常见做法如下:

try {
  saveOrder(order);
  mqClient.sendMsg(msg);
} catch (Exception e) {
  rollBack(order);
}

这种做法很明显是有问题的,如果发送消息其实已经成功,只是由于网络原因,响应的时候没收到消息,那么就会造成不一致。

再看看下面的做法:

try {
  mqClient.sendMsg(msg);
  saveOrder(order);
} catch (Exception e) {
  rollBack(order);
}

上面的代码首先将消息发送到MQ,若发送成功再将订单数据入库。很明显,也是存在问题的,因为消息发送失败并不一定是真的失败,另外,如果消息发送成功,订单保存失败,那么又如何处理呢?

那么对于异步的分布式事务,应该如何优雅的落地与实践呢?

5.2.1 基于MQ的半消息机制

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

  • 事务发起方首先发送半消息消息到MQ(该消息不能被消费);
  • 在发送半消息成功后执行本地事务;
  • 根据本地事务执行结果执行commit或者是rollback;
  • 如果消息是rollback, MQ将删除该半消息不进行投递,如果是commit消息,MQ将会消息发送给consumer端;
  • 如果执行本地事务过程中,执行端挂掉,或者超时,MQ服务器端将不停的询问producer来获取事务状态;
  • Consumer端的消费成功机制由MQ保证

此方案利用MQ的半消息机制,对业务具有比较大侵入性,有以下注意点:

  • 业务方调用半消息,并提供对应的回查方法;
  • MQ提要提供半消息机制(即需要MQ提供支持,以上流程基于RocketMQ),并定期扫描长期半消息,对消息生产者进行回查确认事务。
  • 消费方需要进行幂等消费。

所以,由于事务消息需要MQ提供支持,并且对业务侵入较大,并不是分布式事务的普适方法论。

5.2.2 本地消息表

有时候我们的MQ组件并不支持事务消息,或者我们想尽量少的侵入业务方。这时我们可以使用基于本地消息表的方案,流程图如下:

  • 业务方:直接利用本地事务,将业务数据和事务消息直接写入数据库;
  • 投递线程:使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录。

本地事务消息表的优势在于方案的普适性,无需提供回查方法,进一步减少业务的侵入。在某些场景下,还可以进一步利用注解等形式进行解耦,实现业务代码零侵入。

思考:

1、如何确保消息不丢失?会有重复消息吗?如有,消息需要去重吗?MQ下游消费服务如何确保事务的成功处理?

生产者Confirm、MQ持久化、消费者手动ACK,允许消息重复,不需要去重,消费者确保幂等

2、分布式系统存在性能瓶颈吗?如存在,性能瓶颈在哪里?如何提升性能?为何阿里这样的大厂也搞不定双11?

网关、业务服务是无状态的,可以水平扩容,不存在性能瓶颈;缓存、数据库、MQ可以做集群、分片等,理论上不存在瓶颈。瓶颈存在于:28法则,即80%的请求都访问了20%的数据,虽然数据库做了分片,但是对于同一个商品而言,对应的只是数据库中的某一条记录,所以,对于该条记录所在的那台机器,其实是有瓶颈的。考虑是进行拆分,把一条商品数据拆分为多条记录(比如可以使用type字段加以区分),存到不同的机器上,然后业务层随机访问某一条记录。至于为何还是会出问题,在于ROI太低,理论上只要增加机器就可以解决,但是ROI太低,老板不干。

5.3 同步场景下的分布式事务

前面我们了解了异步场景下的分布式事务普适方法(本地消息表),实际场景中,我们遇到的大多数是同步场景。在5.1节的业务场景中,用户点击立即购买,后台会扣减商品库存、生成订单并保存到订单库,这就是一个同步场景。针对同步场景下的分布式事务,常见的解决方案有2PC、3PC、TCC、Saga、Seata等。

5.3.1 2PC

2PC全称Two-PhaseCommit,中文名是二阶段提交,是XA规范的实现思路,XA规范是 X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。XA 接口函数由数据库厂商提供。

X/Open DTP是X/Open 组织(即现在的 Open Group )1994定义的分布式事务处理模型。XA模型包括应用程序( AP )、事务管理器( TM )、资源管理器( RM )、通信资源管理器( CRM )四部分。一般常见的事务管理器( TM )是交易中间件,常见的资源管理器( RM )是数据库,常见的通信资源管理器( CRM )是消息中间件。

2PC 通常使用到XA中的三个角色AP、TM和RM。

AP:事务发起方,通常为微服务自身;定义事务边界(事务开始、结束),并访问事务边界内的资源;

TM:事务协调方,事务操作总控;管理全局事务,分配事务唯一标识,监控事务的执行进度,负责事务的提交、回滚、失败恢复。

RM:本地事务资源,根据协调方命令进行操作;管理本地共享资源(既数据库)。

2PC的流程如下:

2PC 分成2个阶段,第一阶段:请求阶段(commit-request phase,或称表决阶段,voting phase)和第二阶段:提交阶段(commit phase)。

表决阶段:事务协调者(TM)串行给每个参与者(RM)发送Prepare消息,每个参与者要么直接返回失败,要么在本地执行SQL、记录事务日志(Undo log、Redo log),但不提交,到达一种“万事俱备,只欠东风”的状态。可以进一步将准备阶段分为以下三个步骤:

  • TM串行向每个参与者节点询问是否可以执行提交操作,并等待各参与者节点的响应;
  • 参与者节点执行所有SQL语句,并将Undo log和Redo log写入日志(尚未commit);
  • 各参与者节点响应TM发起的询问。如果参与者节点的事务操作实际执行成功,则返回一个”success”消息;如果参与者节点的事务操作实际执行失败,则返回一个”abort”消息。

提交阶段:如果TM收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据TM的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)。

分支一:当TM从所有参与者节点获得的相应消息都为”success”时:

  • TM向所有参与者节点发出”正式提交(commit)”的请求;
  • 参与者节点正式完成操作,并释放在整个事务期间内占用的资源;
  • 参与者节点向TM发送”完成”消息;
  • TM收到所有参与者节点反馈的”完成”消息后,完成事务。

分支二:如果任一参与者节点在第一阶段返回的响应消息为”abort”,或者 TM在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

  • TM向所有参与者节点发出”回滚操作(rollback)”的请求;
  • 参与者节点利用之前写入的Undo log信息执行回滚,并释放在整个事务期间内占用的资源;
  • 参与者节点向TM发送”回滚完成”消息;
  • TM收到所有参与者节点反馈的”回滚完成”消息后,取消事务。

不管最后结果如何,第二阶段都会结束当前事务。

2PC虽然将XA规范方案细化成思路,也形成了流程图,大部情况下确实能提供原子性操作,但是仍存在一些问题:

  • 全流程的同步阻塞:不管是第一阶段还是第二阶段,所有参与节点都是事务阻塞型。当参与者占有公共资源时,其他第三方访问公共资源可能不得不处于阻塞状态。
  • TM单点故障:由于全流程依赖TM的协调,一旦TM发生故障。参与者会一直阻塞下去。尤其在第二阶段,TM发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。所有参与者必须等待TM重新上线(TM重新选举)后才能继续工作。
  • TM脑裂引起数据不一致:在第二阶段中,当TM向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中TM发生了故障,导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
  • TM脑裂引起事务状态不确定:TM在发出commit消息之后宕机,而接收到这条消息的参与者同时也宕机了。那么即使通过选举协议产生了新的TM,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

5.3.2 3PC

三阶段提交(Three-phase Commit,简称3PC),是为解决2PC中的缺点而设计的。与两阶段提交不同的是,三阶段提交是“非阻塞”协议。

对应于2PC,3PC有两个改动点:

  • 引入超时机制。同时在协调者和参与者中都引入超时机制(2PC只有TM有超时,在发送Prepare之后,等待超时就会发送rollback;3PC的参与者也有超时机制,等待超时没有收到TM的质量,会自动提交/回滚)。
  • 两阶段提交的第一阶段与第二阶段之间插入了一个准备阶段,使得原先在两阶段提交中,参与者在投票之后,由于协调者发生崩溃或错误而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。

第一阶段CanCommit

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送CanCommit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  • 事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
  • 响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No。

第二阶段PreCommit

协调者根据参与者的响应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。

情况一:假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

  • 发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段。
  • 事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo log和redolog信息记录到事务日志中。
  • 响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

情况二:假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

  • 发送中断请求:协调者向所有参与者发送Abort请求。
  • 中断事务:参与者收到来自协调者的Abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

第三阶段DoCommit

该阶段进行真正的事务提交,也可以分为以下两种情况。

情况一:执行提交。

  • 发送提交请求:协调者接收到参与者发送的ACK响应,那么它将从预提交状态进入到提交状态。并向所有参与者发送DoCommit请求。
  • 事务提交:参与者接收到DoCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
  • 响应反馈:事务提交完之后,向协调者发送ACK响应。
  • 完成事务:协调者接收到所有参与者的ACK响应之后,完成事务。

情况二:中断事务。协调者没有接收到参与者发送的ACK响应(可能是参与者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

  • 发送中断请求:协调者向所有参与者发送Abort请求。
  • 事务回滚:参与者接收到Abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
  • 反馈结果:参与者完成事务回滚之后,向协调者发送ACK消息。
  • 中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

在三阶段提交中,如果在第三阶段协调者发送提交请求之后挂掉,并且唯一接收到的参与者执行提交操作之后也挂掉了,这时协调者通过选举协议产生了新的协调者。在二阶段提交时存在的问题就是新的协调者不确定已经执行过事务的参与者是执行的提交事务还是中断事务。但是在三阶段提交时,肯定得到了第二阶段的再次确认,那么第二阶段必然是已经正确的执行了事务操作,只等待提交事务了。所以新的协调者可以从第二阶段中分析出应该执行的操作,进行提交或者中断事务操作,这样即使挂掉的参与者恢复过来,数据也是一致的。所以,三阶段提交解决了二阶段提交中存在的由于协调者和参与者同时挂掉可能导致的数据一致性问题和单点故障问题,并减少阻塞。因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行提交/回滚事务,而不会一直持有事务资源并处于阻塞状态。

不过3PC也存在自身的问题:在提交阶段如果发送的是中断事务请求,但是由于网络问题,导致部分参与者没有接到请求。那么参与者会在等待超时之后执行提交事务操作,这样这些由于网络问题导致提交事务的参与者的数据就与接收到中断事务请求的参与者存在数据不一致的问题。所以无论是 2PC 还是 3PC 都不能保证分布式系统中的数据 100% 一致。

5.3.3 TCC

TCC概念由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出, 在该论文中,TCC还是以Tentative-Confirmation-Cancellation命名。正式以Try-Confirm-Cancel作为名称的是Atomikos公司,并且还注册了TCC商标。国内最早可查引进TCC概念,应是阿里程立2008年在软件开发2.0大会上分享主题《大规模SOA系统中的分布事务处理》中。

Atomikos公司在商业版本事务管理器ExtremeTransactions中提供了TCC方案的实现,但是由于其是收费的,因此相应的很多的开源实现方案也就涌现出来,如:ByteTCC、Himly、TCC-transaction。但是都不推荐使用,下文会详细说明。

TCC 是一种补偿型事务,该模型要求应用的每个服务提供 try、confirm、cancel 三个接口,它的核心思想是通过对资源的预留(调用try接口,提供中间态,比如对于转账,那么有总金额、可用金额和冻结金额三个字段,try阶段总金额不变,可用金额减少,冻结金额增加),尽早释放对资源的加锁(2PC、3PC是等整个长事务完成后才释放锁),如果事务可以提交,则完成对预留资源的确认(调用confirm接口,confirm阶段可用金额不变,总金额减少,冻结金额减少),如果事务要回滚,则释放预留的资源(调用cancel接口,cancel阶段则是总金额不变,可用金额增加,冻结金额减少)。

TCC模型完全交由业务实现,每个子业务都需要实现Try-Confirm-Cancel三个接口,对业务侵入大,资源锁定交由业务方。

  • Try:尝试执行业务,完成所有业务检查(一致性),预留必要的业务资源(准隔离性,中间状态)。
  • Confirm:确认执行业务,不再做业务检查。只使用Try阶段预留的业务资源,Confirm操作满足幂等性。
  • Cancel:取消执行业务,释放Try阶段预留业务资源。

TCC与2PC对比:

TCC将事务提交划分成两个阶段,Try即为一阶段,Confirm 和 Cancel 是二阶段并行的两个分支,二选一。从阶段划分上非常像2PC,我们是否可以说TCC是一种2PC或者2PC变种呢?其实不可以,原因如下:

  • 2PC的操作对象在于资源层(数据库层面),对于开发人员无感知;而TCC的操作在于业务层,具有较高开发成本。
  • 2PC是一个整体的长事务,是刚性事务;而TCC是一组的本地短事务,是柔性事务。
  • 2PC的Prepare(表决阶段)进行了操作表决;而TCC的try并没有表决准备,直接兼备资源操作与准备能力。
  • 2PC是全局锁定资源,所有参与者阻塞交互等待TM通知;而TCC的资源锁定在于Try操作,业务方可以灵活选择业务资源的锁定粒度。

TCC为了解决网络不可靠引起的异常情况,要求业务方在设计上要遵循三个策略:

  • 允许空回滚。原因是异常发生在阶段一时,部分参与方没有收到 Try 请求从而触发整个事务的Cancel 操作;Try 失败或者没有执行 Try 操作的参与方收到 Cancel 请求时,要进行空回滚操作。
  • 保持幂等性。原因是异常发生在阶段二时,比如网络超时,则会重复调用参与方的 Confirm/Cancel 方法,因此Confirm/Cancel方法必须保证幂等性。
  • 防止资源悬挂。原因是网络异常导致两个阶段无法保证严格的顺序执行,出现参与方侧 Try 请求比 Cancel 请求更晚到达的情况,Cancel 会执行空回滚而确保事务的正确性,但是此时 Try 方法也不可以再被执行。

因为TCC对业务的强侵入性,使用成本非常昂贵,虽然提供了更灵活的资源锁粒度,对标2PC拥有更高的吞吐量。但是相对于2PC的强一致性来说,TCC的实施成本和数据一致性的牺牲带来的相对高吞吐量,总体表现出来的性价比非常低,反而在市面上成熟的大型企业中几乎没有使用。

5.3.4 Saga

Saga模型起源于1987年 Hector Garcia-Molina,Kenneth Salem 发表的论文《Sagas》,是分布式事务相关概念最早出现的。

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

Saga模型主要分:

  • 一串子事务(本地事务)的事务链
  • 每个Saga子事务Tn, 都有对应的补偿定义Cn用于撤销Tn造成的结果
  • 每个Tn都没有“预留”动作,直接提交到库。

Saga执行顺序:

  • 子事务序列 T1, T2, …, Tn得以完成 (最佳情况)
  • 或者序列 T1, T2, …, Tj, Cj-1, …, C2, C1, 0 < j < n, 得以完成

Saga三个核心技术:

数据隔离性:

  • 业务层控制并发
  • 在应用层加锁

恢复方式:

  • 向后恢复:补偿所有已完成的事务,如果任一子事务失败
  • 向前恢复:重试失败的事务,假设每个子事务最终都会成功

Saga 模型可以满足事务的三个特性:

  • 原子性:Saga 协调器协调事务链中的本地事务要么全部提交,要么全部回滚。
  • 一致性:Saga 事务可以实现最终一致性。
  • 持久性:基于本地事务,所以这个特性可以很好实现。

从数据隔离性上分析,我们可以发现Saga模型无法保证外部的原子性和隔离性,因为可以查看其他Sagas的部分结果,论文中有对应的表述:

Saga 事务和 TCC 事务一样,都是强依靠业务改造,所以要求业务方在设计上要遵循三个策略:

  • 允许空补偿:网络异常导致事务的参与方只收到了补偿操作指令,因为没有执行过正常操作,因此要进行空补偿。
  • 保持幂等性:事务的正向操作和补偿操作都可能被重复触发,因此要保证操作的幂等性。
  • 防止资源悬挂:原因是网络异常导致事务的正向操作指令晚于补偿操作指令到达,则要丢弃本次正常操作,否则会出现资源悬挂问题。

虽然 Saga 和 TCC 都是补偿事务,但是由于提交阶段不同,所以两者也是有不同的:

  • Saga 没有Try行为,直接Commit,所以会留下原始事务操作的痕迹,Cancel属于不完美补偿,需要考虑对业务上的影响。TCC Cancel是完美补偿的Rollback,补偿操作会彻底清理之前的原始事务操作,用户是感知不到事务取消之前的状态信息的。
  • Saga 的补偿操作通常可以异步执行,TCC的Cancel和Confirm可以跟进需要是否异步化。
  • Saga 对业务侵入较小,只需要提供一个逆向操作的Cancel即可;而TCC需要对业务进行全局性的流程改造。
  • TCC最少通信次数为2n(Try、Confirm/Cancel),而Saga为n(n=子事务的数量)。

目前业界提供了两类Saga的实现方式,一种是基于业务逻辑层Proxy设计(基于AOP实现),比如华为的ServiceComb(已贡献给Apache,现在已经成为了Apache的顶级项目);一种是状态机实现的机制,比如阿里的Seata。

5.3.4.1 基于代理的实现方式

Aop Proxy实现原理如下:

 

业务逻辑层调用上加上事务注解@Around(“execution(* *(..)) && @annotation(TX)”),Proxy在真正业务逻辑被调用之前, 生成一个全局唯一TXID 标示事务组,TXID保存在ThreadLocal变量里,方法开始前写入,完成后清除,并向远端数据库写入 TXID 并把事务组置为开始状态。业务逻辑层调用数据访问层之前,通过RPCProxy代理记录当前调用请求参数。如果业务正常,调用完成后,当前方法的调用记录存档或删除。如果业务异常,查询调用链反向补偿。接口必须保证幂等性,满足本地原子性。提供补偿接口实现反向操作。补偿接口也是必须也有幂等性保证。

以购买商品这个业务场景为例,使用Proxy模式的Saga模型实现分布式事务架构设计如下:

TDB是用于记录分布式事务状态和步骤的,包含3张表:

  • t_txgroup:事务组表,记录事务状态

id:事务全局唯一id

state:状态

create_time:创建时间

update_time:更新时间

……

  • t_txrecord:事务调用记录表

id:全局唯一id

txid:事务组id

manner_name:方式名(RPC/HTTP)

service_name:服务名

class_name:类名

method_name:方法名

params:参数

param_types:参数class

compensate_name:补偿方法名

step:步骤

version:版本

create_time:创建时间

……

  • t_txcomponstate:补偿结果表

id:全局唯一id

step:步骤

txid:事务组id

result:结果

create_time:创建时间

update_time:更新时间

……

Proxy是一个切面,用于拦截需要执行分布式事务的方法(可以通过注解实现),在方法执行前后做一些操作。Proxy分为初始化和销毁两个步骤,初始化时生成txid、state等保存到TDB,销毁时更新state状态。

当一个分布式事务操作到来的时候,首先写入事务组表:

Txid

State

Timestamp

T1

1

今天

在调用每个具体的子事务之前记录调用记录表:

Txid

Actionid

callMethod

Type

Params

T1

1

Ca

RPC

Pa

T1

2

Cb

RPC

Pb

T1

3

Cc

RPC

Pc

大部分情况下,请求都会成功(毕竟失败是小概率事件),那么修改事务组表的状态或者删除记录即可。

如果调用失败,比如下单过程,扣减商品库存成功,但是创建订单失败,那么此时Proxy就会将事务组表的状态修改为失败(比如是3)。然后TM(分布式事务补偿服务)的Schedule线程就会扫描到失败的记录,然后去调用组表查询对应的记录,然后进行逆序补偿,调用相应的补偿方法。

Saga成功案例:

Saga失败案例:

思考:分布式事务补偿服务本身也有可能失败,怎么办呢?

记录错误日志、报警、人工处理。

5.3.4.2 基于状态机的实现方式

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、XA、SAGA 等事务模式,为用户打造一站式的分布式解决方案。

目前SEATA提供的Saga模式是基于状态机引擎来实现的,通过状态图来定义服务调用的流程并生成 json 状态语言定义文件,状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点,状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚(异常发生时是否进行补偿也可由用户自定义决定),可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能。

详见http://seata.io/zh-cn/docs/overview/what-is-seata.html

5.4 尽最大努力通知

最大努力通知事务的主流实现仍是基于MQ来进行事务控制。最大努力通知事务和事务消息都是通知型事务,主要适用于那些需要异步更新数据,并且对数据的实时性要求较低的场景。

最大努力通知事务主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商、支付对接、商户通知等等跨平台、跨企业的系统间业务交互场景;而事务消息主要适用于内部系统的数据最终一致性保障,因为内部相对比较可控,比如订单和购物车、收货与清算、支付与结算等等场景。

普通消息是无法解决本地事务执行和消息发送的一致性问题的。因为消息发送是一个网络通信的过程,发送消息的过程就有可能出现发送失败、或者超时的情况。超时有可能发送成功了,有可能发送失败了,消息的发送方是无法确定的,所以此时消息发送方无论是提交事务还是回滚事务,都有可能不一致性出现。所以通知型事务的难度在于投递消息和参与者自身本地事务的一致性保障。因为核心要点一致,都是为了保证消息的一致性投递,所以最大努力通知事务在投递流程上跟事务消息是一样的,可以将消息写入本地消息表,然后使用单独的线程进行通知。尽最大努力通知事务有几个特性:

  • 业务主动方在完成业务处理后,向业务被动方(第三方系统)发送通知消息,允许存在消息丢失。
  • 业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。
  • 业务被动方提供幂等的服务接口,防止通知重复消费。

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

六、总结

分布式系统的生产环境复杂多变,某些情况是可以导致分布式事务机制失效的,所以无论使用哪种方案,都需要最终的兜底策略,人工校验(虽说是人工,其实也只是客服人员串一下数据,确认无误后系统进行修改),修复数据。

各种实现方式对比如下:

  • 一致性保证:XA > TCC = SAGA > 事务消息
  • 业务友好性:XA > 事务消息 > SAGA > TCC
  • 性 能 损 耗:XA > TCC > SAGA = 事务消息

干货来啦!分布式场景之刚性事务-2PC详解

分布式事务科普(初识篇)

分布式柔性事务的TCC方案

分布式柔性事务之Saga详解

分布式柔性事务之事务消息详解

分布式柔性事务之最大努力通知事务详解

  • 12
    点赞
  • 94
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值