分布式事务

分布式事务

 

前言

分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免,本文就分布式事务来简单聊一下。

数据库事务

在说分布式事务之前,我们先从数据库事务说起。 数据库事务可能大家都很熟悉,在开发过程中也会经常使用到。但是即使如此,可能对于一些细节问题,很多人仍然不清楚。比如很多人都知道数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation)和持久性(Durabilily),简称就是ACID。但是再往下比如问到隔离性指的是什么的时候可能就不知道了,或者是知道隔离性是什么但是再问到数据库实现隔离的都有哪些级别,或者是每个级别他们有什么区别的时候可能就不知道了。

本文并不打算介绍这些数据库事务的这些东西,有兴趣可以搜索一下相关资料。不过有一个知识点我们需要了解,就是假如数据库在提交事务的时候突然断电,那么它是怎么样恢复的呢? 为什么要提到这个知识点呢? 因为分布式系统的核心就是处理各种异常情况,这也是分布式系统复杂的地方,因为分布式的网络环境很复杂,这种“断电”故障要比单机多很多,所以我们在做分布式系统的时候,最先考虑的就是这种情况。这些异常可能有 机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失、其他异常等等…

我们接着说本地事务数据库断电的这种情况,它是怎么保证数据一致性的呢?我们使用SQL Server来举例,我们知道我们在使用 SQL Server 数据库是由两个文件组成的,一个数据库文件和一个日志文件,通常情况下,日志文件都要比数据库文件大很多。数据库进行任何写入操作的时候都是要先写日志的,同样的道理,我们在执行事务的时候数据库首先会记录下这个事务的redo操作日志,然后才开始真正操作数据库,在操作之前首先会把日志文件写入磁盘,那么当突然断电的时候,即使操作没有完成,在重新启动数据库时候,数据库会根据当前数据的情况进行undo回滚或者是redo前滚,这样就保证了数据的强一致性。

接着,我们就说一下分布式事务。

在介绍分布式事务之前,让我们先来看一下没有分布式事务的系统会出现什么问题。

假如没有分布式事务

在一系列微服务系统当中,假如不存在分布式事务,会发生什么呢?让我们以互联网中常用的交易业务为例子:

上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。

正常情况下,两个数据库各自更新成功,两边数据维持着一致性。

但是,在非正常情况下,有可能库存的扣减完成了,随后的订单记录却因为某些原因插入失败。这个时候,两边数据就失去了应用的一致性。

 

这种时候必要要保证数据的一致性。单数据源的一致性依靠单机事务来保证,多数据源的一致性就要依靠[分布式事务]。

什么是分布式事务?

分布式事务用于在分布式系统中保证不同节点之间的数据一致性。分布式事务的实现有很多种,最具有代表性的是由Oracle Tuxedo系统提出的XA分布式事务协议。

XA协议包含两阶段提交(2PC)三阶段提交(3PC)两种实现,这里我们重点介绍两阶段提交的具体过程。

XA协议的运作方式,和魔兽世界中的团队协作有些相似。

在魔兽世界这款游戏中,副本组团打BOSS的时候,为了更方便队长与队员们之间的协作,队长可以发起一个“就位确认”的操作:

当队员收到就位确认提示后,如果已经就位,就选择“是”,如果还没就位,就选择“否”。

当队长收到了所有人都饿就位确认,就会向所有队员们发布消息,告诉他们开始打BOSS。

相应的,在队长发起就位确认的时候,有可能某些队员还并没有就位:

以上就是魔兽世界当中组团打BOSS的确认流程。这个流程XA分布式事务协议的两阶段提交非常相似。

那么XA协议究竟是什么样子呢?在XA协议中包含着两个角色:事务协调者事务参与者。让我们来看一看他们之间的交互流程:

第一阶段:

在XA分布式事务的第一阶段,作为事务协调者的节点会首先向所有的参与者节点发送Prepare请求。

在接到Prepare请求之后,每一个参与者节点会各自执行与事务有关的数据更新,写入Undo Log和Redo Log。如果参与者执行成功,暂时不提交事务,而是向事务协调节点返回“完成”消息。

