分布式事务解决方案


文章较长,我从基础到场景再到实践,深入浅出的带大家去了解一下,保证看完之后有所感悟!!!
在这里插入图片描述

1.分布式事务基础

1.1 事务

事务指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销。
简单地说,事务提供一种“要么什么都不做,要么做全套”机制。

1.2 本地事务

本地事物其实可以认为是数据库提供的事务机制。说到数据库事务就不得不说,数据库事务中的四大特性:
单机环境下我们对传统关系型数据库有苛刻的要求,由于存在网络的延迟和消息丢失,ACID 便是保证事务的原则,这四大原则便是:

  • Atomicity:原子性,一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节;保证了事务操作的整体性
  • Consistency:一致性,在事务开始之前和事务结束以后,数据库的完整性没有被破坏;事务操作下数据的正确性
  • Isolation:隔离性,数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时,由于交叉执行而导致数据的不一致;保证了事务并发操作下数据的正确性
  • Durabilit:持久性,事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。保证了事务对数据修改的可靠性

数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。

ACID是数据库的本地事务,而事务的ACID是通过InnoDB日志和锁来保证

  • 事务的隔离性是通过数据库锁的机制实现的
  • 持久性通过redo log(重做日志)来实现
  • 原子性和一致性通过Undo log来实现

1.3 分布式事务

分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
分布式对应的是单体架构,互联网早期单体架构是非常流行的,好像是一个家族企业,大家在一个家里劳作,单体架构如下图:
在这里插入图片描述

但是随着业务的复杂度提高,大家族人手不够,此时不得不招人,这样逐渐演变出了分布式服务,互相协作,每个服务负责不同的业务,架构如下图:
在这里插入图片描述

因此需要服务与服务之间的远程协作才能完成事务,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务。

典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存。简言之:跨JVM进程产生分布式事务。

2.分布式事务场景

2.1 单体系统访问多个数据库

一个服务需要调用多个数据库实例完成数据的增删改操作

在这里插入图片描述

2.2 多个微服务访问同一个数据库

多个服务需要调用一个数据库实例完成数据的增删改操作
在这里插入图片描述

2.3 多个微服务访问多个数据库

不同的微服务模块都有自己的数据库,服务之间相互调用完成数据的增删改操作
在这里插入图片描述

3.一致性

3.1 强一致性

系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值。

也称为:原子一致性(Atomic Consistency)、线性一致性(Linearizable Consistency)

简言之,在任意时刻,所有节点中的数据是一样的。例如,对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。

总结:

  • 一个集群需要对外部提供强一致性,所以只要集群内部某一台服务器的数据发生了改变,那么就需要等待集群内其他服务器的数据同步完成后,才能正常的对外提供服务。
  • 保证了强一致性,务必会损耗可用性。

3.2 弱一致性

系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。

但即使过了不一致时间窗口这段时间后,后续对该数据的读取也不一定是最新值。

所以说,可以理解为数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。

例如12306买火车票,虽然最后看到还剩下几张余票,但是只要选择购买就会提示没票了,这就是弱一致性。

3.3 最终一致性

是弱一致性的特殊形式,存储系统保证在没有新的更新的条件下,最终所有的访问都是最后更新的值。

不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。

简单说,就是在一段时间后,节点间的数据会最终达到一致状态。

总结:

弱一致性即使过了不一致时间窗口,后续的读取也不一定能保证一致,而最终一致过了不一致窗口后,后续的读取一定一致。

4.分布式事务理论

4.1 CPA理论

CAP原则又叫CAP定理,同时又被称作布鲁尔定理(Brewer’s theorem),指的是在一个分布式系统中,不可能同时满足以下三点。
在这里插入图片描述

4.1.1 一致性(Consistency)

指强一致性,数据一致更新,所有数据变动都是同步的,在写操作完成后开始的任何读操作都必须返回该值,或者后续写操作的结果。

也就是说,在一致性系统中,一旦客户端将值写入任何一台服务器并获得响应,那么之后client从其他任何服务器读取的都是刚写入的数据,一致性保证了不管向哪台服务器写入数据,其他的服务器能实时同步数据

