微服务架构下,最好的分布式数据一致性解决方案就是尽量避免分布式事务,然而,在很多场景下,分布式事务是难以避免的。在金融、电信领域中,很多业务场景要求数据的强一致性,同时要保证服务的可扩展性和可靠性。如何保证分布式事务下的数据一致性成为微服务架构的一个重要课题和难点。
在工程领域,分布式事务的讨论主要聚焦于强一致性和最终一致性的解决方案。常见的分布式事务有基于XA协议的两阶段(2PC)提交模式,以及改良版本的三阶段(3PC)提交模式。JAVA事务编程接口(Java Transaction API,JTA)和Java事务服务(Java TransactionService,JTS)正是基于XA协议的实现。分布式事务包括事务管理器TM(Transaction Manager)和一个或多个支持XA协议的资源管理器RM(Resource Manager)。在微服务架构下,我们倾向使用最终一致性的方案。下面将介绍2PC模式、TCC模式、Saga模式等。
● 2PC模式,分布式事务比较典型的解决方案,但是对于微服务架构而言,可能这一方案并不适用,主要原因是不同微服务可能使用的数据存储类型不同。如果使用的NoSQL不支持事务的数据库,那么事务根本无法实现2PC模式。此外,2PC模式本身也存在同步阻塞、单点故障和性能问题。
● TCC模式,相比2PC模式,具有更强的灵活性和性能优势,TCC模式本质上是基于服务层的2PC编程模式,把事务从数据库层的事务操作逻辑抽象到业务服务层,通过服务层业务逻辑实现服务的补偿模式。
● Saga模式,核心理念是将事务切分成一组依次执行的短事务,也可以理解成基于业务补偿逻辑实现分布式下的高性能分布式事务模式。较之基于单一数据库资源访问的本地事务,分布式事务的应用架构更为复杂。在不同的分布式应用架构下,实现一个分布式事务要考虑的问题并不完全一样,比如对多资源的协调、事务的跨服务传播、业务的侵入性、隔离性等,实现机制也是复杂多变的。
● 可靠消息模式的解决方案是使用消息队列进行系统间的解耦。
由上游服务发起事件,通过消息队列传递到下游服务,下游服务接收到消息后进行事件消费,最终完成业务,达到数据一致。为了解决消息队列及上下游服务的不可靠性,通常还会借助额外的事件表和定时器辅助完成数据补偿。
2PC(两阶段提交)
2PC是一个非常经典的强一致、中心化的通过原子提交来实现分布式事务一致性管理的协议。这里所说的中心化指协议中有两类节点:
中 心 化 协 调 者 节 点 ( Coordinator ) 和 N 个 参 与 者 节 点(Participant)。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要协调者统一掌控所有节点(又称作参与者)的操作结果,并最终指示这些节点是否要把操作结果进行真正的提交或回滚。
2PC将整个事务流程分为两个阶段:准备阶段(Prepare Phase)和提交阶段(Commit Phase)。整个事务过程由事务管理器和参与者组成,事务管理器负责决策整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚。大部分关系数据库,如Oracle、MySQL,都支持两阶段提交协议。下面是计算机数据库进行两阶段提交的说明。
● 准备阶段:事务管理器为每个参与者准备(Prepare)消息,每个参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有被提交。(Undo日志记录修改的数据,用于数据回滚;Redo日志记录修改后的数据,用于提交事务后写入数据文件)。
● 提交阶段:如果事务管理器收到了参与者执行失败或者超时的消息,则直接向每个参与者发送回滚消息;否则,发送提交消息。参与者根据事务管理器的消息执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。
2PC的算法思路可以概括为:参与者将操作成败的结果通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是执行提交操作还是回滚操作。两阶段提交就是分成两个阶段提交,第一阶段询问各个事务数据源是否准备好,第二阶段才真正将数据提交给事务数据源。但因为2PC协议成本比较高,又有全局锁的问题,性能会比较差。现在我们基本上不会采用这种强一致性解决方案。2PC流程如下图所示。
在2PC中,如果两阶段出现协调者和参与者都宕机的情况,则有可能出现数据不一致的问题,同时还存在着诸如同步阻塞、单点问题、脑裂等问题,所以研究者们在2PC的基础上做了改进,提出了3PC。
3PC(三阶段提交)
3PC是2PC的改进版本,流程如下图所示。
3PC要解决的最关键问题就是协调者和参与者同时宕机的问题,所以3PC将2PC的第一阶段一分为二,形成了由canCommit、preCommit和Commit 3个阶段组成的事务处理协议。
3PC的核心理念是:在询问时并不锁定资源,除非所有参与者都同意了,才开始锁资源。一旦参与者无法及时收到来自协调者的信息,它就会默认执行Commit,而不会一直持有事务资源并处于阻塞状态,但是这种机制也会导致数据一致性问题。3PC在2PC的基础上做了如下改进:
● 增加了超时机制。
● 在两阶段之间插入了准备阶段。
关于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland在2007年发表的一篇名为Lifebeyond Distributed Transactions:anApostate’s Opinion的论文中提出的。在该论文中,TCC还是以
Tentative-Confirmation-Cancellation命名的。
TCC的核心思想是:针对每个操作都要注册一个与其对应的确认和补偿(撤销)操作。TCC事务处理流程和2PC类似,不过2PC通常都在跨库的DB层面,而TCC本质上就是一个应用层面的2PC,需要通过业务逻辑来实现。
TCC模式的工作原理
TCC模式的工作原理如下图所示。TCC将一个完整的事务提交分为Try、Confirm、Cancel 3个操作。
● Try:预留业务资源/数据效验。
● Confirm:确认执行真正要执行的业务,如果所有事务参与者的Try操作都执行成功了,就会调用所有事务参与者的Confirm操作,确认资源。Confirm操作满足幂等性,要求具备幂等设计,Confirm失败后需要进行重试。
● Cancel:取消执行,如果有事务参与者在Try阶段执行失败,就调用所有已成功执行Try阶段的参与者的Cancel方法,释放Try阶段占用的资源。Cancel操作满足幂等性,Cancel阶段的异常和Confirm阶段的异常处理方案基本上一致。
下面是实际模拟用户下单的一个业务场景使用TCC模式的主要流程图。TCC第一阶段是图中的粗线条部分,这个阶段需要分别执行订单服务和库存服务的Try事务,预留必需的业务资源;在第一阶段执行成功后,就会进入第二阶段的Confirm操作,如果不成功,则进行Cancel操作。
TCC的优点和缺点
● TCC的优点:让应用自己定义数据库操作的粒度,使降低锁冲突、提高吞吐量成为可能。TCC的特点在于业务资源检查与加锁,一阶段进行校验,锁定资源,如果第一阶段都成功,则第二阶段对锁定资源进行交易逻辑,否则对锁定资源进行释放,这样就避免了数据库两阶段提交中的锁冲突和长事务低性能风险。
● TCC的缺点:业务逻辑的每个分支都需要实现Try、Confirm、Cancel 3个操作,应用侵入性较强,改造成本高。另外,实现难度较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,Confirm和Cancel接口必须实现幂等。
TCC的开源解决方案
目前,国内的蚂蚁金服主要采用TCC模式进行分布式事务管理。下面总结了常用的TCC开源分布式管理框架:
● Seata
● Tcc-transaction
● Hmily
● ByteTCC
● EasyTransaction
1987年,普林斯顿大学的Hector Garcia-Molina和Ke.NETh Salem发表了一篇论文:Sagas[3]。这篇论文提出了使用Saga机制作为分布式事 务 的 替 代 品 , 以 解 决 长 时 间 运 行 的 分 布 式 事 务 ( long-LivedTransaction,LLT)问题,LLT指长时间持有数据库资源的长活事务。
该论文认为业务过程经常由很多步骤组成,每一个步骤都涉及一个事务,如果将这些事务组成一个分布式事务,就可以实现总体一致。然而在长时间运行的分布式事务中,使用分布式事务会影响效率和系统的并发处理能力,因为在执行分布式事务时会有锁产生。Saga通过确保每一个业务过程都有修正事务来减少系统对分布式事务的依赖。这种在业务流程中执行修正事务的方式最终保证了系统数据一致性。
每一个LLT的Saga都由一系列sub-transaction Ti组成,每个Ti都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果。同时Saga定义了两种恢复策略:
● 向后恢复模式(Backward Recovery),在这种模式下,每一个内部子事务都有一个对应的补偿事务,如果任一子事务失败,则将撤销之前所有成功的sub-transaction,使整个Saga的执行结果都撤销。
● 向前恢复模式(Forward Recovery),这种模式假设每个子事务最终都会成功,适用于必须要成功的场景,执行顺序如下:T1,T2,…,Tj(失败),Tj(重试),…,Tn,其中Tj是发生错误的sub-transaction,此时执行重试逻辑,在该模式下不需要执行补偿事务。
微服务架构大师Chris Richardson在介绍微服务架构与数据一致性技术时,提出了Saga模式,将微服务中的分布式事务划分为一组小的事务,所划分事务或者全部提交,或者全部回滚。强调在微服务中,每个单独的微服务都可以确保ACID,因为每一个微服务都具备自己的数据库,但是,Saga模式作为整体并不保证隔离性,所以需要对异常情况进行补偿操作。
在Saga模式中,为了保障事务提交和回滚,应使用事务日志结尾和消息传递的方式。在发消息之前,将消息写入本地数据库,这就是所谓的事务日志结尾。它的作用是当新的日志到来时,可以直接发布,这样就可以强化ACD特性;同时Saga模式中子模块之间的消息通信建议采用消息传递方式,因为和HTTP通信协议相比,消息传递方式具备持久性。
Saga模式的工作原理
在微服务架构下,Saga存在两种协调模式。
● 编排模式(Choreography)
这种模式在微服务之间传递Saga,没有重协调器,每个微服务都监听其他服务,并决定采取行动。这种模式最大的优势就是简单,容易理解,参与者之间松散耦合,对于参与者较少的情况,适合采用这种模式。下面是使用编排模式的分布式事务时序图,实线表示消息发布,虚线表示订阅。
事件执行顺序如下:
(1)订单服务(Order Service)在Approval_pending状态下创建了订单,并发布订单创建事件。
(2)库存服务(Inventory Service)消费订单创建事件,在Inventory_pending状态下验证订单,订单出库,并创建InventoryCreate Event。
(3)账户服务(Account Service)消费订单创建事件并进入等待的状态。
(4)账户服务消费Inventory Event,收取费用并发布AccountEvent。
( 5 ) 库 存 服 务 订 阅 Account Event 商 品 出 库 , 更 改Inventory_pending状态为Accept状态。
(6)订单服务收到Account Event,订单状态改为Approved状态。
(7)如果上述订单服务失败,那么库存服务消费费用收取失败事件,回滚之前库存事务清单,将状态改为拒绝。
(8)如果上述账户服务失败,那么订单服务消费费用收取失败事件,回滚订单状态改为拒绝。
● 编制模式(Orchestrator)
这种模式需要一个集中的服务触发器,跟踪Saga的所有子任务调用情况,根据调用情况来决定是否采用补偿措施。这种中央协调的方式可以减少每一个微服务之间的循环依赖,集成处理事件决策和逻辑排序,也更容易推理Saga组合,保证长事务数据的最终一致性。使用业务流程时,可以定义一个控制类,其唯一职责是告诉Saga参与者该做什么。Saga控制使用命令或异步回复样式交互与参与者进行通信。
下面是使用编制模式的分布式事务时序图,实线表示消息request,虚线表示消息reply。
事件执行顺序如下:
(1)订单服务(Order Service)创建一个订单和一个订单控制器:Saga Orchestrator。
(2)Saga Orchestrator向库存服务(Inventory Service)发送一个创建订单命令。
(3)库存服务回复库存出库命令。
(4)Saga Orchestrator向账号服务(Account Service)发送一个费用收取命令。
(5)账号服务回复订单费用扣除命令。
(6)Saga Orchestrator向订单服务发送一个订单已批准命令。
说明:基于业务流程,Saga编制的每个步骤都包括更新数据库和发布消息的服务,例如订单服务持久化和创建Saga Orchestrator,并向第一个Saga参与者发送消息。第一个Saga参与者是库存服务,通过更新数据库回复消息。然后订单服务通过更新Saga协调器的状态向下一个Saga参与者发送命令,并处理参与者的命令回复响应,服务必须使用事务性的消息传递,以便自动更新数据并发布消息。
编排模式与编制模式
微服务中推荐使用编制模式。相比编排模式,编制模式有下面几个优势。
● 编制模式有更简化的依赖关系。编排模式中,服务之间需要掌握相互依赖关系,而且针对不同的异常,可能还需要采用不同的补偿措施,Saga Orchestrator则调用Saga参与者,所有参与者之间是不需要了解Orchestrator的实现细节的。
● 每个服务只需要暴露各自的API供Orchestrator调用即可,因此每个微服务之间有较少耦合,可以降低每个参与者的复杂度。
● 可以使用Saga Orchestrator更加方便地增加或减少Saga分布式事务逻辑,不需要改变每一个参与者的Saga内部事务。同时可以基于Orchestrator进行关注点分离,并简化业务,操作更加容易、方便。
Saga与ACID
Saga不提供对整体隔离性的保证。一个长事务划分为若干本地事务,在本地事务提交后,整个Saga未完成之前,其他服务可以访问“未完成”的中间状态事务数据;Saga协调器实现原子特性,通过日志实现持久性;通过本地日志与Saga日志保证事务一致性。所以Saga模式只支持ACD,不提供隔离性的保证。
Saga与2PC的区别
Saga与2PC最主要的区别在于,2PC是一个强一致性的分布式事务模式,Saga和TCC都通过应用服务层牺牲ACID特性来实现事务的最终一致性,Saga和TCC可以理解为分布式环境下通过事务补偿模式的事务控制模型,只是Saga和TCC有各自不同的实现策略。
Saga与TCC的区别
Saga和TCC最大的区别在于,Saga没有预留资源,而是直接提交到数据库,Saga比TCC少了一步Try操作,无论最终事务成功或失败,TCC都需要与事务参与方交互两次。而Saga在事务成功的情况下只需要与事务参与方交互一次。如果事务失败,则需要采取补偿事务的方式进行回滚。
Saga的开源解决方案
Saga为相互独立、自治的微服务提供了一种分布式网络场景下的数据一致性的解决方案。下面是一些Saga模式的开源解决方案,由于篇幅所限,这里不再赘述方案的实现细节。
● ServiceComb Saga
● Axon Framework
● Eventuate-tram-sagas
可靠消息模式主要采用一个可靠的消息中间件作为中介,事务的发起方在完成本地事务后向可靠的消息中间件发起消息,事务消费方在收到消息后处理消息,该方案强调的是双方最终的数据一致性。
如下图所示,订单服务将消息发送给订单服务队列,库存服务监听订阅了订单服务的消息队列,并从消息队列中消费信息。由此可以看到,从事务的发起方到消息中间件,再到事务的消费方,中间都会通过网络,由于网络的不可靠性,导致分布式事务的数据不一致性。
基于这种网络的不可靠性,依靠本地消息表和可靠消息队列的模式可以解决分布式事务的不一致性。而这种模式,对业务的侵入性相对较小,在实现复杂度上也比较可控,国内很多互联网公司都采用了可靠消息模式来解决分布式事务的一致性问题。而这一方案的提出和思路来源于Ebay,之后这种模式在业内被广泛使用。这种分布式事务模式的本质是本地事务+可靠消息,以达到最终事务的一致性。我们来看可靠消息模式的具体实现原理,如下图所示。
可靠消息模式的思想是:事务的发起方需要额外创建一个本地消息表,将本地消息表和业务数据放在同一个事务中提交执行。也就是说,二者要么同时成功,要么同时失败。例如,将订单创建事务和库存验证及库存出库事件消息日志放入同一个事务中,下面是伪代码举例:
从上述伪代码可知,本地数据库与库存出库事务处于同一事务中,二者的绑定操作具备了原子性,工作时序如下:
(1)本地事务提交后,可以使用触发的方式对本地消息表进行查询和消息推送,或者使用定时器的方式轮询本地消息表进行消息推送。
(2)消费者的消息可以使用可靠的消息中间件机制,例如RabbitMQ中的ACK(消息确认机制)保证消费者可以一定消费到消息;
又如库存服务在接收到消息并且完成消息业务处理后,回复ACK,说明此时库存服务已经正确同步数据;如果库存服务没有回复ACK,则消息中间件在没收到ACK消息时,将保留消息,并重复投递此消息。
(3)当消息中间件反馈消费成功后,库存服务可以回调一个订单服务的确认API,这时订单服务可以从本地事务表中删除对应的消息队列。
(4)在订单服务中,如果定时任务重复把本地事务表中的消息发到库存服务,则需要消息消费方(库存服务)提供消息的幂等性支持。
幂等性
简单来说,幂等性的概念就是:除了错误或者过期的请求(换言之就是成功的请求),无论多次调用还是单次调用,最终得到的效果都是一致的。通俗来说,只要有一次调用成功,再采用相同的请求参数,无论调用多少次(重复提交),都应该返回成功。
例如库存服务对外提供服务接口,必须承诺实现接口的幂等性,这一点在分布式系统中极其重要。
● 对于HTTP调用,承诺幂等性可以避免表单或者请求操作重复提交,造成业务数据重复。
● 对于异步消息调用,承诺幂等性通过对消息去重处理也是为了避免重复消费造成业务数据重复。
下面是几种常用的幂等性处理设计方案。
● 数据库表设计对逻辑上唯一的业务键唯一索引,这是在数据库层面做最后的保障。
● 业务逻辑上的防重,例如创建订单的接口,首先通过订单号查询库表中是否已经存在对应的订单,如果存在,则不做处理,直接返回成功。
补偿方案
在可靠消息事务方案中,事务发起方需要确保消息发送到消息队列中,而消息队列成为系统的瓶颈。当消息队列异常,或者消息消费失败导致数据不一致时,需要采取补偿措施,而常用的补偿方案由消息消费方负责。之所以使用消费方补偿模式有下面两个主要理由:
● 一般来说,数据不一致大概率发生在消息消费异常场景,如果由消息发送方补偿错误,往往无法解决消费异常问题;同时一个消息可能存在多个消费方,如果消息补偿模块在所有上游服务中编写,可能无法满足所有消费方的异常补偿场景。
● 当消费方出现问题时,需要定位事务发起方,然后才能通过上游来补偿,这种方式会增加处理生产问题的复杂度。
异步消息交互时,采取的补偿措施通常统一由消息消费方实现,这种方式将类似本地事件表的方式,在消息消费失败后,将失败消息写入本地数据库,然后启动定时任务进行重试,当达到重试上限时,进行预警和人工干预。
RabbitMQ可靠消息传输实践
在众多消息队列中,RabbitMQ最重要的特性就是将消息的可靠性作为传输消息考虑的第一要素。目前,RabbitMQ已经成为金融行业中消息队列的标配。我们将通过RabbitMQ的几个关键因素讲解RabbitMQ如何保证消息的可靠传输。RabbitMQ流程如下图所示。
确认机制
网络异常、机器异常、程序异常等多种情况都可能导致业务丢失消息。对消息进行确认可以解决消息的丢失问题,确认成功意味着消息已被验证并被正确处理。确认机制能用在两个方向:允许消费者告诉服务器(Broker)已经收到了消息,也允许服务器告诉生产者接收到了消息。前者就是我们常说的消费者ACK,后者就是我们常说的生产者Confirm。
RabbitMQ使用生产者消息确认、消费者消息确认机制来提供可靠交付功能。
● 生产者消息确认:生产者向RabbitMQ发送消息后,等待它回复确认成功;否则生产者向RabbitMQ重发该消息。此过程可以异步进行,生产者持续发送消息,RabbitMQ将消息批量处理后再回复确认;生产者通过识别确认返回中的ID来确定哪些消息被成功处理。开启生产者消息确认机制:
● 消费者消息确认:RabbitMQ向消费者投递消息后,等待消费者回复确认成功;否则RabbitMQ重新向消费者投递该消息。该过程同样可以异步处理,RabbitMQ持续投递消息,消费者批量处理完后回复确认。可以看出,RabbitMQ/AMQP提供的是“至少一次交付”(at-least-once delivery)的策略,异常情况下消息会被重复投递或消费。开启消费者消息确认机制:
持久化机制
设置交换机、队列和消息都为持久化,它可以在服务器重启时保证消息不丢失信息,集群节点提供冗余能力,对解决Broker的单点故障至关重要。在RabbitMQ集群中,所有的定义都可以被冗余处理,例如交换器和绑定关系等,而队列只存在于一个节点上。对于队列而言,可以通过配置把队列镜像到多个节点上。
● 交换机的持久化(通过查看源码易知,默认是支持持久化的),代码如下:
● 队列的持久化(通过查看源码易知,默认是支持持久化的),代码如下:
● 消 息 的 持 久 化 。 当 我 们 使 用 RabbitTemplate 调 用convertAndSend(String exchange,String routingKey,final Object object)方法时,默认是持久化模式。
生产者
当使用确认机制时,生产者从连接或者Channel故障中恢复过来,会重发没有被Broker确认签收的消息。如此一来,消息就可能被重复发送,可能是由于网络故障等原因,Broker发送了确认,但是生产者没有收到而已。或者消息根本就没有发送到Broker。正因为生产者为了可靠性可能会重发消息,所以在消费者消费消息处理业务时,还需要去重,或者对接收的消息做幂等处理(推荐幂等处理)。生产者增加确认机制非常简单,Channel开启Confirm模式,然后增加监听即可。
说 明 : RabbitMQ 还 有 事 务 机 制 ( txSelect 、 txCommit 、txRollback),也能保障消息的发送。不过事务机制是“同步阻塞”的,所以不推荐使用。而Confirm模式是“异步”机制。通过事务机制与Confirm模式的TPS性能对比,我们可以很明显地看到,事务机制是性能最差的。
RabbitMQ支持的4种交换器类型中,只有fanout模式不存在路由不到队列的情况。因为它会自动路由到所有队列中,跟绑定Key没有任何关系。所以,在满足业务的前提下,笔者建议,尽可能使用fanout模式的类型交换器。
DLX(Dead Letter Exchange,死信邮箱或死信交换机)就是一个普通的交换机,与一般的交换机没有任何区别。当消息在一个队列中变成死信时,通过这个交换机将死信发送到死信队列中。
我们可以通过DLX来解决这个问题,假设一些消息没有被消费,那么它就会被转移到绑定的DLX上。对于这类消息,我们消费并处理死信队列即可。
消费者
只有消费者确认的消息,RabbitMQ才会删除它,不确认就不会被删除。所以,在消费端,建议关闭自动确认机制。应该在收到消息、处理完业务后,手动确认消息。消费者手动确认消息的实现代码如下:
注意上面方法中void basicAck(long deliveryTag,booleanmultiple)的第二个参数multiple。要说明这个参数的含义,首先需要清楚“deliveryTag”概念,即投递消息的唯一标识符,它是一个“单调递增”的Long型正整数。假设此次basicAck的tag为123456,如果 multiple=false , 则 表 示 只 确 认 签 收 这 一 条 消 息 。 如 果multiple=true,则表示确认签收tag小于或等于123456的所有消息。
在微服务架构下,我们强调要根据微服务的数据类型和业务场景选择合适的后端数据存储类型。对于微服务架构下分布式应用中的数据一致性管理,不推荐使用分布式事务,微服务数据架构通过放弃分布式网络的强一致性,来提升微服务之间的交互性能。另外,在微服务数据架构中,我们介绍了常见的TCC、Saga、可靠消息模式,可以作为保证数据之间最终一致性的解决方案。