当事务协调者接到了所有参与者的返回消息,整个分布式事务将会进入第二阶段。

第二阶段:

在XA分布式事务的第二阶段,如果事务协调节点在之前所受到都是正向返回,那么它将会向所有事务参与者发出Commit请求。

接到Commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“完成”消息。

当事务协调者接收到所有事务参与者的“完成”反馈,整个分布式事务完成。

以上所描述的是XA两阶段提交的正向流程,接下来我们看一看失败情况的处理流程:

第一阶段:

第二阶段:

在XA的第一阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。

于是在第二阶段,事务协调节点向所有的事务参与者发送Abort请求。接收到Abort请求之后,各个事务参与者节点需要在本地进行事务的回滚操作,回滚操作依照Undo Log来进行。

以上就是XA两阶段提交协议的详细过程。

XA两阶段提交虽然解决了分布式事务的一致性问题,但是仍然存在着很多的不足之处。

XA两阶段提交的不足

XA两阶段提交究竟有哪些不足呢?

  1. 性能问题

XA协议遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着非常明显的性能问题。

  1. 协调者单点故障问题

事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或回滚通知,参与者会一直处于中间状态无法完成事务。

  1. 丢失消息导致的不一致问题

在XA协议的第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么久导致了节点之间数据的不一致。

如何避免XA两阶段提交的种种问题呢?

有许多其他的分布式事务方案可供选择:

  1. XA三阶段提交

XA三阶段提交在两阶段提交的基础上增加了CanCommit阶段,并且引入了超时机制。一旦事物参与者迟迟没有接到协调者的commit请求,会自动进行本地commit。这样有效解决了协调者单点故障的问题。但是性能问题和不一致的问题仍然没有根本解决。

  1. MQ事务

利用消息中间件来异步完成事务的后一半更新,实现系统的最终一致性。这个方式避免了像XA协议那样的性能问题。

  1. TCC事务

TCC事务是Try、Commit、Cancel三种指令的缩写,其逻辑模式类似于XA两阶段提交,但是实现方式是在代码层面来人为实现。

注意:在分布式环境下,无论是二阶段提交协议,还是三阶段提交协议,都存在很大的缺陷,我们并不会使用。更多的是采用MQ事务和TCC事务。典型的MQ事务有可靠消息最终一致性方案和最大努力通知型方案,典型的TCC事务有TCC补偿方案

关于以上三种事务的工作细节。我们暂时不做展开,有兴趣的小伙伴们可以查阅相关资料。感谢大家的支持!

 

自我总结:

之前我学的事务指的都是本地事务,本地事务可以严格的保证事务的四大特性:ACID,但是在分布式环境下,本地事务就无法使用了。

举个例子:

就拿互联网应用的用户注册功能来说,通常都有两个操作:

- 注册成功,保存用户信息;

- 注册成功,给用户对应的积分或成长值。

如果是一个单体架构实现这个功能就非常简单,在一个本地事务里,往用户信息表中插入一条记录,并且往积分表中插入一条记录,提交事务就完成了。但如果是微服务架构,用户和积分通常是两个独立的服务,它们有各自的应用和数据库,那么本地事务就失效了。

这就涉及到分布式事务问题,分布式事务解决方案基于两个定理,分别是CAP定理和BASE定理。

CAP定理:

CAP定理理论告诉我们,一个分布式系统不可能同时满足一致性、可用性和分区容错性这三个基本需求,最多只能同时满足其中的两项。

一致性

在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态。

对于一个将数据副本分布在不同分布式节点上的系统来说,如果对第一个节点的数据进行了更新操作并且更新成功后,却没有使得第二个节点上的数据得到相应的更新,于是在对第二个节点的数据进行读取操作时,获取的依然是老数据(或称为脏数据),这就是典型的分布式数据不一致情况。在分布式系统中,如果能够做到针对一个数据项的更新操作执行成功后,所有的用户都可以读取到其最新的值,那么这样的系统就被认为具有强一致性(或严格的一致性)。

可用性