4.1.2 可用性(Availability)

可用性(高可用)是指:每次向未崩溃的节点发送请求,总能保证收到响应数据(允许不是最新数据)
可用性高保证了系统的高可用,要求系统非故障的节点在合理的时间内返回合理的响应

4.1.3 分区容忍性(Partition tolerance)

分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,也就是说,服务器A和B发送给对方的任何消息都是可以放弃的,也就是说A和B可能因为各种意外情况,导致无法成功进行同步,分布式系统要能容忍这种情况。除非整个网络环境都发生了故障。

分布式系统理论上不可能选择 CA (一致性 + 可用性)架构,只能选择 CP(一致性 + 分区容忍性) 或者 AP (可用性 + 分区容忍性)架构,在一致性和可用性做折中选择。

4.1.4 为什么只能在A和C之间做取舍?

如果选择了CA,舍弃了P,说白了就是一个单体架构,那么CAP理论也就没有存在的必要了,由于CAP理论是强一致性,如果不能保证P的情况下,就变成本地一致性即ACID。

CAP理论告诉我们只能在C、A之间选择,在分布式事务的最终解决方案中一般选择牺牲一致性来获取可用性和分区容错性。

这里的 “牺牲一致性” 并不是完全放弃数据的一致性,而是放弃强一致性而换取弱一致性。

BASE理论

多数情况下,其实我们也并非一定要求强一致性,部分业务可以容忍一定程度的延迟一致,所以为了兼顾效率,发展出来了最终一致性理论 BASE。
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。

4.2.1 BA(Basic Available)基本可用

整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。这里是属于基本可用。

基本可用和高可用的区别:

  • “一定时间”可以适当延长 当举行大促(比如秒杀)时,响应时间可以适当延长
  • 给部分用户返回一个降级页面 给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果

4.2.2 S(Soft State)柔性状态

称为柔性状态,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统不同节点的数据副本之间进行数据同步的过程存在延时。

4.2.3 E(Eventual Consisstency)最终一致性

同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。

CAP 理论是忽略延时的,而实际应用中延时是无法避免的,这一点就意味着完美的 CP 场景是不存在的,即使是几毫秒的数据复制延迟,在这几毫秒时间间隔内,系统是不符合 CP 要求的。因此 CAP 中的 CP 方案,实际上也是实现了最终一致性,只是“一定时间”是指几毫秒而已。
同时方案中牺牲一致性只是指发生分区故障期间,而不是永远放弃一致性,这一点其实就是 BASE 理论延伸的地方,分区期间牺牲一致性,但分区故障恢复后,系统应该达到最终一致性
总结: BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

5.分布式事务解决方案

在分布式架构下,每个节点只知晓自己操作的失败或者成功,无法得知其他节点的状态。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。

5.1 2PC

二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理。

两个阶段分别为:

  • 准备阶段
  • 提交阶段

参与的角色:

  • 事务协调者(事务管理器):事务的发起者
  • 事务参与者(资源管理器):事务的执行者

5.1.1 准备阶段(投票阶段)

这是两阶段的第一段,这一阶段只是准备阶段,由事务的协调者发起询问参与者是否可以提交事务,但是这一阶段并未提交事务,流程图如下图:
在这里插入图片描述

  • 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
  • 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
  • 如参与者执行成功,给协调者反馈同意,否则反馈中止

5.1.2 提交阶段

这一段阶段属于2PC的第二阶段(提交 执行阶段),协调者发起正式提交事务的请求,当所有参与者都回复同意时,则意味着完成事务,流程图如下:
在这里插入图片描述

  • 协调者节点向所有参与者节点发出正式提交(commit)的请求。
  • 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
  • 参与者节点向协调者节点发送ack完成消息。
  • 协调者节点收到所有参与者节点反馈的ack完成消息后,完成事务。

