Skip to content

Latest commit

 

History

History
131 lines (73 loc) · 17.8 KB

compensation-and-tcc.md

File metadata and controls

131 lines (73 loc) · 17.8 KB

一种基于补偿的事务处理机制(TCC)

介绍

       随着Web服务的成熟和广泛使用,涉及到Web服务的安全、事务和稳定性相关的技术和协议就变得愈发重要。本文重点关注Web服务的事务,尤其是长时间运行事务的概念以及对Web服务实现者的影响。我们将研究事务和补偿如何满足Web服务实现者的业务模型,以及架构需要得到哪种程度的支持。

本文是翻译和总结Guy Pardon的论文《Business Transactions, Compensation and the Try- Cancel/Confirm (TCC) Approach for Web Services》,该论文没有涉及数学公式,所以读起来比较简单。在翻译的过程中,会增加笔者自己的所得。

如果是笔者的理解将会以 self-think来开头,以引言的形式来展示。

定义

       在我们开始之前,让我们确保就一些重要的术语达成一致。

       事务:就本文而言,事务是一组相关的操作(或交互),它们可能由于(部分失败导致的)回滚,而需要在执行后取消。这些操作可以位于不同的地理位置,例如:Internet上的不同Web服务。

self-think 一个Web服务可能会聚合多个Web服务来完成一个具有业务含义的操作,如果对于用户而言,需要保证要么成功,要么失败,那么这些不同实现者提供的Web服务需要在一个事务语义下得到保证。

       长时间运行的事务(或业务事务):就本文而言,长时间运行事务的总持续时间超过单个操作持续时间几个数量级。由于长时间运行事务的持续时间很长,因此导致该类型的事务进行撤销时,会在相对较长的时间内取消操作。

self-think 事务除了可见性的要求,还有完整性的诉求,也就是由多个Web服务聚合的一个事务需要这些Web服务都能够一并完成,所以该事务的持续(或消耗)时间会比单个Web长很多。

       示例:图1展示通过Web服务来预定旅行的航班,预定的路线是从布鲁塞尔到多伦多,路线是由两个航班组成的,这两个航班是从布鲁塞尔到华盛顿和从华盛顿到多伦多。如果这两个航班的预定是由两个航空公司提供,由两个Web服务在不同的系统中来实现。

       如果第二个操作调用失败,那么我会被只有一张从布鲁塞尔到华盛顿的机票卡住(这当然很有趣,但它违背了我去多伦多旅行计划的目的)。在这种情况下,取消去往华盛顿的票可能是一个有效的选择,但是,在我决定这样做之前,我可能一直在寻找替代方案,例如:两小时左右后的另一次从华盛顿到多伦多的转机。而这可能会导致非常长时间运行的整体事务。

       请注意,这里的关键点是在我决定取消第一张票时已经进行了预订。也就是说,航空公司预订系统已经接受并验证了我的第一次预订,并且至少会保留我的座位一段时间。如果没有取消事件,那结果就是:要么我付款了,白订了一张票,要么航空公司会因不需要的预订而赔钱。无论哪种方式,都会有人赔钱。因此,取消事件是有很大的驱动力(或者说价值)。

       纯粹从技术上讲,取消不同位置的不同操作并不是什么新鲜事:几十年来,它一直由称为事务管理器或事务服务的东西完成。CORBAOTS(对象事务服务)就是这种技术的一个例子,J2EE的Java事务API和Java事务服务(JTA/JTS)是另一个例子。

self-think 这里提到的取消在技术上一般称为回滚,但是这种回滚往往在业务语义上是有描述的,比如:在分支逻辑中进行了描述,如果订购不到第二张票该如何处理。因此对于产品设计而言,考虑容错性和面向失败设计,对于产品的用户体验和技术实现都会获得更好的结果。

在技术实现过程中,面对分布式事务的问题,往往是开发比较关注的,会花费更多的时间加以实现,保障其可靠性。此时产品需要明白这种局限性和挑战,在产品设计中就能够针对事务中的断裂(权且这么叫)进行贴心的设计,这样会使产品更加的贴心,同时也会让开发人员避免毫无依靠的野战。

       这些技术都是基于ACID事务的概念。ACID事务要求的工作方式如下:事务访问的所有资源都被当前事务锁定,而其他并行事务无法访问被锁定的资源,直到事务提交(保存更改)或回滚(取消更改)。图2对ACID事务的锁定时间进行了说明。

       对于长时间运行的事务,这并不是我们所期望的,因为只有在所有资源都保持锁定的情况下才能取消。在Web服务这种场景下,长时间的锁定资源是不能满足要求的,原因有以下几点:

  • Web服务在取消之前可能需要很长时间的延迟,尤其是在涉及人工决策的情况下。这将意味着较长的锁定时间,从而导致资源的不可用时长变长;

  • Web服务可能涉及彼此不认识或不信任的各方,而长时间的锁定资源会导致它们容易受到DDoS攻击。

       如果ACID和回滚对我们没有帮助,那有什么办法来解决这个问题吗?您可能已经猜到,一个可能的答案是补偿。

       基于补偿的方法不是将长时间运行的事务实现为一个巨大的分布式ACID事务,而是将每个Web服务调用视为一个(耗时很少的)本地ACID事务,在它执行后立即提交。以丧失ACID事务在数据层自动回滚能力为代价,但是大大的减少了各方锁定资源的时间,这意味着取消(或者概念上的回滚)必须以不同的方式来完成:也就是通过执行一个单独的本地ACID事务,如果整体成功则事务生效,否则该事务将在逻辑层面上完成取消。

