理解这篇分布式事务文章,可以做到吊打面试官

大家好,我是沐子。

不知道你是否遇到过这样的情况,去小卖铺买东西,付了钱,但是店主因为处理了一些其他事,居然忘记你付了钱,又叫你重新付。又或者在网上购物明明已经扣款,但是却告诉我没有发生交易。这一系列情况都是因为没有事务导致的,这说明了事务在生活中的一些重要性。我们无论在工作或者在面试中,都会碰到分布式事务相关问题。近几年我在面试开发工程师们,都发现大都存在一个共同的问题,对分布式事务一知半解,且也仅仅停留在初级的使用层面,基于上述情况,小篇给你彻底讲清楚分布式事务,在面试时争取可以吊打面试官。

一、 什么是分布式事务

在分布式电商网站中,订单和库存分为两个子系统对外提供服务,子系统间使用rpc进行通信。用户对商品进行下单时,需要在订单库中创建一条订单数据,同时需要在库存库中修改当前商品的剩余库存数量,两步操作一个添加,一个修改,我们一定要保证这两步操作一定同时操作成功或失败,否则业务就会出现问题。但由于跨库跨机器,mysql的本地事务不能再保证订单库和库存库的数据一致性,这时候就需要分布式事务来保证。

对于分布式系统而言,要保证分布式系统中的数据一致性就需要一种方案,可以保证数据在子系统中始终保持一致,避免业务出现问题,这种实现方案就叫做分布式事务,要么一起成功,要么一起失败,必须是一个整体性的事务。

二、 分布式事务理论基础

在讲解分布式事务具体方案时,我们首先要熟悉CAP理论和BASE理论,这样才能更好的掌握分布式事务。

1. CAP理论

CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:

一致性(Consistency):客户端知道一系列的操作都会同时发生(生效)

可用性(Availability): 每个操作都必须以可预期的响应结束

分区容错性(Partition tolerance):即使出现单个组件无法可用,操作依然可以完成

对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到 N 个 9,即保证 P 和 A,舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度。

对于涉及到钱财这样不能有一丝让步的场景,C必须保证。网络发生故障宁可停止服务,这是保证 CA,舍弃 P。貌似这几年国内银行业发生了不下10 起事故,但影响面不大,报道也不多,广大群众知道的少。还有一种是保证 CP,舍弃 A。例如网络故障是只读不写。

由于分布式理论不是本篇的重点,这里只做一些简单概述,如果你想更深入的了解CAP,可以点击我的另外一篇文章CAP理论。

2. BASE理论

BASE理论是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写,BASE是对CAP中一致性和可用性权衡的结果,其核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式使系统达到最终一致性

➢基本可用:

基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。这不等价于系统不可用。

响应时间上的损失:正常情况下,一个在线搜索引擎需要在0.5秒内返回给用户相应的查询结果,但由于出现故障,比如系统部分机房发生断点或断网故障,查询结果的享用时间增加到1~2秒。

功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利的完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。

➢弱状态:

弱状态也成为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延迟。

➢最终一致性:

最终一致性强调的是系统所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能能够达到一致,而不需要实时保证系统数据的强一致性。

BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事物ACID特性是相反的。它完全不同于ACID的强一致性模型,是对CAP理论的延伸和补充,主要是对AP的补充,是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。

三、 分布式事务解决方案

分布式事务实现方案从类型上分为刚性事务和柔型事务:

刚性事务满足CAP的CP理论

柔性事务满足BASE理论(基本可用,最终一致)

1. 刚性事务

刚性事务指的是,要使分布式事务,达到像本地式事务一样,具备数据强一致性,从CAP来看,就是说,要达到CP状态。刚性事务通常无业务改造,

强一致性,原生支持回滚/隔离性,低并发,适合短事务。由于同步阻塞,处理效率低,不适合大型网站分布式场景。

1.1 事务协议

XA是一个规范、协议,它只是定义了一系列的接口,只是目前大多数实现XA的都是数据库或者MQ,所以提起XA往往多指基于资源层的底层分布式事务解决方案。而XA模型主要使用了两段提交(2PC

-

Two-Phase-Commit)来保证分布式事务的完整性。其中两阶段提交(2PC)协议是XA规范定义的数据一致性协议,三阶段提交(3PC)协议对

