原文地址:梁桂钊的博客git
博客地址:http://blog.720ui.comgithub
欢迎关注公众号:「服务端思惟」。一群同频者,一块儿成长,一块儿精进,打破认知的局限性。算法
什么是事务?回答这个问题以前,咱们先来看一个经典的场景:支付宝等交易平台的转帐。假设小明须要用支付宝给小红转帐 100000 元,此时,小明账号会少 100000 元,而小红账号会多 100000 元。若是在转帐过程当中系统崩溃了,小明账号少 100000 元,而小红账号金额不变,就会出大问题,所以这个时候咱们就须要使用事务了。请参见图 6-1。spring
这里,体现了事务一个很重要的特性:原子性。事实上,事务有四个基本特性:原子性、一致性、隔离性、持久性。其中,原子性,即事务内的操做要么所有成功,要么所有失败,不会在中间的某个环节结束。一致性,即便数据库在一个事务执行以前和执行以后,数据库都必须处于一致性状态。若是事务执行失败,那么须要自动回滚到原始状态,换句话说,事务一旦提交,其余事务查看到的结果一致,事务一旦回滚,其余事务也只能看到回滚前的状态。隔离性,即在并发环境中,不一样的事务同时修改相同的数据时,一个未完成事务不会影响另一个未完成事务。持久性,即事务一旦提交,其修改的数据将永久保存到数据库中,其改变是永久性的。数据库
本地事务经过 ACID 保证数据的强一致性。ACID是 Atomic(原子性)、Consistency(一致性)、 Isolation(隔离性)和 Durability(持久性)的缩写 。在实际开发过程当中,咱们或多或少都有使用到本地事务。例如,MySQL 事务处理使用到 begin 开始一个事务,rollback 事务回滚,commit 事务确认。这里,事务提交后,经过 redo log 记录变动,经过 undo log 在失败时进行回滚,保证事务的原子性。笔者补充下,使用 Java 语言的开发者都接触过 Spring。Spring 使用 @Transactional 注解就能够搞定事务功能。事实上,Spring 封装了这些细节,在生成相关的 Bean 的时候,在须要注入相关的带有 @Transactional 注解的 bean 时候用代理去注入,在代理中为咱们开启提交/回滚事务。请参见图6-2。apache
随着业务的高速发展,面对海量数据,例如,上千万甚至上亿的数据,查询一次所花费的时间会变长,甚至会形成数据库的单点压力。所以,咱们就要考虑分库与分表方案了。分库与分表的目的在于,减少数据库的单库单表负担,提升查询性能,缩短查询时间。这里,咱们先来看下单库拆分的场景。事实上,分表策略能够概括为垂直拆分和水平拆分。垂直拆分,把表的字段进行拆分,即一张字段比较多的表拆分为多张表,这样使得行数据变小。一方面,能够减小客户端程序和数据库之间的网络传输的字节数,由于生产环境共享同一个网络带宽,随着并发查询的增多,有可能形成带宽瓶颈从而形成阻塞。另外一方面,一个数据块能存放更多的数据,在查询时就会减小 I/O 次数。水平拆分,把表的行进行拆分。由于表的行数超过几百万行时,就会变慢,这时能够把一张的表的数据拆成多张表来存放。水平拆分,有许多策略,例如,取模分表,时间维度分表等。这种场景下,虽然咱们根据特定规则分表了,咱们仍然可使用本地事务。可是,库内分表,仅仅是解决了单表数据过大的问题,但并无把单表的数据分散到不一样的物理机上,所以并不能减轻 MySQL 服务器的压力,仍然存在同一个物理机上的资源竞争和瓶颈,包括 CPU、内存、磁盘 IO、网络带宽等。对于分库拆分的场景,它把一张表的数据划分到不一样的数据库,多个数据库的表结构同样。此时,若是咱们根据必定规则将咱们须要使用事务的数据路由到相同的库中,能够经过本地事务保证其强一致性。可是,对于按照业务和功能划分的垂直拆分,它将把业务数据分别放到不一样的数据库中。这里,拆分后的系统就会遇到数据的一致性问题,由于咱们须要经过事务保证的数据分散在不一样的数据库中,而每一个数据库只能保证本身的数据能够知足 ACID 保证强一致性,可是在分布式系统中,它们可能部署在不一样的服务器上,只能经过网络进行通讯,所以没法准确的知道其余数据库中的事务执行状况。请参见图6-3。api
此外,不只仅在跨库调用存在本地事务没法解决的问题,随着微服务的落地中,每一个服务都有本身的数据库,而且数据库是相互独立且透明的。那若是服务 A 须要获取服务 B 的数据,就存在跨服务调用,若是遇到服务宕机,或者网络链接异常、同步调用超时等场景就会致使数据的不一致,这个也是一种分布式场景下须要考虑数据一致性问题。请参见图6-4。安全
总结一下,当业务量级扩大以后的分库,以及微服务落地以后的业务服务化,都会产生分布式数据不一致的问题。既然本地事务没法知足需求,所以分布式事务就要登上舞台。什么是分布式事务?咱们能够简单地理解,它就是为了保证不一样数据库的数据一致性的事务解决方案。这里,咱们有必要先来了解下 CAP 原则和 BASE 理论。CAP 原则是 Consistency(一致性)、Availablity(可用性)和 Partition-tolerance(分区容错性)的缩写,它是分布式系统中的平衡理论。在分布式系统中,一致性要求全部节点每次读操做都能保证获取到最新数据;可用性要求不管任何故障产生后都能保证服务仍然可用;分区容错性要求被分区的节点能够正常对外提供服务。事实上,任何系统只可同时知足其中二个,没法三者兼顾。对于分布式系统而言,分区容错性是一个最基本的要求。那么,若是选择了一致性和分区容错性,放弃可用性,那么网络问题会致使系统不可用。若是选择可用性和分区容错性,放弃一致性,不一样的节点之间的数据不能及时同步数据而致使数据的不一致。请参见图 6-5。服务器
此时,BASE 理论针对一致性和可用性提出了一个方案,BASE 是 Basically Available(基本可用)、Soft-state(软状态)和 Eventually Consistent(最终一致性)的缩写,它是最终一致性的理论支撑。简单地理解,在分布式系统中,容许损失部分可用性,而且不一样节点进行数据同步的过程存在延时,可是在通过一段时间的修复后,最终可以达到数据的最终一致性。BASE 强调的是数据的最终一致性。相比于 ACID 而言,BASE 经过容许损失部分一致性来得到可用性。网络
如今,业内比较经常使用的分布式事务解决方案,包括强一致性的两阶段提交协议,三阶段提交协议,以及最终一致性的可靠事件模式、补偿模式,阿里的 TCC 模式。咱们会在后面的章节中详细介绍与实战。
在分布式系统中,每一个数据库只能保证本身的数据能够知足 ACID 保证强一致性,可是它们可能部署在不一样的服务器上,只能经过网络进行通讯,所以没法准确的知道其余数据库中的事务执行状况。所以,为了解决多个节点之间的协调问题,就须要引入一个协调者负责控制全部节点的操做结果,要么所有成功,要么所有失败。其中,XA 协议是一个分布式事务协议,它有两个角色:事务管理者和资源管理者。这里,咱们能够把事务管理者理解为协调者,而资源管理者理解为参与者。
XA 协议经过二阶段提交协议保证强一致性。
二阶段提交协议,顾名思义,它具备两个阶段:第一阶段准备,第二阶段提交。这里,事务管理者(协调者)主要负责控制全部节点的操做结果,包括准备流程和提交流程。第一阶段,事务管理者(协调者)向资源管理者(参与者)发起准备指令,询问资源管理者(参与者)预提交是否成功。若是资源管理者(参与者)能够完成,就会执行操做,并不提交,最后给出本身响应结果,是预提交成功仍是预提交失败。第二阶段,若是所有资源管理者(参与者)都回复预提交成功,资源管理者(参与者)正式提交命令。若是其中有一个资源管理者(参与者)回复预提交失败,则事务管理者(协调者)向全部的资源管理者(参与者)发起回滚命令。举个案例,如今咱们有一个事务管理者(协调者),三个资源管理者(参与者),那么这个事务中咱们须要保证这三个参与者在事务过程当中的数据的强一致性。首先,事务管理者(协调者)发起准备指令预判它们是否已经预提交成功了,若是所有回复预提交成功,那么事务管理者(协调者)正式发起提交命令执行数据的变动。请参见图 6-6。
注意的是,虽然二阶段提交协议为保证强一致性提出了一套解决方案,可是仍然存在一些问题。其一,事务管理者(协调者)主要负责控制全部节点的操做结果,包括准备流程和提交流程,可是整个流程是同步的,因此事务管理者(协调者)必须等待每个资源管理者(参与者)返回操做结果后才能进行下一步操做。这样就很是容易形成同步阻塞问题。其二,单点故障也是须要认真考虑的问题。事务管理者(协调者)和资源管理者(参与者)均可能出现宕机,若是资源管理者(参与者)出现故障则没法响应而一直等待,事务管理者(协调者)出现故障则事务流程就失去了控制者,换句话说,就是整个流程会一直阻塞,甚至极端的状况下,一部分资源管理者(参与者)数据执行提交,一部分没有执行提交,也会出现数据不一致性。此时,读者会提出疑问:这些问题应该都是小几率状况,通常是不会产生的?是的,可是对于分布式事务场景,咱们不只仅须要考虑正常逻辑流程,还须要关注小几率的异常场景,若是咱们对异常场景缺少处理方案,可能就会出现数据的不一致性,那么后期靠人工干预处理,会是一个成本很是大的任务,此外,对于交易的核心链路也许就不是数据问题,而是更加严重的资损问题。
二阶段提交协议诸多问题,所以三阶段提交协议就要登上舞台了。三阶段提交协议是二阶段提交协议的改良版本,它与二阶段提交协议不一样之处在于,引入了超时机制解决同步阻塞问题,此外加入了预备阶段尽量提前发现没法执行的资源管理者(参与者)而且终止事务,若是所有资源管理者(参与者)均可以完成,才发起第二阶段的准备和第三阶段的提交。不然,其中任何一个资源管理者(参与者)回复执行,或者超时等待,那么就终止事务。总结一下,三阶段提交协议包括:第一阶段预备,第二阶段准备,第二阶段提交。请参见图 6-7。
三阶段提交协议很好的解决了二阶段提交协议带来的问题,是一个很是有参考意义的解决方案。可是,极小几率的场景下可能会出现数据的不一致性。由于三阶段提交协议引入了超时机制,若是出现资源管理者(参与者)超时场景会默认提交成功,可是若是其没有成功执行,或者其余资源管理者(参与者)出现回滚,那么就会出现数据的不一致性。
二阶段提交协议和三阶段提交协议很好的解决了分布式事务的问题,可是在极端状况下仍然存在数据的不一致性,此外它对系统的开销会比较大,引入事务管理者(协调者)后,比较容易出现单点瓶颈,以及在业务规模不断变大的状况下,系统可伸缩性也会存在问题。注意的是,它是同步操做,所以引入事务后,直到全局事务结束才能释放资源,性能多是一个很大的问题。所以,在高并发场景下不多使用。所以,阿里提出了另一种解决方案:TCC 模式。注意的是,不少读者把二阶段提交等同于二阶段提交协议,这个是一个误区,事实上,TCC 模式也是一种二阶段提交。
TCC 模式将一个任务拆分三个操做:Try、Confirm、Cancel。假如,咱们有一个 func() 方法,那么在 TCC 模式中,它就变成了 tryFunc()、confirmFunc()、cancelFunc() 三个方法。
tryFunc(); confirmFunc(); cancelFunc();
在 TCC 模式中,主业务服务负责发起流程,而从业务服务提供 TCC 模式的 Try、Confirm、Cancel 三个操做。其中,还有一个事务管理器的角色负责控制事务的一致性。例如,咱们如今有三个业务服务:交易服务,库存服务,支付服务。用户选商品,下订单,紧接着选择支付方式进行付款,而后这笔请求,交易服务会先调用库存服务扣库存,而后交易服务再调用支付服务进行相关的支付操做,而后支付服务会请求第三方支付平台建立交易并扣款,这里,交易服务就是主业务服务,而库存服务和支付服务是从业务服务。请参见图 6-8。
咱们再来梳理下,TCC 模式的流程。第一阶段主业务服务调用所有的从业务服务的 Try 操做,而且事务管理器记录操做日志。第二阶段,当所有从业务服务都成功时,再执行 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://github.com/changmingxie/tcc-transaction)tcc-transaction 框架主要涉及 tcc-transaction-core、tcc-transaction-api、tcc-transaction-spring 三个模块。其中,tcc-transaction-core 是 tcc-transaction 的底层实现,tcc-transaction-api 是 tcc-transaction 使用的 API,tcc-transaction-spring 是 tcc-transaction 的 Spring 支持。 tcc-transaction 将每一个业务操做抽象成事务参与者,每一个事务能够包含多个参与者。参与者须要声明 try / confirm / cancel 三个类型的方法。这里,咱们经过 @Compensable 注解标记在 try 方法上,并定义相应的 confirm / cancel 方法。
// try 方法 @Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = MethodTransactionContextEditor.class) @Transactional public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {} // confirm 方法 @Transactional public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {} // cancel 方法 @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 confirm / cancel 方法,使用参与者的事务编号和远程的分支事务进行关联,从而实现事务的提交和回滚。事务状态 TransactionStatus 包含 : 尝试中状态 TRYING(1)、确认中状态 CONFIRMING(2)、取消中状态 CANCELLING(3)。此外,事务类型 TransactionType 包含 : 根事务 ROOT(1)、分支事务 BRANCH(2)。当调用 TransactionManager#begin() 发起根事务时,类型为 MethodType.ROOT,而且事务 try 方法被调用。调用 TransactionManager#propagationNewBegin() 方法,传播发起分支事务。该方法在调用方法类型为 MethodType.PROVIDER 而且 事务 try 方法被调用。调用 TransactionManager#commit() 方法提交事务。该方法在事务处于 confirm / cancel 方法被调用。相似地,调用 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 通知消息队列,消息队列才从消息队列中删除该持久化消息。那么,消息队列若是一直重试失败而没法投递,就会出现消息主动丢弃的状况,咱们须要如何解决呢?聪明的读者可能已经发现,咱们在上个步骤中,主业务服务已经将要发送的消息持久化到本地数据库。所以,从业务服务消费成功后,它也会向消息队列发送一个通知消息,此时它是一个消息的生产者。主业务服务(消费者)接收到消息后,最终把本地的持久化消息标志状态为“完成”状态。说到这里,读者应该能够理解到咱们使用“正反向消息机制”确保了消息队列可靠事件投递。固然,补偿机制也是必不可少的。定时任务会从数据库扫描在必定时间内未完成的消息并从新投递。请参见图 6-13。
注意的是,由于从业务服务可能收到消息处理超时或者服务宕机,以及网络等缘由致使而消息队列收不到消息的处理结果,所以可靠事件投递而且消息队列确保事件传递至少一次。这里,从业务服务(消费者)须要保证幂等性。若是从业务服务(消费者)没有保证接口的幂等性,将会致使重复提交等异常场景。此外,咱们也能够独立消息服务,将消息服务独立部署,根据不一样的业务场景共用该消息服务,下降重复开发服务的成本。
了解了“可靠事件模式”的方法论后,如今咱们来看一个真实的案例来加深理解。首先,当用户发起退款后,自动化退款服务会收到一个退款的事件消息,此时,若是这笔退款符合自动化退款策略的话,自动化退款服务会先写入本地数据库持久化这笔退款快照,紧接着,发送一条执行退款的消息投递到给消息队列,消息队列接受到消息后返回响应成功结果,那么自动化退款服务就能够执行后续的业务逻辑。与此同时,消息队列异步地把消息投递给退款基础服务,而后退款基础服务执行本身业务相关的逻辑,执行失败与否由退款基础服务自我保证,若是执行成功则发送一条执行退款成功消息投递到给消息队列。最后,定时任务会从数据库扫描在必定时间内未完成的消息并从新投递。这里,须要注意的是,自动化退款服务持久化的退款快照能够理解为须要确保投递成功的消息,由“正反向消息机制”和“定时任务”确保其成功投递。此外,真正的退款出帐逻辑在退款基础服务来保证,所以它要保证幂等性,及出帐逻辑的收敛。当出现执行失败的状态而且超太重试次数时,就说明这个任务永久失败了,须要开发人员进行手工介入与排查问题。请参见图 6-14。
总结一下,引入了消息队列并不能保证可靠事件投递,换句话说,因为网络等各类缘由而致使消息丢失不能保证其最终的一致性,所以,咱们须要经过“正反向消息机制”确保了消息队列可靠事件投递,而且使用补偿机制尽量在必定时间内未完成的消息并从新投递。
开源项目中对分布式事务的应用有不少值得咱们学习与借鉴的地方。本节,咱们就来对其实现进行解读。
Apache RocketMQ 是阿里开源的一款高性能、高吞吐量的分布式消息中间件。在历年双 11 中,RocketMQ 都承担了阿里巴巴生产系统所有的消息流转,在核心交易链路有着稳定和出色的表现,是承载交易峰值的核心基础产品之一。RocketMQ 同时存在商用版 MQ 可在阿里云上购买(https://www.aliyun.com/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 基于华为内部的 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://github.com/apache/servicecomb-saga/tree/master/saga-demo
@EnableOmega public class Application{ public static void main(String[] args) { SpringApplication.run(Application.class, args); } } @SagaStart public void xxx() { } @Compensable public void transfer() { }
如今,咱们来看一下它的业务流程图,请参见图 6-17。
更多精彩文章,尽在「服务端思惟」!
本文由博客一文多发平台 OpenWrite 发布!