微服务分布式事务指南大全


本白皮书汇总了经典事务和分布式事务的概念。
然后,我们解释了基于云的应用如何受到分布的影响。
最后,我们介绍了基于补偿的事务,作为基于微服务的应用事务的可靠方法,即使是在云中。

1、经典事务
我们将简要概述事务概念的演变,首先总结ACID范式,经典世界认为它是 "事务"的同义词。我们还将介绍一个经典的事务实例,该实例将作为整个白皮书的运行实例,并将在后续章节中加以完善。

1.1 起源
控制范围[1]通常被认为是后来在数据库背景下被称为事务[2](有时也被称为逻辑工作单元)的先驱。一个控制范围可以防止其他人在它仍处于活动状态时修改在其中处理的信息。它还确定哪些步骤共享相同的身份,从而实现对修改的跟踪。此外,它还控制其结果的使用,并可以单方面撤销其处理。事务的概念[3]建立在控制范围的基础上,将事务定义为具有原子性、耐久性和一致性属性的数据转换的集合。这些属性保证了事务要么完全完成,要么根本没有完成,其效果是持久的,并且数据库保持在一个一致的状态。

当一个事务成功时,其步骤所做的所有修改对外界来说都是可见的。然而,如果在事务执行过程中发生错误,其步骤所做的修改将被撤销,世界的状态将被恢复到原来的状态。这一原则是原始事务概念的基础,它确保了事务的效果被完全应用或完全回滚。然而,这一原则在基于补偿的恢复中不能得到保证。

1.2 ACID范式
事务的属性是原子性、一致性、隔离性和持久性,形成了缩写为ACID的记忆法[4]。我们简要地定义这些属性如下(更多细节见[5]):

  • 原子性(A):一个事务必须完全应用或完全回滚它的所有步骤以确保原子性。
  • 一致性(C):为了保证一致性,一个事务必须从一个一致的状态转换到另一个一致的状态,如果一个状态遵守了与应用领域相关的一套规则,就被认为是一致的。在整个事务执行过程中,这个属性保证了数据库保持在一个有效的状态中。
  • 隔离(I):为了确保数据的完整性和防止数据损坏,一个事务的影响必须在其执行过程中不被其他事务发现,这可以通过保持事务的隔离来实现。
  • 持久性(D):为了确保事务工作的持久性,并允许在系统发生故障时进行恢复,必须保证一个成功的事务的结果能够在任何故障中生存。

研究人员已经开发了大量的知识和技术来实现遵守ACID范式的系统[6]。尽管这本参考书可能被认为是旧书,但它仍然是经典事务和分布式事务背景的最终参考书。各种技术,如基于锁定和版本控制的并发控制机制,被用来实现ACID范式。这些技术确保事务在隔离状态下执行,并在事务的整个生命周期内保持数据的一致性。如第3.4节所述,这些技术可以在微服务的背景下应用。  

图1显示了一个资金转移,这是事务的一个常见例子。

  • 假设在同一个银行数据库里有两个账户,Account_1和Account_2。需要一个微服务函数将100个货币单位从账户_1转移到账户_2。该微服务将转账实现为一个事务,以使其成为现实。
  • 它使用一个显式操作(如BEGIN)开始事务,将100个货币单位记入账户_2,从账户_1借入100个货币单位,最后结束事务。
  • 如果没有检测到错误,微服务就用COMMIT完成事务,请求环境确保事务的原子性和结果的持久性,同时确保事务的隔离性。

微服务的逻辑通过确保贷方金额与借方金额相同来保证一致性,这是资金转移规则的要求。

然而,如果在处理过程中检测到错误(例如,如果Account_1的余额少于100个货币单位),事务以ROLLBACK结束,请求环境撤销贷记和借记步骤。环境也确保了隔离性和原子性。

明确地开始和结束一个业务交易被称为事务transaction ,这被认为是一种最佳做法。

环境可以单方面决定撤消事务,即使微服务要求提交它,如果它识别出错误情况,例如无法保证结果的持久性。

2.0 分布式事务
公司经常使用多个存储系统,导致分布式事务,这给原子承诺带来了挑战。两相commit协议是解决这个问题的可能方案。为了说明这一点,让我们考虑一个基于微服务的分布式事务的例子。此外,数据复制是另一种值得讨论的分布形式。资源管理者经常采用数据复制来隐含地利用复制。