可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里我们重点看下“有限的时间内”和“返回结果”。

“有限的时间内”是指,对于用户的一个操作请求,系统必须能够在指定的时间(即响应时间)内返回对应的处理结果,如果超过了这个时间范围,那么系统就被认为是不可用的。另外,“有限的时间内”是一个在系统设计之初就设定好的系统运行指标,通常不同的系统之间会有很大的不同。比如说,对于一个在线搜索引擎来说,通常在0.5秒内需要给出用户搜索关键词对应的检索结果。以Google为例,搜索“分布式”这一关键词,Google能够在0.3秒左右的时间,返回大约上千万条检索结果。而对于一个面向HIVE的海量数据查询平台来说,正常的一次数据检索时间可能在20秒到30秒之间,而如果是一个时间跨度较大的数据内容查询,“有限的时间”有时候甚至会长达几分钟。

从上面的例子中,我们可以看出,用户对于一个系统的请求响应时间的期望值不尽相同。但是,无论系统之间的差异有多大,唯一相同的一点就是对于用户请求,系统必须存在一个合理的响应时间,否则用户便会对系统感到失望。

让我们再来看看上面提到的在线搜索引擎的例子,如果用户输入指定的搜索关键词后,返回的结果是一个系统错误,通常类似于“OutOfMemoryError”或“System Has Crashed”等提示语,那么我们认为此时系统是不可用的。

分区容错性

分区容错性约束了一个分布式系统需要具有如下特性:分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

网络分区是指在分布式系统中,不同的节点分布在不同的子网络(机房或异地网络等)中,由于一些特殊的原因导致这些子网络之间出现网络不连通的状况,但各个子网络的内部网络时正常的,从而导致整个系统的网络环境被切分成了若干个孤立的区域。需要注意的是,组成一个分布式系统的每个节点的加入与退出都可以看作是一个特殊的网络分区。

以上就是对CAP定理中一致性、可用性和分区容错性的讲解,通常使用图1-2所示的示意图来表示CAP定理。

既然在上文中我们提到,一个分布式系统无法同时满足上述三个需求,而只能满足其中的两项,因此在进行对CAP定理的应用时,我们就需要抛弃其中的一项,表1-2所示是抛弃CAP定理中任意一项特性的场景说明。

从CAP定理中我们可以看出,一个分布式系统不可能同时满足一致性、可用性和分区容错性这三个需求。另一方面,需要明确地一点是,对于一个分布式系统而言,分区容错性可以说是一个最基本的要求。为什么这样说,其实很简单,因为既然是一个分布式系统,那么分布式系统中的组件必然需要被部署到不同的节点,否则也就无所谓分布式系统了,因此必然出现子网络。而对于分布式系统而言,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。因此系统架构设计师往往需要把精力花在如何根据业务特点在C(一致性)和A(可用性)之间寻求平衡。

基于CAP定理,我们知道,要在分布式系统中实现事务的ACID特性是不现实的,必须进行取舍,因此出现了BASE定理。

BASE定理:

Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)。

将BASE定理和CAP定理结合可知:BASE定理牺牲高一致性,获取可用性和分区容错性。

将BASE定理与ACID特性结合可知:BASE定理保障了事务的原子性(A)和持久性(D),降低了一致性(C)和隔离性(I)的要求。

基于CAP定理和BASE定理,常见的分布式事务解决方案有三种:

(1)可靠消息最终一致性方案(MQ事务)(适用场景比较广)

(2)TCC事务补偿型方案(TCC事务)

(3)最大努力通知型方案(MQ事务)

简单介绍一下可靠消息最终一致性方案。

答:可靠消息最终一致性方案依赖于消息中间件,可以把整个方案分成两个问题,是消息发送一致性问题,一个是消息投递问题。市场上现有的消息中间件,如MQ系列,并不能保证消息发送的一致性问题,所以,可靠消息最终一致性方案除了用到消息中间件外,还需要基于消息中间件完成一些辅助设计,这些设计可分为如下子系统:

消息服务子系统

实时消息服务子系统(消息中间件)

消息状态确认子系统