2PC协议的一种扩展。

1.1.1 两阶段提交(2PC)

两阶段提交又称2PC,2PC是一个非常经典的强一致、中心化的原子提交协议。

这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点(coordinator)和N个参与者节点(partcipant)。

两个阶段:第一阶段:投票阶段和第二阶段:提交/执行阶段

举例订单服务A,需要调用支付服务B去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理为失败状态。

第一阶段:投票阶段

第一阶段主要分为3步

1)事务询问

协调者向所有的参与者发送事务预处理请求,称之为Prepare,并开始等待各参与者的响应。

2)执行本地事务

各个参与者节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向协调者报告说:“我这边可以处理了/我这边不能处理”。.

3)各参与者向协调者反馈事务询问的响应

如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行,如果没有参与者成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行。

第一阶段执行完后,会有两种可能。1、所有都返回Yes. 2、有一个或者多个返回No。


第二阶段:提交/执行阶段(成功流程)

成功条件:所有参与者都返回Yes。

第二阶段主要分为两步

1)所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交

协调者所有参与者节点发出Commit请求.

2)事务提交

参与者收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源。


第二阶段:提交/执行阶段(异常流程)

异常条件:任何一个参与者协调者反馈了No响应,或者等待超时之后,协调者尚未收到所有参与者的反馈响应。

异常流程第二阶段也分为两步

1)发送回滚请求

协调者向所有参与者节点发出RoollBack请求.

2)事务回滚

参与者接收到RoollBack请求后,会回滚本地事务。


2PC缺点

通过上面的演示,很容易想到2pc所带来的缺陷

1)性能问题

无论是在第一阶段的过程中,还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,

参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大

2)丢失消息导致的数据不一致问题

数据不一致。当执行事务提交过程中,如果协调者向所有参与者发送Commit请求后,发生局部网络异常或者协调者在尚未发送完Commit请求,即出现崩溃,最终导致只有部分参与者收到、执行请求。于是整个系统将会出现数据不一致的情形;

3)单节点故障由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(虽然协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)


2PC出现单点问题的三种情况

(1)协调者正常,参与者宕机

由于协调者无法收集到所有参与者的反馈,会陷入阻塞情况。

解决方案:引入超时机制,如果协调者在超过指定的时间还没有收到参与者的反馈,事务就失败,向所有节点发送终止事务请求。

(2)协调者宕机,参与者正常

无论处于哪个阶段,由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交状态的参与者都会陷入阻塞情况.

解决方案:引入协调者备份,同时协调者需记录操作日志.当检测到协调者宕机一段时间后,协调者备份取代协调者,并读取操作日志,向所有参与者询问状态。

(3)协调者和参与者都宕机

1)发生在第一阶段:因为第一阶段,所有参与者都没有真正执行commit,所以只需重新在剩余的参与者中重新选出一个协调者,新的协调者在重新执行第一阶段和第二阶段就可以了。

2)发生在第二阶段 并且 挂了的参与者在挂掉之前没有收到协调者的指令。也就是上面的第4步挂了,这是可能协调者还没有发送第4步就挂了。这种情形下,新的协调者重新执行第一阶段和第二阶段操作。

3)发生在第二阶段 并且 有部分参与者已经执行完commit操作。就好比这里订单服务A和支付服务B都收到发送的commit信息,开始真正执行本地事务commit,但突发情况,Acommit成功,B确挂了。这个时候目前来讲数据是不一致的。虽然这个时候可以再通过手段让他和协调者通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了!2PC 无法解决这个问题。

2PC 方案比较适合单体应用里,因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。


1.1.2 三阶段提交(3PC)

三阶段提交协议(3PC)主要是为了解决两阶段提交协议的阻塞问题,2pc存在的问题是当协作者崩溃时,参与者不能做出最后的选择。因此参与者可能在协作者恢复之前保持阻塞。三阶段提交(Three-phase commit),是二阶段提交(2PC)的改进版本。