2.1 多个存储系统
在实践中,公司经常使用多个存储系统来管理他们的数据,有几个原因:

  • 由于数据的多样性,单一类型的数据库可能不适合一个公司。例如,表格数据适用于关系型数据库系统,而高度网络化的数据则最好由图形数据库管理。  
  • 有时,公司使用同一类型的多个数据库系统,即使数据是同质的。例如,他们可能会使用同一供应商的多个关系型数据库系统来进行工作负荷分配或灾难恢复准备。另外,由于当地的购买偏好或作为供应商多样性战略的一部分,公司可能在不同地点使用不同供应商的关系数据库系统。
  • 有些数据具有独特的特性,需要专门的存储系统。例如,信息往往需要持久性,以满足可用性的需要,但只是在很短的时间内(被称为 "飞行中 "的数据)。为了管理这种类型的数据,单独的消息存储被用来管理队列和主题等专门的存储。    
  • 微服务的技术自主性是一个关键优势,允许每个微服务独立选择最适合实现其特定功能的存储技术。

确保消息的完整性需要在处理来自多个存储系统的数据时,在一个事务中执行多种数据操作。例如,当一个队列中的请求消息需要操作关系数据库中的数据并生成响应消息发送到另一个队列时,接收请求消息、操作表和提交响应消息的整个过程必须是原子的和隔离的。这就需要在一个事务中执行整个处理过程。

要想在事务中进行整体处理,需要所有相关的存储系统都支持事务并参与到联合事务中。能够参与联合事务的存储系统被称为资源管理器。此外,在一个应用程序中,可能会出现涉及来自不同组织的资源管理器的功能。涉及分布在不同地点的几个资源管理器的事务被称为分布式事务。

ACID范式也适用于分布式事务,这意味着每个资源管理器必须进行原子的、一致的、隔离的和持久的数据操作。这些影响分布式事务中的特定资源管理器的数据操作被称为本地事务。然而,分布式事务中的本地事务不一定按顺序执行。相反,它们可以交织在一起,也就是说,一个本地事务的数据操作可能发生在另一个本地事务的两个数据操作之间。不同的本地事务的数据操作甚至有可能同时发生。  

图2说明了一个使用分布式事务进行资金转移的实际例子。

假设账户_1在银行_A,账户数据存储在数据库管理系统DBMS_A管理的数据库_A,而账户_2在银行_B,其数据存储在数据库_B,并由DBMS_B管理。

一个分布式事务被用来执行资金转移,它涉及到一个由银行_A提供的借方功能和一个由银行_B提供的贷方功能。资金转移可以使用微服务来完成,其中一个转移微服务调用借记和贷记微服务。一个借方微服务将一个账户减少一个特定的金额,而一个贷方微服务将一个账户增加一个特定的金额。

反过来,借记和贷记微服务使用相应数据库管理系统的本地数据操作功能来修改账户数据。这些数据操作分别作为DBMS_A和DBMS_B上的本地事务执行。

这两个本地交事务代表了资金转移的整体分布式事务。

2.2 两阶段提交
分布式事务中会遇到被称为原子承诺的基本问题。仅仅询问每个资源管理器是不足以提交整个分布式事务的。这是因为提交处理的结果可能不是原子性的。

例如,如果有三个资源管理器参与,并向所有的资源管理器发送了一个提交,结果可能是两个资源管理器成功地处理了提交,而第三个则因为在提交处理过程中检测到错误而回滚。一个资源管理器可以在提示提交处理成功之前的任何时候单方面决定撤销一个事务。

为了在分布式事务中实现原子承诺[7],需要一个类似于著名的两相提交协议[8]的协议。该协议分两个阶段运行(因此而得名),由一个被称为事务管理器(也被称为协调器)的特殊中间件控制。

如上图3所示,实现分布式事务的程序利用事务管理器来开始事务,并通过提交或回滚来结束事务。事务管理器运行第一阶段,即投票阶段,要求每个参与的资源管理器准备在程序想要提交事务时提交其本地事务。每个资源管理器都会回答它是否能保证提交其本地事务。

一旦收到所有的回应,事务管理器就会决定第二阶段的处理,即完成阶段。 如果所有的资源管理器都能保证提交,事务管理器就会指示每个资源管理器这样做;否则,事务管理器就会指示资源管理器回滚。这就保证了原子承诺的实现(更多细节,见[5])。有几个开源的事务管理器的实现,如Seata[9]、Atomicos[10]或Narayana[11]。