但是如果任意一个参与者节点在第一阶段返回的消息为终止,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时,那么这个事务将会被回滚,回滚的流程图如下:
在这里插入图片描述

  • 协调者节点向所有参与者节点发出回滚操作(rollback)的请求。
  • 参与者节点利用阶段1写入的undo信息执行回滚,并释放在整个事务期间内占用的资源。
  • 参与者节点向协调者节点发送ack回滚完成消息。
  • 协调者节点受到所有参与者节点反馈的ack回滚完成消息后,取消事务。

不管最后结果如何,第二阶段都会结束当前事务。

二阶段提交的事务正常提交的完整流程如下图:
在这里插入图片描述

二阶段提交事务回滚的完整流程如下图:
在这里插入图片描述

5.1.3 优缺点

2PC的缺点

二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:

  • 性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  • 可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
  • 数据一致性问题:二阶段无法解决的问题:协调者在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
  • 实现复杂:牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
2PC的优点

尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)

5.2 3PC

三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点。

  • 在协调者和参与者中都引入超时机制
  • 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。处理流程如下:
在这里插入图片描述

5.3.1 阶段一:CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  • 事务询问:协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
  • 响应反馈:参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。

CanCommit阶段流程如下图:
在这里插入图片描述

总结起来就是 :询问各个节点是否能够进行提交 ,每个节点都确认可以提交的话执行进入PreCommit阶段

5.3.2 阶段二:PreCommit阶段

协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。

假如所有参与者均反馈 yes,协调者预执行事务。

  • 发送预提交请求 :协调者向参与者发送PreCommit请求,并进入准备阶段
  • 事务预提交 :参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中(但不提交事务)
  • 响应反馈 :如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

在这里插入图片描述

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

  • 发送中断请求 :协调者向所有参与者发送abort请求。
  • 中断事务 :参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

在这里插入图片描述

PreCommit阶段的思路也很简单,即开始执行所有的事务操作,但是不提交,当所有的结点确定可以提交事务之后进入下一个阶段doCommit,但是 一旦有任何一个结点在执行事务操作时响应时间过长或者操作失败就会反馈给协调者,此时协调者会向所有参与者发送abort请求,来进行事务的中断。

5.3.3 阶段三:doCommit阶段

进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 do Commit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。

该阶段进行真正的事务提交,也可以分为以下两种情况。
1.执行提交

  • 发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
  • 事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
  • 响应反馈 事务提交完之后,向协调者发送ack响应。
  • 完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

在这里插入图片描述

2.中断事务:任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务

  • 发送中断请求 如果协调者处于工作状态,向所有参与者发出 abort 请求
  • 事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
  • 反馈结果 参与者完成事务回滚之后,向协调者反馈ACK消息
  • 中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

在这里插入图片描述

5.3.4 优缺点

优点

相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。

缺点

数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

5.3 TCC(事务补偿)

TCC(Try Confirm Cancel)方案是一种应用层面侵入业务的两阶段提交。是目前最火的一种柔性事务方案,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。

TCC分为两个阶段,分别如下:

  • 第一阶段:Try(尝试),主要是对业务系统做检测及资源预留 (加锁,锁住资源)
  • 第二阶段:本阶段根据第一阶段的结果,决定是执行confirm还是cancel
    • Confirm(确认):执行真正的业务(执行业务,释放锁)
    • Cancle(取消):是预留资源的取消(出问题,释放锁)

在这里插入图片描述

为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。

假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。
1.Try 阶段
TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:

  • 完成所有业务检查( 一致性 ) 。
  • 预留必须业务资源( 准隔离性 ) 。
  • Try 尝试执行业务。

在这里插入图片描述

2.Confirm / Cancel 阶段
根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。

Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。

Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作,业务如下图:
在这里插入图片描述

这里使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。

Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。

Cancel:当 Try 阶段存在服务执行失败, 进入 Cancel 阶段,业务如下图:
在这里插入图片描述

Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。

5.3.1 最终一致性保证

TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。

Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。也就是说只要Try成功,Confirm一定成功(TCC设计之初的定义) 。
Confirm与Cancel如果失败,由TCC框架进行重试补偿

存在极低概率在CC环节彻底失败,则需要定时任务或人工介入

5.3.2 优缺点

TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点:

  • 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
  • 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
  • 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。

缺点:
TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。

5.4 本地消息表

本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理

角色:

  • 事务主动方
  • 事务被动方

通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。

这样可以避免以下两种情况导致的数据不一致性:

  • 业务处理成功、事务消息发送失败
  • 业务处理失败、事务消息发送成功

整体的流程如下图:
在这里插入图片描述

上图中整体的处理步骤如下:

  • 1.事务主动方在同一个本地事务中处理业务和写消息表操作
  • 2.事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
  • 3.事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
  • 4.事务主动方接收中间件的消息,更新消息表的状态为已处理。

一些必要的容错处理如下:

  • 当①处理出错,由于还在事务主动方的本地事务中,直接回滚即可
  • 当②、③处理出错,由于事务主动方本地保存了消息,只需要轮询消息重新通过消息中间件发送,事务被动方重新读取消息处理业务即可。
  • 如果是业务上处理失败,事务被动方可以发消息给事务主动方回滚事务
  • 如果事务被动方已经消费了消息,事务主动方需要回滚事务的话,需要发消息通知事务主动方进行回滚事务。

5.4.1 优缺点

优点
  • 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
  • 方案轻量,容易实现。
缺点
  • 与具体的业务场景绑定,耦合性强,不可公用。
  • 消息数据与业务数据同库,占用业务系统资源。
  • 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。

5.5 可靠消息服务(MQ事务方案)

基于可靠消息服务的方案是通过消息中间件保证上、下游应用数据操作的一致性。
假设有A和B两个系统,分别可以处理任务A和任务B。此时存在一个业务流程,需要将任务A和任务B在同一个事务中处理。就可以使用消息中间件来实现这种分布式事务。
在这里插入图片描述

大致流程如下:
第一步:消息由系统A投递到中间件

    1. 在系统A处理任务A前,首先向消息中间件发送一条消息
    1. 消息中间件收到后将该条消息持久化,但并不投递。持久化成功后,向A回复一个确认应答
    1. 系统A收到确认应答后,则可以开始处理任务A
    1. 任务A处理完成后,向消息中间件发送Commit或者Rollback请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了。
    1. 如果消息中间件收到Commit,则向B系统投递消息;如果收到Rollback,则直接丢弃消息。但是如果消息中间件收不到Commit和Rollback指令,那么就要依靠"超时询问机制"。

何为“超时询问机制呢?”

超时询问机制

系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。当消息中间件收到发布消息便开始计时,如果到了超时没收到确认指令,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果,中间件根据三种结果做出不同反应:

  • 提交:将该消息投递给系统B
  • 回滚:直接将条消息丢弃
  • 处理中:继续等待

第二步:消息由中间件投递到系统B
消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成后便向消息中间件返回应答。

  • 如果消息中间件收到确认应答后便认为该事务处理完毕
  • 如果消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。
  • 一般消息中间件可以设置消息重试的次数和时间间隔,如果最终还是不能成功投递,则需要手工干预。这里之所以使用人工干预,而不是使用让A系统回滚,主要是考虑到整个系统设计的复杂度问题。

基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。

MQ事务方案整体流程和本地消息表的流程很相似,如下图:
在这里插入图片描述

从上图可以看出和本地消息表方案唯一不同就是将本地消息表存在了MQ内部,而不是业务数据库中。

那么MQ内部的处理尤为重要,下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。

在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下:

正常情况:事务主动方发消息
在这里插入图片描述

这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:

  • 步骤①:发送方向 MQ 服务端(MQ Server)发送 half 消息。
  • 步骤②:MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。
  • 步骤③:发送方开始执行本地事务逻辑。
  • 步骤④:发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
  • 步骤⑤:MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。

异常情况:事务主动方消息恢复
在这里插入图片描述

在断网或者应用重启等异常情况下,图中提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:

  • 步骤⑤:MQ Server 对该消息发起消息回查。
  • 步骤⑥:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  • 步骤⑦:发送方根据检查得到的本地事务的最终状态再次提交二次确认。
  • 步骤⑧:MQ Server基于 commit/rollback 对消息进行投递或者删除。