与两阶段提交不同的是,三阶段提交有两个改动点。

1、引入超时机制。同时在协调者和参与者中都引入超时机制。

2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

CanCommit阶段

之前2PC的一阶段是本地事务执行结束后,最后不Commit,等其它服务都执行结束并返回Yes,由协调者发生commit才真正执行commit。而这里的CanCommit指的是尝试获取数据库锁如果可以,就返回Yes。

这阶段主要分为2步

事务询问协调者参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。

响应反馈参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。这里的PreCommit阶段跟上面的第一阶段是差不多的,只不过这里协调者和参与者都引入了超时机制(2PC中只有协调者可以超时,参与者没有超时机制)。

DoCommit阶段

这里跟2pc的阶段二是差不多,这里就不过多讲解了。

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞, 因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

基于LCN框架解决分布式事务

TX-LCN定位于一款事务协调性框架,框架其本身并不操作事务,而是基于对事务的协调从而达到事务一致性的效果,核心采用3PC。(LCN不生产事务,它只是事务的搬运工)

  2)当请求的发起方(调用方)进入接口业务之前,会通过AOP技术进到@LcnTransaction注解中去LCN协调者那边生成注册一个全局的事务组Id(groupId)。

  3)当发起方(调用方)通过rpc调用参与方(被调用方)的时候,lcn重写了Feign客户端,会从ThreadLocal中拿到该事务组Id(groupId),并将该事务组Id设置到请求头中。

  4)参与方(被调用方)在请求头中获取到了这个groupId的时候,lcn会标识该服务为参与方并加入到该事务组,并会被lcn代理数据源,当该服务业务逻辑执行完成后,进行数据源的假关闭,并不会真正的提交或回滚当前服务的事务。

  5)当发起方执行完全部业务逻辑的时候,如果无异常会告知lcn协调者,lcn协调者再分别告诉该请求链上的所有参与方可以提交了,再进行真正的提交。若发起方调用完参与方后报错了,也会告知lcn协调者,lcn协调者再告知所有的参与方进行真正的回滚操作,这样就解决了分布式事务的问题。

由于LCN核心采用3PC协议,它的优点是能够保证数据的强一致性;但同时也具有3PC协议的一些缺点,即可能会造成死锁的现象,比如,订单服务调用派单服务成功以后,订单服务还没执行完毕就宕机,此时,TxManage并没有收到通知,派单服务的事务也不能顺利进行,导致死锁;其次lcn的性能不是特别强大。但由于代码侵入性低,实现分布式事务成本低,如果项目对性能要求不是特别高时,可以考虑使用该框架。

2. 柔性事务

在电商领域等互联网场景下,刚性事务在数据库性能和处理能力上都暴露出了瓶颈。其中柔性事务有两个特性:基本可用和柔性状态。

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

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

柔性事务指的是,不要求强一致性,而是要求最终一致性,允许有中间状态,也就是Base理论,换句话说,就是AP状态。与刚性事务相比,柔性事务的特点为:有业务改造,最终一致性,实现补偿接口,实现资源锁定接口,高并发,适合长事务。

柔性事务主要分为通知型(MQ事务消息、最大努力通知型)和补偿型(TCC)。

2.1 通知型

通知型事务的主流实现是通过MQ(消息队列)来通知其他事务参与者自己事务的执行状态,引入MQ组件,有效的将事务参与者进行解耦,各参与者都可以异步执行,所以通知型事务又被称为异步事务。通知型事务主要适用于那些需要异步更新数据,并且对数据的实时性要求较低的场景,主要包含:异步确保型事务和最大努力通知事务两种。

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

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

2.1.1 异步确保型事务

指将一系列同步的事务操作修改为基于消息队列异步执行的操作,来避免分布式事务中同步阻塞带来的数据操作性能的下降,其中常见的方案:MQ事务消息方案和本地消息表方案。

a. MQ事务消息方案

基于MQ的事务消息方案主要依靠MQ的

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

半消息:在原有队列消息执行后的逻辑,如果后面的本地逻辑出错,则不发送该消息,如果通过则告知MQ发送;

流程

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