了解分布式事务中的不同请求如何协同工作是非常有帮助的。

下图4显示了一个叫做μ_0的微服务,它使用其他微服务μ_1到μ_n,来完成它的工作。

事务是由μ_0请求事务管理器启动的。事务管理器发回一个事务上下文,其中有分布式事务的唯一标识符和它自己的地址。当μ_0要求μ_j做某事时,它将事务上下文与请求一起发送(作为请求消息的消息头的一部分)。当μ_j看到事务上下文时,它意识到自己是分布式事务的一部分,不能再决定事务的结果。它现在已经被分布式事务所感染了。当μ_j要求它的资源管理器进行修改时,它也将事务请求一起发送。资源管理器看到事务请求,意识到它也被感染了。

在处理μ_j的第一个操纵请求时,资源管理器从上下文中获得事务管理器的地址,并发送自己的地址以用于两相提交协议。现在,资源管理器μ_j被当前分布式事务的事务管理器所控制,只能在事务管理器的控制下结束自己的本地事务。

在实践中,有几个因素会减慢分布式事务的速度。

  • infection介入过程,
  • 两相提交协议,
  • 网络延迟,
  • 以及其他原因都会加在一起。

正因为如此,通常最好限制参与分布式事务的资源管理器(和微服务)的数量。平均来说,两到三个资源管理器应该就可以了。
当决定在实践中使用基于ACID的分布式事务时,一定要记住这一点。其他类型的分布式事务可以同时处理更多的微服务(详情请见第4节)。


2.3 数据复制
使用微服务的应用程序通常是分布式的。在云环境中尤其如此,构成一个应用程序的微服务可能分散在不同的地方,彼此之间相距甚远。一个微服务有可能无法与它正常工作所需的数据封闭。网络问题和碎片化会导致一个微服务无法访问它所需要的数据。在云环境中,所使用的硬件通常是标准的,这意味着它可能出现故障。如果发生这种情况,存储在该硬件上的数据将不可用。云环境中的数据经常被复制或拷贝,以避免这样的问题。这样一来,如果一个副本变得不可用,还有其他副本可以使用。

数据复制意味着在不同的地方复制相同的数据[7]。每当一个副本发生变化,这个变化就会被发送到所有其他的副本。这可能是立即发生的,也可能是延迟发生的,或者是一次发送几个变化。数据的使用者通常不知道复制的过程。特殊的中间件负责制作和同步数据的副本,以确保它们包含相同的数据。

在下图5中,一个叫做X的数据项被复制了n次,并存储在n个不同的位置。每当X被改变时,负责X的副本的中间件会将更新发送到X的每个副本。如何发送变化--是立即发送、稍后发送、一次发送,还是一次发送,与做出改变的个人无关。

下图5:在不同位置的复制的数据

数据复制有几个优点。下面是一些例子:

  • 拥有同一数据的多个副本可以增加其可用性。只要至少有一个副本仍然可以访问,你就可以对数据进行操作,包括阅读它。如果一些副本不可用,它们最终会再次变得可用,并且会被更新。这有助于防止网络问题或硬件故障导致的问题。
  • 几个副本有助于提高响应时间。当一个操作请求被提出时,在物理上最接近请求者的副本,例如,可以使用本地的副本。这减少了在网络上发送请求的时间,使整个数据操作更快。
  • 数据丢失的危险也减少了。如果一个副本丢失了,比如说由于系统崩溃,你可以从剩下的一个副本中恢复它。

但是,数据复制也有其缺点。
  • 数据复制需要额外的努力来将数据的副本分配到目标位置。
  • 需要额外的处理来保持数据的一致性(详情请见3.2节)。
  • 数据复制增加了保存数据副本所需的存储量。

在一个理想的情况下,一个数据项的所有副本应该同时被修改,或者至少在一个分布式事务中被修改,以确保它们总是有相同的值。这种方法涉及的处理开销(如第2.2节所述)只有在使用很少的数据项副本时才能容忍。在云环境中,同步数据复制并不总是实用的,因为网络碎片会阻碍变化传播到一个数据项的所有副本。因此,云环境倾向于使用异步复制机制,这就引入了应用程序必须考虑的新一致性模型。第3.2节对这些一致性模型进行了深入的研究。


2.4 复制的隐式使用
资源管理器或由资源管理器控制的中间件通常处理复制。应用程序可能没有意识到正在使用的复制技术。