消息恢复子系统

消息管理子系统

 

其正向流程如下:

  1. 主动方应用系统调用“预发送消息接口”把消息发给消息服务子系统,消息状态标记为“待确认”;
  2. 消息服务子系统调用“存储预发送消息接口”,把消息持久化到消息服务库,但并不向被动方应用系统投递消息;
  3. 消息服务子系统向主动方应用系统返回消息持久化结果(成功/失败),主动方应用系统根据返回结果进行判断如何进行业务操作处理:
  1. 失败:放弃业务操作处理,结束(必要时向上层返回失败结果);
  2. 成功:执行业务操作处理;
  1. 业务操作完成后,把业务操作结果(成功/失败)发送给消息服务子系统;
  2. 消息服务子系统收到业务操作结果后,根据业务结果进行处理;
  1. 失败:删除消息存储中的消息,结束;
  2. 成功:将状态为“待确认”的消息更新为“待发送(可发送)”,并将消息发送给实时消息服务子系统。
  1. 被动方应用系统中的消息业务消费端对实时消息服务子系统进行实时监控;
  2. 当检测到有消息需要处理时,调用“业务处理”接口,处理业务。
  3. 业务处理完毕,被动方应用系统中的消息业务消费端向实时消息服务子系统返回一个带有ACK标志的确认信息。实时消息服务子系统收到这个确认消息之后,就会将队列中对应的消息删除掉;
  4. 被动方应用系统中的消息业务消费端调用消息服务子系统中的“确认消息已被成功消费”接口,将对应的“待发送”状态的消息删除掉。

若整个过程发生异常怎么处理?

(消息发送一致性过程异常处理)

当消息服务子系统收到主动方应用系统发送的“待确认”消息,并将之持久化进消息服务器后,会向主动方应用系统返回一个持久化结果,接下来就是等待主动方应用系统返回的业务操作结果。可是这个过程因为是跨服务,就有可能因为网络原因中断。消息服务子系统迟迟接收不到主动方应用系统返回的业务执行结果,无法知道执行到底是成功还是失败,那该怎么办呢?

消息状态确认子系统用来处理这种异常情况。我们可以将消息状态确认子系统看作是一个定时轮询的进程,它会定时调用消息服务子系统中的“查询状态确认超时的消息”接口,从消息服务库中将那些一直得不到确认的“待确认”消息捞取出来,同时消息状态确认子系统也要调用主动方应用系统中的“业务查询”接口,来判断对应的业务操作有没有执行成功,若执行成功,消息状态确认子系统就会调用消息服务子系统中的“确认并发送消息”接口,将“待确认”消息的状态改为“待发送”,并将消息发送给实时消息服务子系统;若执行失败,消息状态确认子系统就会调用消息服务子系统中的“删除消息”接口,将那些得不到确认的“待确认消息”删除。

消息状态确认子系统是用来处理消息发送一致性中的异常情况的。

(消息投递过程异常处理)

被动方应用系统中的消息业务消费端监听到实时消息服务子系统中有消息需要处理时,就会调用“业务操作”接口执行业务操作,之后会返回给实时消息服务子系统一个确认信息。两者也是跨服务的,可能会因为网络原因导致中断。无法将ACK确认消息返给实时消息服务子系统。这样就会导致消息服务子系统中状态为“待发送”的消息得不到“消息已被成功消费”的确认。对于这种异常情况,我们要如何处理呢?

消息恢复子系统是用来解决消息消费过程中出现的异常情况的。我们也可以将消息恢复子系统看作一个定时轮询的进程,它会定时调用消息服务子系统中的“查询消费确认超时的消息”接口,将那些迟迟得不到消费确认的状态为“待发送”状态的消息捞取出来,并把这些消息重新投递给实时消息服务子系统。让消息投递过程再重新执行一遍。

简单介绍一下TCC事务补偿型方案。

答:TCC方案不需要用到消息中间件。解决分布式事务问题时,大多数情况下,我们选择使用可靠消息最终一致性方案,但有些业务场景对实时性要求特别高,这个时候我们必须要选择使用TCC方案。比方说账户处理、收费等业务场景。