self-think 业务操作需要联立多方资源,而传统的分布式事务处理方式是对所有资源进行加锁,导致对资源占用时间的延长,随之带来的出错会使得一致性收到挑战。补偿的方法是让在一定程度上降低事务的一致性级别,让业务层承担数据一致性的挑战,将大块的锁定资源方式变为离散且短小的锁定方式,反而在可用性上得到提升,最终也兑现了(弱)一致性。

       补偿:就本文而言,补偿被定义为一个单独的ACID事务(在Web服务提供者本地),它通过业务逻辑的取消操作来代替旧有的ACID操作。

       基于补偿的方法将数据库锁定时间保持在最低限度,从而避免了前面提到的所有缺点。让我们看看它是如何工作的。

       再次考虑航班预订的例子。预订飞往华盛顿的航班本身就是一项ACID事务,在各个航空公司系统中是本地的,并且会立即提交。当需要取消时,第二个ACID事务将在逻辑上撤消预定(也就是反向操作已经创建了的预定),由此产生锁定资源的时间要短得多。

       这是如何工作的?这取决于应用程序,应用程序开发者负责规范他们需要做什么。例如:取消航班预订可以通过不同的方式完成,我们可以删除预订记录(不留下任何痕迹),或者我们可以明确地将其标记为已取消。无论哪种方式都能够完成概念上的回滚,而选择哪一种是这个Web服务实现所属的业务来决定。

       一般来说,我们有两种类型的补偿:

       完美补偿:反向逻辑清除了原始的所有操作痕迹,也就是我们完全删除预订的情况。

       不完美的补偿:如果反向逻辑留下可检测的原始痕迹,就会发生这种情况。例如:如果我们将预订记录的状态明确更新为“已取消”状态。

self-think 如果是业务层进行取消操作,那么在Web服务中,连续调用两个航空公司的Web服务是不是也是一样的?理论上是一样的,但前提是调用任意一个Web服务都不能出错。

两个连续的Web服务调用,一旦调用第二个Web服务出现错误、超时或者异常逻辑时,将无法保证事务,一旦出错就没有一个协调者能够将剩余的补偿逻辑进行执行。因此从稳定性上看,在完成补偿逻辑的触发上,一定是需要一个旁路系统进行支持的。

以业务角度去看补偿

       现在我们已经了解了补偿的概念,让我们看一下它是如何来支持业务。当我们面对服务提供者的业务模式时,可以确定两个主要的不同类型。

       类型1:补偿只是另一种业务逻辑

       在这种情况下,就服务提供者而言,补偿并没有什么特别之处:补偿任务也可以是常规业务交互。特别是,Web服务不需要提供额外的服务实现来进行补偿。

       这种类型业务的一个例子是股票经纪:如果您购买股票,那么您可以通过之后再次出售来补偿它。当然,在过去几年进行投资的每个人都知道,这种补偿模式并不完美:买入和卖出的股票价格仍然相同的可能性非常低。这意味着要求补偿的客户要么赔钱,要么赚钱。

       尽管如此,这种补偿还是很有趣的,因为服务提供者不必担心补偿逻辑。它是最简单的一种基于补偿的系统,其中服务提供者是无状态的。