然而,我们不能忽视复制对应用程序的影响。复制涉及分布数据的副本,保持一致性,并增加存储需求。此外,由此产生的一致性模型深深地影响着应用的设计。因此,设计者在创建应用程序时应牢记复制,以确保它们能够处理一致性模型并从复制中受益。在第3节中,我们将探讨一致性模型对应用程序设计的影响。

下图6显示了一个场景,数据库管理系统(DBMS)由多个数据库系统组成,命名为DBMS_1到DBMS_n。每当任何一个实例管理数据时,它都会自动复制到所有其他的实例中。因此,一个应用程序对任何实例的修改请求将改变所有其他的实例,而不需要应用程序的通知。因此,DBMS看起来像一个单一的虚拟系统,将数据存储在几个地方。这使得数据可以在云提供商的不同部分被访问。


上图 6:分布式数据库中的复制

3.0 高度分布式的事务
各种自主的组件,每个都使用自己的底层技术,包括数据库技术,组成了基于微服务的应用程序,这些应用程序是高度分布的。正如第2.4节所讨论的,几个可能使用复制的资源管理器,必须由整个应用来处理。当一个基于微服务的应用程序在云环境中运行时,它的组件往往在地理上变得分散。如[12]所述,网络连接中断和服务器中断可能发生,导致必须由应用程序处理的错误。

如果云中基于微服务的应用程序需要事务支持,事务将变得高度分散。如果单个微服务在单个资源管理器上使用事务,而该资源管理器与任何其他微服务的处理没有部分分离,即使底层资源管理器依赖复制的数据,经典的事务仍然可以实现。

3.1 CAP
一个理想的分布式应用应该确保其底层数据的一致视图,并在托管该应用的每个响应节点上提供其所有功能,即使不是所有托管该应用片段的节点都可用。因此,一个理想的分布式应用应该具有以下特点:

  • 一致性(C)是指一个数据项的所有副本都有相同的值,使任何响应的处理节点都能读取一个数据项的实际值。
  • 可用性(A)表示数据在任何响应的处理节点上都可以更新,确保故障不会影响整个应用提供的功能集。
  • 分区容忍度(P)假定网络连接或处理节点的故障不会影响整个应用程序的功能。

不幸的是,在一般情况下,一个应用程序不能同时拥有所有三种CAP特征。这一点最早是在[13]中提出的,但后来在[14]中被精确地表述和证明。这现在被称为CAP定理,即 "三选一"。

对于高度分布式的应用,假定必须容忍分区[15]。因此,这种应用必须在任何时候在可用性和一致性之间做出选择。选择一致性意味着,如果在某种环境或情况下知道不可能发生分区,那么应用程序除了可以容忍分区外,还可以同时具有一致性和可用性。
一个应用程序可以选择在不同的时间支持不同的功能对。此外,一个整体应用程序的不同部分可能会做出不同的选择。例如,如果一个应用程序的一个部分在同一个服务器上运行,在不需要容忍分区的情况下,它可能既一致又可用,因为它不能发生。这样的考虑导致了对CAP定理的修订[15]。

3.2 一致性模型
在复制数据的背景下,确保一致性涉及到保证在修改请求后访问数据项的任何副本将返回相同的结果。这假定所有的副本都是同时更新的,与ACID范式中的一致性概念有很大不同。在后者中,一致性指的是一个事务的数据操作集合必须遵守的一组规则(见1.2节),但它没有对操作的及时性做出说明。

随着时间的推移,复制数据的一致性概念已经演变成一个非二元的概念,有几种较弱的一致性形式被定义并在各种系统中实现。最强的一致性形式,被称为严格的一致性,确保所有的副本都有相同的值。已经定义的较弱的一致性形式,比如单调读,它保证一旦客户看到一个特定的值,对一个数据项的后续访问将不会向客户返回任何以前的值。同样,读-写保证了客户端在更新一个数据项后,总是会访问它的更新值,而不会看到旧的值。这些较弱的一致性形式被统称为弱一致性。

最终一致性是在复制数据背景下的一种较弱的一致性变体,它保证所有的复制体最终都会收敛到某个特定数据项的相同值,前提是在复制过程中不发生错误,并且在足够长的时间内不做进一步的修改。不一致窗口的持续时间,或不同副本可能有不同数值的时期,取决于各种因素,如网络延迟和副本的数量。