比方说在电子商务中,当用户订单创建完成之后,需要向资金库添加资金,向积分库添加积分,这种情况对实时性要求特别高,对一致性要求特别高,对隔离性要求特别高,这种情况下就要选择使用TCC方案来处理分布式事务问题。

一个完整的TCC事务方案包含三个角色,分别是:主业务服务、从业务服务和业务活动管理器。

•主业务服务负责发起并完成整个业务活动

•从业务服务提供TCC型业务操作

•业务活动管理器控制业务活动的一致性,它登记业务活动中的操作,并在业务活动提交时确认所有的TCC型操作的confirm操作,在业务活动取消时调用所有TCC型操作的cancel操作。

我们可以这样理解,主业务活动负责发起整个事务,但具体的工作由从业务服务来完成,我们所讲的TCC事务方案主要指的就是从业务服务所做的事情。

TCC对应着三个英文单词,分别是Try、Confirm和Cancel三种操作,这三种操作的业务含义如下:

•Try:预留业务资源

•Confirm:确认执行业务操作

•Cancel:取消执行业务操作

 

这样讲特别抽象,我们举一个具体的例子来说明:

账务拆分的业务场景如下,分别位于三个不同分库的帐户A、B、C,A和B一起向C转帐共80元:

(1)Try:尝试执行业务。

完成所有业务检查(一致性):检查A、B、C的帐户状态是否正常,帐户A的余额是否不少于30元,帐户B的余额是否不少于50元。

预留业务资源(准隔离性):帐户A的冻结金额增加30元,帐户B的冻结金额增加50元,这样就保证不会出现其他并发进程扣减了这两个帐户的余额而导致在后续的真正转帐操作过程中,帐户A和B的可用余额不够的情况。

(2)Confirm:确认执行业务。

真正执行业务:如果Try阶段帐户A、B、C状态正常,且帐户A、B余额够用,则执行帐户A给账户C转账30元、帐户B给账户C转账50元的转帐操作。

不做任何业务检查:这时已经不需要做业务检查,Try阶段已经完成了业务检查。

只使用Try阶段预留的业务资源:只需要使用Try阶段帐户A和帐户B冻结的金额即可。

(3)Cancel:取消执行业务

释放Try阶段预留的业务资源:如果Try阶段部分成功,比如帐户A的余额够用,且冻结相应金额成功,帐户B的余额不够而冻结失败,则需要对帐户A做Cancel操作,将帐户A被冻结的金额解冻掉。

问题1:TCC方案是二阶段提交操作,但和XA协议中的二阶段提交2PC不是一回事。

问题2:TCC方案的主要缺点就是开发成本高(需要人为实现代码,难!)。

问题3:在实时性要求高、一致性要求高、隔离性要求的业务场景下,一般都要选择TCC事务方案。

 

简介一下最大努力通知型方案。

答:该方案和可靠消息最终一致性方案一样,都要借助消息中间件。具体业务流程没细研究。

 

分布式中为什么会经常用到消息中间件(消息队列),有什么用?

答:消息队列利用发布——订阅模式工作,消息发送者发布消息,一个或者多个消息接收者订阅消息。消息发送者是消息源,在对消息进行处理后将消息发送至分布式消息队列,消息接收者从分布式消息队列获取该消息后继续进行处理。

消息中间件在分布式系统中的主要作用:异步通讯解耦并发缓冲。通过引入消息中间件来解耦应用间(服务间)的直接调用,同时也会起到异步通讯和缓冲并发的作用。

异步通讯:消息发送方将消息发送给消息队列之后,不需要等待消息接收方将数据处理,就可以再发送其它的消息给消息队列。

解耦:消息发送者和消息接受者之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,而消息接受者只需要从分布式消息队列获取消息后进行处理,不需要知道该消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而降低了网站业务的耦合性。

并发缓冲:在网站访问高峰,消息可以暂时存储在消息队列中等待消息接收者根据自身负载处理能力控制消息处理速度,减轻数据库等后端存储的负载压力。

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值