2. MQ通知发送方消息发送成功;

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

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

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

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

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

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

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

基于阿里 RocketMQ实现MQ异步确保型事务

有一些第三方的MQ是支持事务消息的,这些消息队列,支持半消息机制,比如RocketMQ,ActiveMQ。但是有一些常用的MQ也不支持事务消息,比如 RabbitMQ 和 Kafka 都不支持。

以阿里的 RocketMQ 中间件为例,其思路大致为:

1.producer(本例中指A系统)发送半消息到broker,这个半消息不是说消息内容不完整, 它包含完整的消息内容, 在producer端和普通消息的发送逻辑一致

2.broker存储半消息,半消息存储逻辑与普通消息一致,只是属性有所不同,topic是固定的RMQ_SYS_TRANS_HALF_TOPIC,queueId也是固定为0,这个tiopic中的消息对消费者是不可见的,所以里面的消息永远不会被消费。这就保证了在半消息提交成功之前,消费者是消费不到这个半消息的

3.broker端半消息存储成功并返回后,A系统执行本地事务,并根据本地事务的执行结果来决定半消息的提交状态为提交或者回滚

4.A系统发送结束半消息的请求,并带上提交状态(提交 or 回滚)

5.broker端收到请求后,首先从RMQ_SYS_TRANS_HALF_TOPIC的queue中查出该消息,设置为完成状态。如果消息状态为提交,则把半消息从RMQ_SYS_TRANS_HALF_TOPIC队列中复制到这个消息原始topic的queue中去(之后这条消息就能被正常消费了);如果消息状态为回滚,则什么也不做。

6.producer发送的半消息结束请求是 oneway 的,也就是发送后就不管了,只靠这个是无法保证半消息一定被提交的,rocketMq提供了一个兜底方案,这个方案叫消息反查机制,Broker启动时,会启动一个

TransactionalMessageCheckService 任务,该任务会定时从半消息队列中读出所有超时未完成的半消息,针对每条未完成的消息,Broker会给对应的Producer发送一个消息反查请求,根据反查结果来决定这个半消息是需要提交还是回滚,或者后面继续来反查

7.consumer(本例中指B系统)消费消息,执行本地数据变更(至于B是否能消费成功,消费失败是否重试,这属于正常消息消费需要考虑的问题)

在RocketMq中,不论是producer收到broker存储半消息成功返回后执行本地事务,还是broker向producer反查消息状态,都是通过回调机制完成,我把producer端的代码贴出来你就明白了:

半消息发送时,会传入一个回调类TransactionListener,使用时必须实现其中的两个方法,executeLocalTransaction

方法会在broker返回半消息存储成功后执行,我们会在其中执行本地事务;checkLocalTransaction方法会在broker向producer发起反查时执行,我们会在其中查询库表状态。两个方法的返回值都是消息状态,就是告诉broker应该提交或者回滚半消息。

b. 本地消息表方案

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

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

本地消息表流程

发送消息方:

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

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

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

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

消息消费方:

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

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

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

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

c. MQ事务消息 VS 本地消息表二者的共性:

1、 事务消息都依赖MQ进行事务通知,所以都是异步的。

2、事务消息在投递方都是存在重复投递的可能,需要有配套的机制去降低重复投递率,实现更友好的消息投递去重。

3、事务消息的消费方,因为投递重复的无法避免,因此需要进行消费去重设计或者服务幂等设计。

二者的区别:

MQ事务消息:

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

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

DB本地消息表:

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

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

根据上述MQ事务消息 和 本地消息表对比,我们一般会不采用本地消息表,而是采用MQ事务消息方案(RocketMQ)来实现分布式事务。

2.1.2 最大努力通知

最大努力通知方案的目标,就是发起通知方通过一定的机制,最大努力将业务处理结果通知到接收方。本质是通过引入定期校验机制实现最终一致性,对业务的侵入性较低,适合于对最终一致性敏感度比较低、业务链路较短的场景。

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

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

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

基于MQ自身的事务消息方案

基于DB的本地事务消息表方案

a. MQ事务消息方案

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

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