在分布式系统中,冲突检测和解决是复制的数据管理的一个重要方面。时间戳通常用于检测冲突,对一个数据项的每一次更新都被分配一个唯一的时间戳。当检测到冲突的更新时,会使用一个调和机制来确定并发修改的最终值。一个常见的调和政策是 "最后的作者获胜",其中对一个数据项的最新更新被认为是最终值。使用一套技术来确定并发修改的最终值的过程被称为调和。

从ACID的角度来看,最终一致性的适用性似乎很有限,但是许多应用在实践中可以在某种程度上使用不一致的数据。例如,在航班预订中,并发事务之间发生碰撞的概率很低,最终一致性可能是一个合适的一致性模型。如果一个航班被预订,而另一个预订在不一致窗口内发生,该航班将被超额预订。在这种情况下,可以采取补偿行动,如向某人提供奖励,让其乘坐较晚的航班,以解决不一致的问题,这在第4.3节中讨论过。

3.3 BASE
复制的数据强加了一致性模型,使得基于ACID的事务不切实际。相反,我们使用基于BASE范式的事务,它代表着 "基本可用"、"软状态 "和 "最终一致性"。BASE的定义并不像ACID或CAP那样明确,所以下面的解释可能有些主观。尽管如此,在这种情况下,根据资料[16, 17],这些术语可以被理解为如下。

  • 基本上可用(BA)意味着应用程序可以处理一些故障。当一个节点发生故障时,其功能和数据变得不可用,但其余的功能和数据是可用的。如果数据在故障节点和响应节点之间被复制,数据可能不同步,一旦故障恢复,就需要进行调和。
  • 软状态(S)的特点是,在一个应用程序完成后,对数据所做的改变被传播到整个环境中。这意味着所需的更新可能还没有应用到目标存储,包括对复制的更新和目前正在处理的进一步修改的 "飞行中 "请求(如下所述)。
  • 最终一致性(E)的定义同前。

让我们进一步分解软状态的概念。图7说明了一个叫做X的数据项被复制到多个地方。当请求对X进行修改时,复制中间件(图7中顶部虚线所示)接收请求,并将其作为消息放入一个消息队列。该队列作为每个本地复制组件的输入队列。这些复制组件然后从它们的消息队列中接收更新请求,并相应地更新它们的本地复制。然而,由于不是所有的本地复制组件都会同时处理它们的更新请求,所以有一个时间窗口,在这个窗口中,一些副本会有新的值,而另一些副本仍持有旧的值。但陈旧副本的新值在相应的消息队列中,这些陈旧的副本最终会被更新,实现最终的一致性。

下图7:懒惰复制产生的软状态


这样一来,所有副本的整体状态都是 "软 "的,新值分布在那些已经更新的数据项的数据存储中,而消息队列则以更新请求的形式持有尚未更新的数据项的新值。需要注意的是,那些打算在数据存储中成为持久性的但尚未被硬化的信息被称为 "飞行中的信息"。在这个术语中,排队的更新请求被称为飞行中的请求。

下图8显示了BASE范式在实现两个账户之间的资金转移中的使用。


和以前一样,账户_1由DBMS_A的数据库_A中的银行_A管理,而账户_2则由DBMS_B的数据库_B中的银行_B管理。
转账是通过三个微服务实现的:

  • 一个借记微服务,将账户减少一个给定的金额,
  • 一个贷记微服务,将账户增加一个给定的金额,
  • 以及一个转账微服务,使用借记和贷记微服务来实现同步和异步请求的混合转账。

如果资金转移是从账户_1到账户_2,转移微服务立即通过同步调用借方微服务减少账户_1的金额。 它还将一个相应的信贷请求作为消息放入信贷微服务的输入队列。 信贷微服务在任何方便的时间处理其输入队列中的信贷请求消息,这意味着账户_2的增加可能会在以后发生。 因此,在信贷请求尚未被处理期间,受影响账户的金额没有反映资金转移。然而,飞行中的信贷请求确保最终两个账户都会反映资金转移的语义,实现最终的一致性。 通过这种方式,资金转移的整体状态在该时间段是 "软 "的。

将信用请求消息放入队列也是一个同步动作。一旦消息被传递到队列中,投放请求就完成了。请注意,借记微服务的同步调用和信贷请求在队列中的同步放置都是在一个经典的分布式事务中进行的。这个事务是基于ACID的,并且受到两相提交协议的保护,在第2.2节中已经讨论过。这个事务的目的是确保在没有发送增加帐户_2的请求之前,帐户_1永远不会减少。同样,从信贷微服务的输入队列中检索信贷请求消息,并通过数据库操作增加账户_2也是在这个分布式事务中进行的。该应用实际上是通过这两个经典的分布式事务实现了资金转移,并保证了银行之间的信息转移。