self-think 这种类型一般可以在两个不相关的系统交互时可以看到,A系统创建一条数据,然后向B系统进行调用,创建一条B系统中的数据,且这个操作是用户发起的。当A系统操作成功,而调用B系统失败时,只需要将对B系统调用的逻辑放置在后续操作的检查中(即没有调用则调用之),那么可以依靠用户在第一次出错后,返回到页面,再次触发来解决这个问题,其实质就是依靠用户操作来完成补偿,系统不维护状态,只是必要的检查和逻辑处理。

       类型2:补偿是同一业务的第二阶段

       在这种情况下,补偿是同一业务事务的明确第二阶段,涉及自定义逻辑以及要保留业务事务状态的编码。机票预订示例属于此类(与我们刚刚看到的股票经纪人示例进行对比)。我们可以将这种方法称为TRY-CANCEL/CONFIRM(TCC) 方法。TRY代表正常的业务逻辑,例如:预订座位。CANCEL表示通过补偿机制对正常业务逻辑生成的数据进行的取消,例如:根据预定编号取消座位。另一方面,如果不需要取消,则CONFIRM会被执行。CONFIRM也是一个ACID事务,用于更新数据库中的状态,例如:根据预定编号将预定转为确认。

       为什么要确认CONFIRM?要理解这一点,我们需要为此在业务生命周期中保留这种状态。首次进行预订时,其业务状态为PENDING。对于供应商(航空公司)而言,这意味着预订尚未最终确定,换句话说:仍可以根据客户要求取消预订。这种状态对于航空公司的报告和预订服务很重要:它不应该在最终时间来临时,(还)保留预订。

       如果航空公司愿意,它也可以设定一个超时时间,之后预定会在超时时间到达时自动取消。如果在没有确认的情况下进行预订,则可能会发生这种情况。如果CONFIRM一旦被触发,Web服务就会知道预订是永久性的并且可以向客户收费。

       TCC方法非常适用于已经分为两个阶段的业务模型,例如:以某种方式进行预订的业务模型,当然服务提供者需要公开额外的接口来处理CANCELCONFIRM事件。

对长时间运行事务和补偿的架构支持

       现在我们可以讨论一些基于补偿系统的架构含义,在前文中的两种类型需要进行分别讨论。

       类型1:补偿只是另一种业务逻辑

       如果补偿可以被视为正常业务的一部分(如股票经纪人的例子),那么股票经纪人就没有什么可担心的了。任何股票购买都会通过后来的销售得到补偿,股票经纪人可能会从两者中赚取佣金。绝对不涉及商业风险,因此这是最灵活的补偿案例。

       通过诸如业务流程执行语言标准 (BPEL) 之类的常规工作流技术,对这种补偿的架构支持是完全可行的。可以使用远程工作流引擎对整个任务执行进行建模和执行,而购买股票只是整个过程中的一个步骤。如果需要取消流程(或长时间运行的事务),则工作流引擎会发送业务请求以出售其先前获得的股票,这里不需要其他任何东西。此外,股票经纪人并不关心补偿需要多长时间,仅仅因为对他(或她)来说这只是另一项单独的商业交易:不需要经纪人特定的状态将补偿与原始状态联系起来。

       类型2:补偿是同一业务的第二阶段

       对于TCC方法,实际情况将会变得复杂。特别是TCC所服务的业务方几乎不可能允许远程工作流承担其补偿的全部责任,因为在TCC过程期间存在隐式资源预留(或者说占用),而TCC事务的状态管理属于服务提供者。

self-think 或者说属于服务提供者(实现者)所依赖的某些支撑型系统,比如提供TCC能力的中间件。

       比如在航班预订的例子中,只要预订仍处于PENDING状态,就会将座位处于已预订的状态,在此期间,其他潜在客户无法预订。因此这个过程需要多长时间是非常重要的,并且业务方不太可能允许远程工作流引擎一直保持未决的状态。这意味着将有一个超时,在此之后预订服务会自动取消预订。一个合乎逻辑的结果是预订服务至少需要知道如何它取消,换句话说:取消逻辑与预订服务位于同一系统中。触发取消最有可能通过带有交易令牌(ID)的事件发生,因为预订已经生成,这同样适用于CONFIRM逻辑。

       就像业务模型一样,最终的架构遵循一个两阶段的协议:TRY 是第一阶段,而第二阶段包括取消请求CANCEL或确认请求CONFIRM。这些要求可以推知,支持TCC的系统需要一个两阶段事务管理器,服务提供者可以使用这样的装置来自动化其TCC事务的状态管理。

       工作流引擎也可以使用这样的装置来自动化向所有相关站点发送CANCELCONFIRM请求的过程。例如,在BPEL引擎中,事务管理装置可以检测所有远程调用,从而了解分布式任务中的所有参与者。当CANCEL事件发生时,事务管理装置会联系每个参与的Web服务并要求它取消其部分工作。如果我们牢记每个工作流步骤都可以有它的补偿,那么这将减少大约50% 的工作流建模复杂性:而不是将每个补偿显式建模为工作流逻辑中的一个步骤,事务管理装置会跟踪哪些站点需要补偿并在需要时处理取消。这样的话,所有补偿步骤都可以被排除在工作流逻辑之外,而工作流会因此变得更加简单。

结论

       正如我在本文中试图展示的,基于补偿的服务模型基本上有两种主要类型:股票经纪人类型(无状态服务)和航空机票预订类型(有状态保留类型,TCC)。对于前者,工作流就足够了。对于后者,两阶段协议更适合。