因为异步确保型在于内部的事务处理,所以MQ和系统是直连并且无需严格的权限、安全等方面的思路设计。

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

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

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

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

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

特点

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

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

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

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

b. 本地消息表方案

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

发送消息方:

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

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

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

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

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


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

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

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

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

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

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

最大努力通知事务在我认知中,其实是基于异步确保型事务发展而来适用于外部对接的一种业务实现。他们主要有的是业务差别,如下:

• 从参与者来说:最大努力通知事务适用于跨平台、跨企业的系统间业务交互;异步确保型事务更适用于同网络体系的内部服务交付。

• 从消息层面说:最大努力通知事务需要主动推送并提供多档次时间的重试机制来保证数据的通知;而异步确保型事务只需要消息消费者主动去消费。

• 从数据层面说:最大努力通知事务还需额外的定期校验机制对数据进行兜底,保证数据的最终一致性;而异步确保型事务只需保证消息的可靠投递即可,自身无需对数据进行兜底处理。

2.2 补偿型

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

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

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

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

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


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

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

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

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

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

什么是补偿模式?

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

TCC 事务模型

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

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

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

Confirm 操作:对业务系统做确认提交,确认执行业务操作,不做其他业务检查,只使用 Try 阶段预留的业务资源。

Cancel 操作:在业务执行错误,需要回滚的状态下执行业务取消,释放预留资源。

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

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

TCC事务模型的要求:

可查询操作:服务操作具有全局唯一的标识,操作唯一的确定的时间。

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

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

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

TCC与2PC对比

TCC其实本质和2PC是差不多的:

T就是Try,两个C分别是Confirm和Cancel。

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

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

在阶段1:

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

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

在阶段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方案,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。

Seata事务解决方案

在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。商业化产品GTS 先后在阿里云、金融云进行售卖。

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,具体的实践案例就不在这里说明了,大家可以在网上找到相关的内容。

四、 分布式事务方案对比分析

1)2PC最大的诟病是一个阻塞协议。RM 在执行分支事务后需要等待 TM 的决定,此时服务会阻塞并锁定资源。由于其阻塞机制和最差时间复杂度高,因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并发较高以及子事务生命周期较长(long-running transactions) 的分布式服务中。如果我们的项目中并发量要求不是特别的高,可以采用lcn或者seata框架实现分布式事务。

2)如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC 通常都是在跨库的 DB 层面,而 TCC则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现 Try、Confirm、Cancel 三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。典型的使用场景:满减,登录送优惠券等,开源的框架可以采用lcn或者seata框架实现分布式事务。

3)异步确保型(可靠消息最终一致性)事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注册送积分,登录送优惠券等。由于本地消息表耦合到业务中等缺点,我们一般采用RocketMQ实现分布式事务。

4)最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果通知等,一般也采用MQ来实现分布式事务。

在条件允许的情况下,我们尽可能选择本地事务单数据源,因为它减少了网络交互带来的性能损耗,且避免了数据弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否合理,是否高内聚低耦合?是否粒度太小?分布式事务一直是业界难题,因为网络的不确定性,而且我们习惯于拿分布式事务与单机事务

ACID 做对比。

总之,无论是数据库层的 XA、还是应用层 TCC、异步确保型(可靠消息)、最大努力通知等方案,都没有完美解决分布式事务问题,它们不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。

沐子总结了上述几种常见的分布式事务方案,篇幅有些长,大家耐心的多看几遍,多多总结,相信在面试或者工作中会有一定的帮助。最后,如果我的文章对你有所帮助或者有所启发,还请帮忙点赞、在看、转发一下,你的支持会激励我输出更高质量的文章,非常感谢!

最后,如果我的文章对你有所帮助或者有所启发,欢迎关注公众号(微信搜索公众号:首席架构师专栏),里面有许多技术干货,也有我对技术的思考和感悟,还有作为架构师的验验分享;关注后回复 【面试题】,有我准备的面试题、架构师大型项目实战视频等福利 , 小编会带着你一起学习、成长,让我们一起加油!!!

你可能感兴趣的:(理解这篇分布式事务文章,可以做到吊打面试官)