基于微服务的应用程序和云环境使用确保消息完整性的技术,将从队列中获取消息或将消息放入队列,并使用分布式事务进行相关的数据操作。这种技术保证了请求数据操作的消息和相关的数据操作本身总是被原子化处理,从而保持了数据存储中由消息和相关数据代表的飞行信息的完整性。由于只有两个资源管理器参与这样的分布式事务,所以它是一种基于微服务的应用和云环境的实用方法。

3.4 并发控制
拥有同一微服务的多个实例对于确保可扩展性、弹性和可用性是必要的。这些实例将一起工作,处理进来的请求。同一微服务的多个实例可能会在同一个消息队列中竞争请求,这被称为竞争消费者[18]。然而,在某些情况下,这可能会导致并发性的问题,如丢失更新或不一致的读取[7]。例如,如果微服务使用的数据没有自己的并发控制功能(如文件或HTTP资源),微服务的两个实例可能试图同时更新相同的数据。

为了避免并发性问题,应用程序控制对数据的并发访问。像锁定或时间戳排序[7]这样的标准技术被用来管理并发访问。锁定在实践中更常用[19],它有两种形式:悲观的锁定和乐观的锁定。悲观锁是在预计冲突经常发生时使用的,而乐观锁是在预计冲突很少时使用的。

悲观锁涉及到一个应用程序在访问一个数据项之前获得一个锁。获得的锁必须与应用程序的访问意图相对应。如果数据项已经存在一个兼容的锁,例如一个现有的读锁与请求的读锁相匹配,则允许访问。然而,如果存在一个不兼容的锁,例如一个现有的读锁与请求的更新锁不匹配,则拒绝访问。应用程序检查现有的锁,验证其兼容性,并根据结果采取适当的行动。另一方面,乐观锁在操作一个数据项之前不获取锁。相反,它在不加锁的情况下检查冲突。例如,客户端在更新HTTP资源时可以使用条件性请求[20]。客户端比较条件的值,如实体标签或最后修改的时间戳,这些都是缓存的或通过先前请求获得的。

4.0 基于补偿的事务
为了支持基于云的应用程序中的事务,开发人员通常必须在应用程序本身中实现相应的工作单元。一个微服务的更新可能会被冲突解决所抹去(如3.2节所讨论的),或者锁冲突(如3.4节所讨论的)可能会导致属于一个工作单元的单个微服务的回滚。因此,工作单元的概念是危险的,因为被分组的功能可能会遇到故障,而这些故障只有在其他功能成功完成后才能被发现。此外,工作单元的步骤可能会操作资源管理器不保护的数据,也就是说,它们的操作可能不会被简单地要求撤销。在这种情况下,开发者可以通过使用基于补偿的事务来建立工作单元。

4.1 回滚背后的基本原则
为了撤销已经应用于数据项的更新,当一个事务需要回滚时,经典的数据库系统[6]使用持久化的日志记录来存储更新前的数据项的修改值。在回滚过程中,一个被称为(事务性)恢复的过程,数据库系统只是将修改过的数据项的 "前映像 "资源化。为了避免并发冲突(见第3.4节),数据库系统代表一个事务对修改过的数据项持有锁。数据库系统代表一个事务对修改后的数据项加锁,防止其他事务访问这些数据,直到事务结束。这确保了其他事务永远不会看到 "脏更新",即后来会被撤销的中间更新,并且不会根据这些数据做出任何决定。

事务的回滚确保世界被恢复到之前的状态,就像该事务从未发生一样。在经典事务中,回滚事务的影响是完全撤销的,代表了一个基本原则。然而,基于补偿的事务违反了这一原则(如4.3节所讨论的),事务的影响不能完全撤销,甚至在事务回滚后,一些影响可能仍然存在。

4.2 补偿的曙光
"长期运行 "的事务会通过降低并发性和增加延迟对系统的性能和可扩展性产生负面影响。为了避免这种情况,事务应该被设计成尽快完成它们的工作并释放它们持有的任何锁。我们还可以将工作分解成更小的事务或子事务,每个事务都能快速完成并释放其锁。这可以减少锁的持续时间,增加并发性。

