前言分布式CAP理论应该是众所周知的,它描述了一致性(C)、可用性(A)、分区容错性(P)之间的一系列权衡。很多时候,我们不得不在一致性和可用性之间做一个权衡,而分布式事务就是在这个大前提下尽可能的达到一致性的要求。目标小,问题大,方法不一。《如何在微服务中实现分布式事务?》一般来说,当我被问到这样的问题时,我会回答“尽量避免使用分布式事务”,这也是MartinFowler推荐的。但现实总是残酷的。微服务拆分后,分布式事务是一个非常硬核的需求,绕不开。我们还是得想办法解决。然而,分布式环境错综复杂,并伴随着网络状况导致的超时。如何让交易达到一致状态是非常困难的。分布式事务由一系列小的子事务组成。这些子事务和大型分布式事务一样,也遵循ACID的原则。在一致性的属性上,按照达到一致性之前的时间,分为强一致性和最终一致性(BASE)。注意,对于子事务,这里有个小误区。不仅处理数据库的操作被称为事务。在微服务环境下,如果通过RPC调用另一个远程接口,引起相关数据状态发生变化,这个RPC接口也称为事务。因此,在分布式事务中,我们将这些子事务涉及的操作称为资源。当操作正常完成时,根本不需要额外的处理。事务主要处理异常发生后的流程。接下来,我们来看看常见的分布式事务解决方案。一阶段提交(1PC)我们先来看最简单的事务提交情况。如果您的业务只需要协调一个资源,那么可以直接提交。比如使用数据库,可以直接使用begin、commit等命令完成事务提交。在Spring中,通过注解可以完成这样的事务。如果发生嵌套事务,其实现方式本质上是通过ThreadLocal向下传递。所以如果你的应用程序有子线程相关的事务需要管理,它就做不到。我们再来看看分布式事务。所谓分布式事务,就是协调两个或多个资源,达到联合提交或联合失败的效果,即分布式ACID。两阶段提交(2PC)在一阶段提交概念的延伸下,最简单的分布式事务解决方案就是两阶段提交。两阶段提交并不是说有两个参与资源,而是有两个分布式协调阶段,可能有多个资源需要协调。|重要的参与者如下:coordinator,也就是我们需要建立自己的事务管理器,通常整个系统中只有一个事务参与者(participants),也就是我们所说的资源,通常会有多个,否则不能称之为分布式事务|2PC(twophasecommit)广义的流程分为哪两个阶段?客户端分布式事务发起者提交-请求/投票准备阶段提交/回滚提交或回滚准备阶段,也称为投票阶段。所谓投票,就是参与者通知协调者自己的资源是否可以提交(意思是准备好了),或者取消事务(比如发生异常)。这次投票比较有意思。只要有一个参与者返回false,就需要终止事务,然后执行rollback。只有所有票数都通过,commit才会正常。协调者将结果传达给所有参与者的过程是第二阶段。两阶段提交其实很好理解。你可以把每个参与者的执行看作是一个普通的SQL更新语句。它们挂在那里,等待协调器给出准确的提交或回滚消息,然后它们就会正常进行。|问题如下:阻塞问题。两阶段提交最大的问题是它是一种阻塞协议,效率低下。如果协调器永久失效,某些参与者,将永远无法完成其事务的单点故障问题。由于协调器在整个过程中起着非常重要的作用,一旦发生单点故障,整个系统将变得不可用,这是一个无法容忍的事务完整性问题。在某些情况下,比如协调器发送commit命令后,出现异常,部分执行成功,会导致整个事务不一致。因为能不能提交在第一阶段就确定了,第二阶段只是通知。死也要臣服于我。并非所有资源都支持2PC(或XA)。对于第三点,我们举个例子。例如,您所有的提交请求阶段都返回是,然后协调器发送一个提交命令。但是此时,一台服务器A宕机了,无法执行commit。这时,我们的客户端也会收到一条成功信息。A重启机器后,必须能够恢复并继续执行commit命令,这在工程中是必须要处理的。|Framework2PC也称为XA事务,MySQL等大多数数据库都支持XA协议。在Java中,JTA(不是JPA)是XA协议的一个实现。Spring也有JTA事务管理器:Atomikos和bitronix实现了JTA,他们只需要提供jar包即可。实现了XA协议的数据库或者消息队列,已经具备了准备、提交、回滚等各种能力,可以在seata等框架中使用,需要启动一个独立的seata服务协调器节点。seata使用的AT,在外部事务管理器的帮助下,在概念上类似于三阶段提交(3PC)中的XA。与两阶段提交相比,三阶段提交最典型的特点就是增加了超时机制。当然,3stage证明它有3个stage,区别更显着。它本质上只是2PC的一些改进,所以完全充满了2PC的影子。|重要玩家3PC和2PC是一样的。3PC流程比2PC多了一步,就是查询阶段:CanCommit查询阶段PreCommit准备阶段DoCommit提交阶段提交阶段无非是发送commit或者rollback命令,重要的处理还在准备阶段,3PC拆解itinto2.注意下面的对应关系。2PC和3PC都有准备阶段,只是功能不同。3PC2PCCanCommitcommit-request/votingPreCommitDoCommitcommit3PC的query阶段对应2PC的preparation阶段,即询问参与者是否准备好,只是在执行过程上有些差异。你为什么要这样做?因为2PC是高效的。2PC的执行过程被阻塞。资源进入准备阶段后,必须等待所有资源就绪后才能进行下一步。在这个过程中,他们对大局一无所知。比如有ABCDE等5个参与者,E其实就是一个有问题的参与者资源。但是2PC每次都会执行ABCD预提交。问E的时候,发现有问题,然后依次执行ABCD和其他参与者的回滚。在这种情况下,ABCD进行了无用的事务预处理和回滚,这是一种资源浪费。3PC通过拆分这个查询阶段,只有在所有参与者都健康的情况下才会发起真正的交易处理,在效率和容错性上更胜一筹。从概率上来说,由于commit前的粒度变小了,commit阶段出问题的概率也变小了,可以省去很多麻烦。另外,3PC引入了超时机制。在PreCommit阶段,如果超时,则认为失败;在DoCommit阶段,超时会继续执行。但无论如何,整个业务不会永远等待。|问题3PC理论上更好,可以避免阻塞问题,但是多了一个网络通信。如果参与人数比较多,网络质量比较差,这个开销是非常可观的。它的实现也比较复杂,在实际应用中,并不过分。3PC并不完美,因为PreCommit阶段和DoCommit都不是原子的。和2PC类似,仍然存在一致性问题。TCCTCC是弹性交易,而以上都是刚性交易。有时,一个技术问题可以通过业务建模来实现。2PC和3PC在概念上看似简单,但是在分布式环境下,考虑各种超时和宕机问题,如果你想透彻,真的会害死你。2PC的框架挺多的,但是3PC找遍了全网,发现几乎没有一个大家熟知的实现。别难过,我们有更容易理解、更直观的分布式事务。那是TCC,一款2007年份的葡萄酒。TCC就是大家熟知的补偿事务,是互联网环境下最常用的分布式事务。它的核心思想是:对每一个操作,准备一个确认动作和对应的补偿动作,一共3个方法。与其依赖数据库,不如依赖自己的代码!2PC和3PC都与数据库紧密相关,而TCC是coder的最爱(也就是说,你需要写更多的代码)。如上图,TCC也分为三个阶段,但是很粗略:try阶段:尝试锁定资源confirm确认阶段:尝试提交锁定资源cancel取消阶段:其中一个链接执行失败,以及会发起一个事务取消动作这三个阶段好像是一种2-phasecommit?完全没有。但是他们的过程是可以比较的。TCC2PCTrybusinesslogicConfirmcommit-request/voting+commitCancelrollback从上面可以看出,2PC是事务流程的一个划分,TCC是对正常情况和异常情况提交的补偿。与传统代码相比,try和confirm的结合才是真正的业务逻辑。TCC很好理解,但是有一个很大的前提,就是这三个动作必须是幂等的,对业务有一定的要求。以转账为例,尝试是冻结金额;confirm即完成扣款;取消就是解冻。只要对应的订单号一致,多次执行是没有问题的。由于TCC事务的发起者可以直接在业务节点完成,所以与TCC代码在同一个地方。因此,TCC不需要额外的协调器和事务处理器,它可以存储在本地表或资源中。是的,它也需要记录一些信息,即使是在HashMap中,不然它会根据什么回滚呢?|问题TCC事务需要更多编码和正确划分尝试和确认。由于没有中央协调器,不需要阻塞,因此TCC具有高并发性,被广泛应用于互联网服务中。团队必须能够设计TCC接口,将其拆分为正确的Try和Confirm阶段,实现业务逻辑的分类。|FrameworkByteTCC、tcc-transaction、seata等。SAGASAGA也是flex事务。Saga的历史比较久远,可以追溯到1987年的一篇论文,可以说是老酒了。它主要处理长寿命事务,但不保证ACID,只保证最终一致性。所谓long-livedtransaction,可以分解为交错分布的子事务,通过消息协调一系列本地子事务,实现最终一致性。我们可以将SAGA编排器视为一个状态机。无论何时处理一条消息,它都能够知道要执行的下一条消息(子事务)。例如,我们将事务T拆分为T1、T2、T3、T4。那么我们就要为这些子交易提供相应的执行逻辑和补偿逻辑。没错,和TCC一样,只是比TCC少了一步Try动作,而且这些操作也要求是幂等的。你看,其实SAGA的概念很好理解,你只要按照正常的业务逻辑去执行就可以了。只是如果任何一个步骤发生异常,之前提交的所有数据都必须回滚(补偿)。唯一特别的是,它通常是消息驱动来完成事务操作。如果硬要追求它的本质,SAGA和TCC一样,先记录执行轨迹,然后通过不断的重试,达到最终状态。上图是robvettor绘制的一个典型的SAGA事务拆分图。图中黑线为正常业务流程,红线为薪酬业务流程。这是一个简单的电子商务结账流程。整个事务跨越了5个微服务,可以说是一个非常大很长的事务。可以看出,这样的交易流程很难通过文字描述来理解,所以SAGA通常会配备流程编辑器,直接将交易安排的流程可视化。|问题这个问题更有趣:嵌套问题。SAGA只允许两层嵌套,因为依赖消息流是非常复杂的。深嵌套级别不允许性能和时序。如果你的交易包含很多子交易,很有可能在某个阶段执行失败。向上。但是,如果补偿操作也失败了怎么办?在极端情况下,需要人为干预。很多时候需要记录日志(sagalog)来配合完成。由于这些小事务不是同时提交的,所以在执行过程中会产生脏数据,这和数据库中readuncommited|的概念是一样的。框架在《微服务架构设计模式》的第四章中,讲解了SAGA的具体使用示例,现在网上的文章大部分都出自于此。但是据我了解,互联网公司用SAGA的不多,用TCC的多(可能是遇到的分布式事务不是长事务)。seata还提供了SAGA方式,主要采用状态机驱动的编排方式。为了支持事务的编排,seata提供了专门的流程编辑器(在线)。http://seata.io/saga_designer/index.html设计完成后可以导出为JSON文件,解析后写入数据库。bytetcc虽然叫tcc,但是也支持SAGA。|SAGAvsTCC上面说到,我在日常工作中使用TCC多于SAGA,这也是业务场景决定的。我们做一个简单的比较:开发难度。TCC的开发难度高于SAGA,因为需要处理Try阶段冻结资源,而SAGA直接执行本地事务的脏读问题。TCC没有脏读,因为try阶段不影响数据;SAGA在小事务之间或者取消之间会有脏读效率问题。无论TCC成功与否,都需要与参与者进行两次互动;SAGA在正常情况下交互一次,异常情况下交互两次,所以效率应该是高的业务流程。TCC适合少量的分布式事务进程,否则写起来就是噩梦;SAGA适用于业务流程长、参与者多的业务,或者遗留系统无法转化为TCC业务方式的业务。TCC通过业务建模解决技术问题;SAGA通过技术手段解决事务编排。本地消息表的使用场景比较有限。它依赖于MQ来实现,它解决了数据库事务和MQ之间的事务问题。如图,有一个分布式事务。正常入库后,需要通过MQ来协调后续业务的执行。但是写DB和写MQ无法做到一致性,所以需要增加一个本地消息表来缓存发送给MQ的状态。下面我描述一下这个过程:正常写入数据库,写入数据库的同时写入本地消息表。该表用于记录MQ消息处理的状态,可以有两种状态:发送中和完成。由于消息表和普通业务表在同一个DB,可以实现本地事务,同时保证消息表写入成功后可以异步发送MQ消息,不需要关心关于投递成功与否。后续业务订阅MQ消息。消费成功后,会通过MQ发送执行成功状态。本地业务订阅该执行状态,并将消息表中对应的记录状态更改为完成;如果消费失败,有定时任务不断扫描本地消息表,状态为正在发送消息(注意延迟),将这些消息再次发送给MQ,重复2的过程。通过这样一个循环,就可以实现本地DB和MQ消费者状态的一致性,完成最终一致的分布式事务。可以看到我们有重发MQ的过程,所以这种模式需要消费者也实现幂等的功能,避免重复对业务造成影响。|问题使用本地消息表方案的系统相当多,但其缺点也很明显:需要开发专门的代码,加上业务,无法完成抽象框架。需要将本地消息表写入数据库。如果数据库本身/O已经比较高了,会增加数据库的压力。最大努力补偿最大努力补偿是一种衰减的补偿机制。让我们举一个最简单的例子。如果您是微信支付的接入方,微信支付成功后,会将支付结果推送到您指定的接口。微信支付+你的支付结果处理,可以看做一个大的分布式事务。涉及微信的系统也包括你自己的系统。如果您的系统一直无法处理成功,那么微信支付会一直重试。这称为最大努力补偿,它既可以在系统内使用,也可以在系统之间使用。但是你不能无限重试,重试间隔通常会随着时间衰减。有常用的衰减策略。messageDelayLevel=1s5s10s30s1m2m3m4m5m6m7m8m9m10m20m30m1h2h上面公式的意思是如果进程不能处理成功,1s...后重试,最多2小时。如果不成功,只能进入人工处理通道。尽力而为补偿只是一个??想法,实际应用的方式有很多种。比如我先把事务落地到消息队列上,然后依靠消息队列的重试机制来达到besteffortcompensation的效果。这些都是可行的方案。综上所述,在这篇文章中,我们从本地事务出发,讲了2PC、3PC、TCC、SAGA、本地消息表、besteffortcompensation等,了解了一些应用场景和各种解决方案的解决方案。在这些理论的基础上,对分布式事务框架进行了或多或少的修改,也有很多创新之处。比如LCN框架(lock,confirm,notify)抽象了controller和initiator的概念,有兴趣的可以自行理解。在互联网公司中,由于对高并发的需求,在实际应用中,相对于强事务,大家一般都会选择软事务进行业务处理。使用最多的是TCC、SAGA、本地消息表等解决方案。SAGA特别擅长处理长事务,但隔离性稍差;TCC一致性好,并发度高,但需要更多编码;本地消息表的应用场景有限,耦合服务无法复用。每种方案都有利有弊,必须结合使用场景进行选择。在框架方面,阿里的seata(早年叫fescar)已经被广泛使用,支持XA、TCC、SAGA等模式。如果你需要这个功能,你可以尝试集成它。希望看完本文后,你会遇到“如何在微服务中实现分布式事务?”的问题。再次。除了回答“尽量避免使用分布式事务”之外,你还可以找到可行的解决方案。