优缺点

优点

相比本地消息表方案,MQ 事务方案优点是:

  • 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
  • 吞吐量大于使用本地消息表方案。
缺点
  • 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
  • 业务处理服务需要实现消息状态回查接口。

基于可靠消息服务的分布式事务,前半部分使用异步,注重性能;后半部分使用同步,注重开发成本。

5.6 最大努力通知

最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到消息,此时可以调用事务主动方提供的消息校对的接口主动获取。
在这里插入图片描述

第一步: 消息由系统A投递到中间件

    1. 处理业务的同一事务中,向本地消息表中写入一条记录
    1. 准备专门的消息发送者不断地发送本地消息表中的消息到消息中间件,如果发送失败则重试

第二步: 消息由中间件投递到系统B

    1. 消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行
    1. 当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该事务完成
    1. 对于投递失败的消息,利用重试机制进行重试,对于重试失败的,写入错误消息表
    1. 消息中间件需要提供失败消息的查询接口,下游系统会定期查询失败消息,并将其消费

最大努力通知的整体流程如下图:
在这里插入图片描述

在可靠消息事务中,事务主动方需要将消息发送出去,并且消息接收方成功接收,这种可靠性发送是由事务主动方保证的;

但是最大努力通知,事务主动方尽最大努力(重试,轮询…)将事务发送给事务接收方,但是仍然存在消息接收不到,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。

最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。

优缺点

  • 优点: 一种非常经典的实现,实现了最终一致性。

  • 缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

5.7 Saga 事务

Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。

Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

Saga 事务基本协议如下:

  • 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
  • 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。
  • TCC事务补偿机制有一个预留(Try)动作,相当于先报存一个草稿,然后才提交;Saga事务没有预留动作,直接提交。

对于事务异常,Saga提供了两种恢复策略,分别如下:

  • 向后恢复(backward recovery)
  • 向前恢复(forward recovery)

5.7.1 向后恢复(backward recovery)

在执行事务失败时,补偿所有已完成的事务,是“一退到底”的方式。如下图:

在这里插入图片描述

从上图可知事务执行到了支付事务T3,但是失败了,因此事务回滚需要从C3,C2,C1依次进行回滚补偿。

对应的执行顺序为:T1,T2,T3,C3,C2,C1

这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。

5.7.2 向前恢复(forward recovery)

也称之为:勇往直前,对于执行不通过的事务,会尝试重试事务,这里有一个假设就是每个子事务最终都会成功。

流程如下图:
在这里插入图片描述

适用于必须要成功的场景,事务失败了重试,不需要补偿。

Saga事务有两种不同的实现方式,分别如下:

  • 命令协调(Order Orchestrator)
  • 事件编排(Event Choreographyo)

5.7.3 命令协调

中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。整体流程如下图:
在这里插入图片描述

上图步骤如下:

  • 事务发起方的主业务逻辑请求 OSO 服务开启订单事务
  • OSO 向库存服务请求扣减库存,库存服务回复处理结果。
  • OSO 向订单服务请求创建订单,订单服务回复创建结果。
  • OSO 向支付服务请求支付,支付服务回复处理结果。
  • 主业务逻辑接收并处理 OSO 事务处理结果回复。

中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。

基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。

5.7.4 事件编排

没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。

在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。

当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。
在这里插入图片描述

上图步骤如下:

  • 事务发起方的主业务逻辑发布开始订单事件。
  • 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
  • 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
  • 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
  • 主业务逻辑监听订单已支付事件并处理。

事件/编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。

5.7.5 优缺点

优点

命令协调设计的优点如下:

  • 服务之间关系简单,避免服务之间的循环依赖关系,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器。
  • 程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
  • 易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试。

事件/编排设计优点如下:

  • 避免中央协调器单点故障风险。
  • 当涉及的步骤较少服务开发简单,容易实现。
缺点

命令协调设计缺点如下:

  • 中央协调器容易处理逻辑容易过于复杂,导致难以维护。
  • 存在协调器单点故障风险。