为了帮助解决长期运行的事务,专家们推荐了几种方法。一些方法不使用锁[7],通常是由数据库系统做出的。其他的则被记录为模式[19],供应用程序使用。例如,如果一个应用程序想一次更新大量的数据项,它可以把它们分割成较小的集合,称为 "迷你批",可以在一个事务中处理。每个迷你批事务都有一个计数器,以跟踪最后更新的项目。接下来的事务会读取该计数器,并继续处理该小批。这种方法被称为 "链式事务"方案。

如果这个 "链式事务 "中的一个小批次不能被处理,原子性就会被破坏,所有之前的小批次的更新都必须被撤销。但是,由于这些更新已经被提交,它们不能通过恢复预映像来自动撤销。为了解决这个问题,应用程序必须使用补偿逻辑[21]来逆转之前的更新。这个逻辑被实现为另一个链式事务。

4.3 语义恢复
正如第4.1节所讨论的那样,事务性恢复涉及恢复预图像,是一个 "机械 "过程,不知道任何特定于应用程序的逻辑。相反,语义恢复应用特定于应用程序的补偿逻辑来回滚一个事务。这是因为有必要了解应用程序的语义来撤销一个失败的事务。回滚一个链式事务是语义恢复的一个例子。依靠补偿逻辑的事务被称为基于补偿的事务。请注意,应用程序的程序员不需要采取特殊的行动来实现在事务恢复期间恢复预图像,这可以在所有应用程序中统一执行。相比之下,他们必须明确地提供实现语义恢复的补偿逻辑的程序。因此,使用语义恢复的事务被称为基于补偿的事务。

在我们的资金转移例子的基础上,我们将说明语义恢复是如何在这种场景中被利用的,如上图9所示。一般来说,资金转移是由第三方使用转移微服务异步进行的。转账微服务通过将借记请求消息放入银行_A的借记微服务的输入队列,以及将贷记请求消息放入银行_B的贷记微服务的输入队列,来启动一个分布式事务。然后该事务被提交。如果将一个请求信息放入任何一个队列都失败了,另一个信息就会从其队列中移除,事务就会回滚。
通过这种方式,账户_1和账户_2都不会受到影响,并且一致性得到了保留。假设事务成功提交,借记和贷记微服务随后将从其输入队列中接收相应的请求消息并更新目标账户。这两个微服务将向转账微服务(图9中未显示)发出成功信号,导致资金转移成功。

如果借记请求失败,因为Account_1会超过它的信用额度,那么信贷微服务仍然会处理信贷请求并增加Account_2的余额,这违反了一致性。为了解决这个问题,必须撤消对Account_2的增加。借记微服务将向转账微服务发送一个故障消息,表明借记请求的失败。转账微服务将启动另一个事务,并向银行_B的借记微服务发送一个新的借记请求,以减少帐户_2的余额。这将重新建立两个账户余额之间的一致性。值得注意的是,消费故障消息和提交新的借记请求是独立的事务。
上图10显示,语义恢复似乎遵循了恢复的主要原则,因为它没有借记账户_1或贷记账户_2,使世界处于与事务前相同的状态。但是,如果在临时贷记和最终借记之间的时间里,账户_2被另一个事务减少,系统中可能会出现不一致的情况。因此,语义恢复在确保所有情况下的完全一致性方面可能有局限性。

下图11显示了语义恢复的局限性的一个例子。假设Account_2在信贷微服务添加100个单位之前有300个单位,导致余额为400个单位。然后,另一个事务将余额减少了400个单位,使账户_2的单位为0。最后,借记微服务试图将账户减少100个单位以补偿失败的借记,但余额变成了负数(-100个单位)。结果,产生了一个透支费用,账户持有人必须支付该费用。
在实践中实施语义恢复时,必须考虑基于补偿的事务,如语义恢复,通常不坚持恢复的基本原则(如第4.1节所讨论的)的限制。

同样重要的是要考虑到,如果对Account_2的补偿行动失败了,比如因为Bank_B的政策不允许透支账户而导致扣款微服务失败,那么事务的整体一致性就完全被破坏了。这需要采取更复杂的行动,甚至可能需要人工干预。为了简化这个过程,从业人员通常使用,补偿行动不会失败。虽然这看起来不现实,但它与数据库技术中的假设相似,如果恢复失败,必须进行复杂的处理。因此,必须考虑赔偿失败的可能性,并制定应急计划来处理这种情况。第4.4节中讨论的工作流技术在这种情况下可以发挥作用。

许多应用程序使用基于补偿的事务。在一个领域中存在一对函数,其中一个函数补偿另一个函数,例如借记和贷记、预订和取消、投放和获取、封锁和释放等,被称为补偿对[22] 。实现这些对不需要额外的实施工作,因为它们在应用中非常普遍。中间件,如工作流系统,支持这些事务,甚至可以自动进行语义恢复(见下一节)。


4.4 补偿步骤流程
图12显示了补偿对的最早的系统使用之一,即SAGA[23]概念。

SAGA是一个补偿对(Si,Ci)的集合,其中每个步骤(Si)及其相关的补偿函数(Ci)都是作为一个经典的事务来实现的。当一个SAGA被执行时,它按顺序运行步骤S1,S2,...,Sn。如果没有错误发生,SAGA成功完成。在出现错误的情况下,例如,如果步骤Sk失败(并因此回滚),已经成功完成的步骤S1,S2,...,Sk-1将通过调用其补偿函数以相反的顺序撤销,即Ck-1,...,C1将被执行。这意味着SAGA被回滚,可以重试。请注意,SAGA继承了上面勾画的问题,也就是说,它们违反了回滚的基本原则,并假定补偿不会失败。


在实践中,应用程序需要比SAGA模型中看到的严格的执行步骤顺序更多的灵活性。需要典型的编程语言支持的控制流结构,如分支、循环等,还需要并行执行。例如,当一个微服务向其他微服务发送消息时,这些微服务可以独立处理这些消息,就像我们前面的场景一样。在这种情况下,有些步骤可能不需要补偿动作,因为不需要撤销它们,如图12中的S1。其他一些步骤可能被分组,其语义是:如果其中一个步骤失败,在它之前的所有已完成的步骤都将得到补偿。这一组被称为补偿球[24]。默认情况下,当补偿球体被回滚时,包含在球体中的控制流图将被反转,在遵循反转的控制流图时,它们的补偿动作将被执行,而不是步骤。请注意,这是对SAGA的直接概括。此外,一个球体,如图12中的S,可以有自己的补偿动作与之相关,如图12中的C。当球体内发生错误时,可以通过运行与球体本身相连的补偿动作来补偿,称为积分补偿[24],或者通过运行球体内包含的步骤的补偿动作来补偿,称为离散补偿[24]。甚至可以决定在控制流离开球体后回滚补偿球体,比如在以后的时间点上检测到错误时。这些控制流结构被称为微流--这些将在另一篇论文中讨论。



上图13强调了微流是一种被称为工作流的业务流程,它可能涉及到人的互动和中断[22]。例如,如果一个补偿行动失败,可以通知人类纠正错误,工作流程可以恢复。工作流语言,如BPEL[26]或BPMN[27],做了微小的调整以支持补偿范围。与图13类似的BPMN的图形表示见图14:


5.0 总结
在这篇白皮书中,我们讨论了ACID范式,它是经典事务的核心概念,以及它与控制范围的想法的关系。然后我们概述了使用多个资源管理器的事务的基本问题,特别是原子承诺,以及两相承诺协议如何解决这个问题。我们还研究了数据复制和它对事务概念的影响。我们还回顾了在云原生架构和微服务的推动下,高度分布式应用的崛起。这导致了对新的一致性模型的需求,该模型依赖于BASE范式,反映了CAP定理。因此,必须牺牲ACID属性,特别是在云原生和基于微服务的应用中。为了使一个合适的工作单元概念能够支持适当的恢复,我们分析了回滚的基本原理。我们引入了语义恢复作为适合高度分布式和长期运行的应用程序的概括。我们还讨论了作为语义恢复基础的补偿对,以及基于补偿球体的应用程序的部分回滚。所有这些概念共同建立了基于补偿的事务的概念。

综上所述,关于微服务环境下的事务的主要发现如下。

  • 对单个微服务使用经典的、基于ACID的事务是一种合适的方法。
  • 在受两相提交协议保护的分布式事务中,让两个基于微服务的应用的资源管理器参与进来是可行的、实用的。
  • 基于微服务的应用程序,特别是在云中,必须通过仔细考虑一致性和可用性来有效地处理软状态。
  • 如果需要将几个微服务组合成一个工作单元,有必要将相关微服务的功能实现为补偿对。
  • 基于补偿的事务概念适用于基于微服务的应用