事件/编排设计缺点如下:

  • 服务之间存在循环依赖的风险。
  • 当涉及的步骤较多,服务间关系混乱,难以追踪调测。

由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证隔离性。

当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。

5.8 Seata

上面我们讲了那么多理论知识,可是没有任何具体的实现,下面引入我们的主角Seata

Seata的设计目标是对业务无侵入,因此从业务无侵入的2PC方案着手,在传统2PC的基础上演进。
它把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务。

在这里插入图片描述

Seata主要由三个重要组件组成:

  • TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
  • TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
  • RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。

5.8.1 AT模式

seata目前支持多种事务模式,分别有AT、TCC、SAGA 和 XA,下面具体讲一讲AT模式
AT模式的特点就是对业务无入侵式,整体机制分二阶段提交(2PC)

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    1.提交异步化,非常快速地完成
    2.回滚通过一阶段的回滚日志进行反向补偿。

在 AT 模式下,用户只需关注自己的业务SQL,用户的业务SQL 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
在这里插入图片描述

Seata的执行流程如下:

    1. A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID
    1. A服务的RM向TC注册分支事务,并及其纳入XID对应全局事务的管辖
    1. A服务执行分支事务,向数据库做操作
    1. A服务开始远程调用B服务,此时XID会在微服务的调用链上传播
    1. B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖
    1. B服务执行分支事务,向数据库做操作
    1. 全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚
    1. TC协调其管辖之下的所有分支事务, 决定是否回滚

5.8.2 Seata实现2PC与传统2PC的差别

  1. 架构层次方面,传统2PC方案的 RM 实际上是在数据库层,RM本质上就是数据库自身,通过XA协议实现,而 Seata的RM是以jar包的形式作为中间件层部署在应用程序这一侧的。

  2. 两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率。

后面我们详细讲解一下Sega事务以及Sega的具体实现《SpringCloud alibaba-分布式事务:Seata》

5.9 各个方案的常见的使用场景

  • 2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。

  • TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。

  • 本地消息表/MQ 事务:都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。

  • Saga 事务:由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
华为内部 Java 软件项目开发过程中,常用的分布式事务解决方案主要有两种: 1. 基于XA协议的分布式事务解决方案 2. 基于TCC(Try-Confirm-Cancel)模式的分布式事务解决方案 其中,基于XA协议的分布式事务解决方案是比较成熟和常用的方案之一。它通过在分布式事务参与者之间协调工作,保证了分布式事务的原子性、一致性、隔离性和持久性。在华为内部,XA协议的分布式事务解决方案通常使用Seata框架来实现。 具体的实现步骤如下: 1. 在分布式事务的发起方(一般为业务系统)使用Seata提供的`@GlobalTransactional`注解标识事务方法。 2. 在事务方法中,调用多个分布式事务参与者(一般为微服务),并使用Seata提供的`@Transactional`注解标识每个参与者的事务方法。 3. 在Seata服务端,根据全局事务ID和分支事务ID,对所有分支事务进行协调,并最终提交或回滚整个分布式事务。 例如,下面的代码演示了如何使用Seata框架实现基于XA协议的分布式事务: ```java @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private StorageService storageService; @GlobalTransactional public void createOrder(Order order, Integer count) { // 1. 创建订单 orderMapper.insert(order); // 2. 扣减库存 storageService.deduct(order.getProductId(), count); } } @Service public class StorageService { @Autowired private StorageMapper storageMapper; @Transactional public void deduct(Long productId, Integer count) { // 1. 查询库存 Storage storage = storageMapper.selectByProductId(productId); if (storage.getCount() < count) { throw new RuntimeException("库存不足"); } // 2. 扣减库存 storage.setCount(storage.getCount() - count); storageMapper.updateByPrimaryKey(storage); } } ``` 其中,`@GlobalTransactional`注解标识了`createOrder()`方法是一个分布式事务方法,`@Transactional`注解标识了`deduct()`方法是一个分支事务方法。Seata框架会在运行时对这些注解进行解析,并自动协调所有的分支事务,保证整个分布式事务的一致性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZNineSun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值