1. 基础概念

1.1. 什么是事务

举个例子,你去用支付宝付款给小卖部的老板。货和钱必须要要全部成功,不会你钱没付成功,老板把货给你,老板货没给你,你不会把钱给他。只要交货和付款任意一方出现问题,那么这次交易就认为失败,事务将撤销本次交易。
分布式事务.png
通过上述的例子,我们可以将事务定义为:一个大的活动,由许多的小活动组成,在举行这个活动时要么全部成功,要么全部失败。不会出现部分活动成功或失败。

1.2. 本地事务

在实际业务开发中,我们的事务实现一般采用关系型数据库来控制事务,这是利用关系型数据库本身的事务特性来实现的,因此叫数据库事务。我们的单体应用主要靠数据库来实现事务的控制,而应用又和关系型数据库在同一个服务器上,所以我们叫这种基于关系型数据库的事务称为本地事务
数据库事务的四大特性(ACID)

  1. A(Atomic):原子性,描述的是事务的执行要么全部成功,要不全部失败,不会出现部分成功或者失败的情况
  2. C(Consistency):一致性,事务执行前后,数据库的一致性没有被破坏。比如:王五向张三转账100元,那么王五的账户就要减100元,张三的账户要增加100元。我们称这种事务执行前后数据的正确性叫一致性
  3. I(IsoIation):隔离性,数据库的事务一般都是并发的,就会存在并发的情况。在并发时,多个事务的执行互不干扰,一个事务看不到另一个事务的运行过程。通过数据库的配置可以避免脏读、幻读、重复度等问题
  4. D(Durability):持久性,事务完成之后,事务会对数据库的更改持久化到数据库中,并且不会回滚

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

1.3. 分布式事务

我们把一个系统拆分成多个可以独立部署的微服务,因此需要服务与服务之间的调用来完成一个事务的操作。这种分布式环境下服务与服务之间通过网络远程调用实现的事务称作分布式事务。

通过一个转账的例子,来描述下分布式事务

  1. begin transaction
  2. //1. 本地数据库事务向张三转账100元,王五账户扣减100元
  3. //2. 远程调用资产服务向张三转账100元,张三账户增加100元
  4. commit transaction

通过上面的例子,如果张三账户增加100元成功了,但是由于网络延时等原因没有返回结果,导致本地数据库事务回滚(王五扣减100元)。此时王五和张三的数据库会不一致了。
因此在分布式事务中,传统的本地事务已经无法解决了。因为王五和张三的账户根本不在同一个数据库中,采用本地事务的方式无法控制两个数据库的事务,导致分布式的事务问题

1.4. 分布式事务的产生场景

1.4.1. 跨JVM进程产生分布式事务

如订单微服务和库存微服务,下单的同时订单服务会去调用库存服务扣减库存。
分布式事务-第 2 页.jpg

1.4.2. 跨数据库实例产生分布式事务

用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。

分布式事务-第 3 页.jpg

1.5. 数据一致性

数据一致性问题总体可以分为数据多副本、调用超时、缓存和数据库数据不一致、多个缓存节点数据不一致几类

1.5.1. 数据多副本

如果数据的存储在多副本的情况,当网络、服务器等出现故障时,可能会导致一部分副本写入成功,一部分副本没有写入失败,造成各个副本之间的数据不一致

1.5.2. 调用超时

调用超时分为同步调用异常和异步调用异常
同步调用是指A服务同步调用B服务,由于网络或者服务器等异常导致超时,就会出现A服务和B服务之间的数据不一致的问题
异步调用是指A服务异步调用B服务,同样由于网络或者服务器等异常导致A服务调用B服务失败,出现A服务和B服务之间的数据不一致的情况。

1.5.3. 缓存与数据库不一致

在高并发场景下,我们一般会对一些热点数据从数据库中查询出来放进缓存中。此时如果我们对数据库进行了增、删、改操作,缓存中的数据如果没有及时更新就会导致缓存和数据库数据不一致问题

1.5.4. 多个缓存节点数据不一致

在缓存集群中,如Redis集群,由于网络等异常原因引起问题,就会导致多个多个缓存节点数据不一致问题

2. 分布式事务基础理论

通过分布式的基础概念,我们已经知道分布式事务与本地事务的区别,分布式事务提供各个节点在不同的服务器或者是跨JVM进程上,相互之间是通过网络交互的。不能因为网络问题而导致整个系统无法提供服务,所以网络问题成为解决分布式事务考量标准之一。因此,分布式事务需要更进一步的理论支持,我们就衍生出CAP理论。在分析解决分布式事务之前需要先学习一些理论基础,通过理论知识指导我们确定分布式事务控制的目标,帮助我们解决每个方案

2.1. CAP理论

2.1.1. 理解CAP理论

CAP 是Consistency、Availbility、Partition tolerance 三个词语的缩写,分别表示:一致性、可用性、分区容错性。
我们通过一个例子来分别解释什么一致性、可用性、分区容错性。
订单系统向主数据库写入订单信息
主数据库同步数据到从数据库
向订单系统返回写入结果
订单系统请求从数据库查询订单信息

分布式事务-第 4 页.jpg

C- 一致性
我们的数据一致性是指在数据存在多个副本(从数据库)时,对一份数据进行更新操作时(包括新增、修改、删除),要么同步成功到所有的副本上,要么同步失败到所有的副本上。也就是说一致性要求对所有副本的数据修改是一个原子操作,要么全部成功,要么全部失败。

  • 如何实现一致性
  1. 写入主数据库后将数据同步到从数据库
  2. 主数据库将数据同步到从数据库,会存在一定的延时,这个过程需要对从数据库锁定,避免从数据库读取到旧的数据(主、从数据库数据不一致),等待数据成功同步到从数据库在释放锁。
  • 一致性的特点
  1. 主数据库同步到从数据库会存在一定的延迟
  2. 为了保证读取数据和写入数据一致,在主数据库同步到从数据库时锁定从数据库,等数据同步成功后释放锁资源
  3. 主数据库同步到从数据库成功,返回成功的结果。反之没有同步成功返回失败结果

A- 可用性
可用性是指系统访问数据的时候,都能够得到响应。不会出现响应超时和报错,就算超时也要返回一个事先定义好的结果

  • 如何实现可用性
  1. 写入主数据库后将数据同步到从数据库
  2. 由于要保证从数据库的可用性,所以不能对从数据库上锁
  3. 查询从数据库时,从数据库一定要返回数据,哪怕主数据没有同步过来也要把旧数据返回出去,旧数据没有时,返回一个默认的数据
  • 可用性的特点
  1. 所有的请求都会有响应
  2. 不会存在响应超时或错误的情况

P- 分区容忍性
将分布式系统拆分为多个微服务部署在多个节点上,并且这些节点处于不同的网络中,这就形成了网络分区,此时不可避免的会出现网络问题,导致节点之间的通信出现失败的情况,但是此时系统还能对外提供服务,这就是分区容错性

  • 如何实现分区容忍性
  1. 主数据库同步到从数据库时,采用异步的方式代替同步方式
  2. 尽量多的增加一些数据库节点,如果一个节点挂掉,其他从数据库节点继续能提供服务
  • 分区容忍性的特点
  1. 一个节点挂掉,不影响其他节点对外提供服务的能力
  2. 分区容忍性是分布式系统必须具备的基础能力

2.1.2. CAP组合方式

在分布式系统中,不会同时存在CAP三个特性,因为我们说过P(分区容错性)是分布式系统必须具备的基本能力,所以我们只能看C和A了,而C和A又不能共存,为什么呢?
通过上面的例子分析为什么C和P不能共存,上面例子中讲到订单系统向主数据库写入数据,主数据库同步数据到从数据库,返回写入结果,然后订单系统查询从数据库。
如果要满足C(一致性),当订单信息写入主数据库后同步到从数据库失败时,导致主从数据不一致,在查询从数据库时就要返回错误信息或超时信息
而要满足P(可用性),当订单信息写入主数据库后同步到从数据库失败时,导致主从数据不一致,在查询从数据库时不能返回错误信息,一定要给系统响应结果,就算是返回旧数据或者默认数据也可以

由此可见,在满足分区容错性的情况下,C和P是相互矛盾的。

CAP的组合方式

  1. AP

放弃一致性,追求分区容忍性和可用性。这也是很多分布式系统的设计选择。
在实际项目开发中,采用AP的组合方式,这不意味着就放弃了一致性。架构方案应该采用的是最终一致性,允许多个节点的数据在一定的时间内存在差异,但是一定时间过后最终数据要一致

  1. CP

放弃可用性,追求分区容忍性和一致性。这种组合方式对数据库的一致性要求比较高,追求的是强一致性
在项目开发中,跨行转账业务需要每个银行系统都执行完转账操作,整个事务才算完成,这是典型的CP方式

  1. CA

放弃分区容忍性,追求系统一致性和可用性。此时系统不会进行分区,也不会考虑网络问题和节点挂掉问题。主数据库和从数据库不会进行数据同步,此时系统也不再是一个标准的分布式系统

2.1.3. CAP总结

CAP是一个已经被证实的理论,一个分布式系统最多只能同时满足一致性、可用性和分区容忍性这个中的两个。它可以作为我们进行架构设计、技术选型的参考标准。对于多数大型互联网应用场景,节点众多,部署分散,集群规模大,所以节点故障是常态,而且要保证可用性达到N个9(99.99…%),并要达到良好的响应性能来提高用户体验,因此一般都会做出保证P和A,放弃C强一致性保证最终一致性的选择

2.2. Base理论

分布式系统最多只能同时满足CAP理论中的两个特性,大部分分布式系统都会采用AP方式,保证可用性和分区容忍性,抛弃一致性(注意这里的一致性是指强一致性,也就是我们CAP理论中的C)。虽然我们不能保证系统的强一致性,但是我们通常情况下要保证最终一致性。最终一致性是指系统允许一定时间内数据不一致,过了一定时间后数据要保证一致。

2.2.1. 概述

Base 理论是对CAP理论中AP组合的一个扩展,通过抛弃强一致性来获得系统的可用性。Base理论的缩写是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)的缩写。当系统出现故障时,Base理论允许部分数据不可用,但是会保证核心功能可用。允许数据在一段时间内不一致,但经过一段时间后,数据最终是一致的。符合Base理论的事务我们称之为柔性事务。

2.2.2. 基本可用

基本可用是指分布式系统出现故障时,允许损失系统的部分可用性,比如响应时间或者功能上的损失,但是要保证系统基本可用。例如:在电商业务场景中,添加购物车和下单功能出现故障,但是商品浏览功能可用

2.2.3. 软状态

软状态是指系统中允许存在中间状态,这些中间状态不影响系统的整体功能可用性,只是允许系统各个节点之间的数据存在同步延迟。例如:电商系统中,订单中的“支付中”、“退款中”等状态就是中间状态。当达到一段时间后,就会变成“支付成功”或者“退款成功”的状态。

2.2.4. 最终一致性

最终一致性是指系统各个节点的数据副本经过一段时间的同步,最终能够达到一致性。最终一致性并不需要各个节点中的数据实时一致(这和强一致性区别,强一致性是要求数据实时一致)。例如:订单中的“支付中”、“退款中”等状态,最终变成“支付成功”、“退款成功”的状态,经过一段时间的延迟,能够使得订单中的状态最终交易结果一致

3. 强一致性分布式事务解决方案

3.1. 概述

最早采用的是符合CAP理论的强一致性事务方案来解决分布式事务问题,强一致性分布式事务要求在任意时刻一个全局事务内操作各个节点的数据都是一致的。

3.2. 适用场景

在分布式事务解决方案中,强一致性事务要求系统在任何时间,读取任一节点上的数据都是最新写入的。强一致性事务主要用于对数据一致性要求比较高,在任意时刻都要查询到最新写入的数据场景。例如:跨行转账业务,张三向王五转账100元,那么张山账户扣减100元,王五账户增加100元,这两个操作要么全部成功,要么全部失败。不存在一个成功,另一个失败的情况

3.3. 优缺点

强一致性方案的优点:

  1. 数据一致性比较高
  2. 在任意时刻都能查询到最新写入的数据

强一致性方案的缺点:

  1. 存在性能问题,在分布式事务未完全提交或者回滚之前,应用程序不会查询到最新的数据
  2. 实现复杂
  3. 牺牲了可用性
  4. 不适合高并发场景

3.4. 2PC模型

2PC 模型是指两阶段提交协议模型,这种模型将整个事务流程分为Prepare阶段和Commit阶段。

3.4.1. 2PC 模型的执行流程

2PC 模型的执行流程分为Prepare和Commit 两个阶段

  1. Prepare 阶段

在Prepare阶段,事务管理器给每个参与全局事务的资源管理器(分支事务)发送Prepare消息,资源管理器要么返回失败,要么在本地执行相应的事务,并将事务写入本地的Redo Log文件和Undo Log文件,此时事务并没有提交

  1. Commit 阶段

如果事务管理器收到参与全局事务的资源管理器返回的失败结果,则直接给Prepare阶段执行成功的资源管理器发送回滚消息,否则向每个资源管理器发送Commit消息。对应的资源管理器根据事务管理器发送过来的指令,执行回滚或者提交操作,并且释放事务处理过程中使用的锁资源

3.4.2. 事务执行成功流程

事务管理器向资源管理器发送Prepare消息,资源管理器收到消息后,将事务写入本地的Redo Log和Undo Log 日志,并向事务管理器返回执行成功的状态
分布式事务-第 5 页.jpg

事务管理器接收到参与全局事务的资源管理器返回成功消息后,再由事务管理器向参与全局事务的资源管理器发送Commit消息,资源管理器接收到消息后提交本地事务,并返回提交成功的给事务管理器,同时释放资源
分布式事务-第 6 页.jpg

3.4.3. 执行事务失败流程

执行事务失败的情况,在事务管理器向资源管理器发送Prepare消息时,资源管理器接收到消息后,将事务写入本地的Redo Log和Undo Log日志失败,那么会向事务管理器返回执行失败的消息
分布式事务-第 7 页.jpg
事务管理器接收到资源管理器返回的失败消息时,在Commit阶段,事务管理器会向Prepare 阶段执行事务成功的资源管理器发送Rollback 消息,对应的资源管理器收到Rollback消息后回滚本地事务,并将回滚成功的消息返回给事务管理器
分布式事务-第 8 页.jpg

3.4.4. 2PC 模型存在的问题

结合上面的事务的执行流程,我们分析2PC模型是存在一些问题的

  1. 同步阻塞问题:事务执行过程中,所有参与事务的节点都会占用公共资源加锁,导致其他访问公共资源的进程或者线程阻塞
  2. 单点故障问题:如果事务管理器发生故障,那么资源管理器会一致阻塞
  3. 数据不一致问题,如果在Commit阶段。资源管理器(分支事务)由于宕机或者网络故障导致接收不到资源管理器发送过来的Commit消息,也就是说分之事务没有提交,就会导致数据不一致问题
  4. 无法解决的问题:在Commit阶段,如果事务管理器向资源管理器发送Commit消息后马上宕机了,资源管理器并且接收Commit消息的资源管理器也宕机了,那么在这时事务管理器是无法知道事务到底是否已经提交

3.5. 3PC 模型

前面我们通过2PC的理论知识已经知道2PC的一些缺陷问题,现在我们在2PC的基础上进行改进,两阶段提交变为三阶段提交,也就是3PC三阶段提交模型
3PC三阶段模型是指将事务分为三个阶段执行,分别是CanCommit阶段,PreCommit阶段和doCommit阶段。只有事务管理器和资源管理器在这三个阶段都执行成功,整个分布式事务才算执行成功

3.5.1. 事务执行成功流程

在CanCommit阶段,事务管理器向资源管理器发送CanCommit消息,资源管理器接收到消息后认为能够执行事务,会向事务管理器返回Yes消息,进入预备状态
分布式事务-第 9 页.jpg
在PreCommit阶段,事务管理器会向参与全局事务的资源管理器发送PreCommit消息,资源管理器接收到消息后执行事务操作,将Undo Log 和 Redo Log 信息写入事务日志,并向事务管理器响应Ack 状态,此时不会提交事务
分布式事务-第 10 页.jpg
在doCommit阶段,当所有PreCommit阶段返回成功时,事务管理器会向参与全局事务的资源管理器发送doCommit消息,资源管理器接收到消息后正式提交事务,并释放事务执行期间占用资源,同时向事务管理器响应事务已提交的状态。事务管理器收到资源管理器响应的事务已提交状态,完成事务的提交
分布式事务-第 11 页.jpg

3.5.2. 事务执行失败流程

在3PC模型中,如果资源管理器在CanCommit 阶段收到事务管理器发送的消息,认为不能执行事务,会向事务管理器发送无法执行事务的No消息,之后事务管理器会在PreCommit 阶段向资源管理器发送准备回滚消息,资源管理器向事务管理器响应准备好事务回滚的消息。在doRollback阶段,事务管理器会向资源管理器发送回滚事务消息
在Cancommit阶段,事务管理器会向参与全局事务的资源管理器发送CanCommit消息,如果资源管理器收到CanCommit消息后,认为不能执行事务会向事务管理器发送NO消息
分布式事务-第 12 页.jpg

在PreCommit阶段,事务管理器会向参与全局事务的资源管理器发送Abort消息,资源管理器收到Abort消息或者期间出现超时,都会中断事务的执行
分布式事务-第 13 页.jpg

在doRollback阶段,事务管理器会向参与全局事务的资源管理器发送Rollback消息,资源管理器会利用Undo Log 日志信息回滚事务,并释放执行事务期间占用的资源,向事务管理器返回事务已经回滚的状态。事务管理器收到资源管理器返回的事务已回滚的消息,完成事务回滚
分布式事务-第 14 页.jpg

3.5.3. 3PC 模型中存在的问题

和2PC模型相比,3PC模型主要解决了单点故障问题,并减少了事务执行过程中产生的阻塞现象,3PC模型中,如果资源管理器无法及时收到来自事务管理器发出的消息,那么资源管理器就会提交事务操作,而不是一直持有事务的资源并处于阻塞状态,但是这种机制会导致数据不一致的问题

如果由于网络故障等原因,导致资源管理器没有及时收到事务管理器发出的Abort消息,则资源管理器会在一段事件后提交事务,这就导致与其他接收到Abort消息并执行了事务回滚的资源管理器数据不一致

4. 最终一致性分布式事务解决方案

4.1. 概述

强一致性分布式事务解决方案要求参与事务的各个节点的数据实时保持一致,查询任意节点的数据都能得到最新的数据结果。这就导致在高并发场景下性能受到影响。最终一致性分布式事务解决方案并不要求参与事务的各个节点的数据实时保持一致,允许其存在中间状态,只要一段时间后,能够达到数据的最终一致状态即可。

4.2. 经典方案

典型的最终一致性分布式事务解决方案

  1. TCC解决方案
  2. 可靠消息最终一致性解决方案
  3. 最大努力通知型解决方案

4.3. 最终一致性方案优缺点

优点:

  1. 性能比较高,这是因为最终一致性分布式事务解决方案不要求数据时刻保持一致,不会因为长时间的持有事务占用资源而消耗过多的性能
  2. 具备可用性
  3. 适合高并发场景

缺点:

  1. 因为数据存在短暂的不一致,所以在某个时刻查询出的数据状态可能会不一样
  2. 不太使用对事务一致性要求比较高的场景(如:支付转账场景)

4.4. TCC解决方案

TCC 是一种典型的解决分布式事务的问题方案,主要解决跨服务调用场景下的分布式事务问题,广泛应用于分布式事务场景

4.4.1. TCC 执行流程

TCC 的三个阶段

  1. Try 阶段

不会执行任何业务逻辑,仅做业务的一致性检查和预留相应的资源,这些资源能够和其他操作保持隔离

  1. Confirm 阶段

当Try阶段所有分支事务执行成功后开始执行Confirm阶段。通常情况下,采用TCC方案解决分布式事务时会认为Confirm阶段是不会出错的。也就是说Try阶段的操作执行成功了,Confirm 阶段就一定会执行成功。如果Confirm阶段出错了,就需要引入重试机制或人工处理,对于出错的事务进行干预

  1. Cancel 阶段

在业务执行异常或者出现错误的情况下,需要回滚事务的操作,执行分支事务的取消操作,并且释放Try 阶段预留的资源。通常情况下采用TCC则认为Cancel阶段也一定会成功。若Cancel阶段真的出错了,需要引入重试机制或人工处理

4.4.2. TCC方案的优缺点

TCC方案在分布式事务中的优点:

  1. 在应用层实现具体逻辑,锁定资源的粒度小,不会锁定所有资源,提升了系统的性能
  2. 在Confirm和Cancel阶段的方法具备幂等性
  3. 由业务方发起整个事务,无论是主业务还是分支事务所在的业务,都能部署为集群模式,从而解决了XA规范的单点故障问题

TCC方案在分布式事务中的缺点:
代码需要耦合到具体的业务中,每个参与分布式事务的业务方法都要拆分成Try、Confirm和Cancel三个阶段的方法,提高开发成本

4.4.3. TCC方案需要注意的问题

使用TCC 方案解决分布式事务问题时,需要注意空回滚、幂等和悬挂的问题

  1. 空回滚问题
  • 空回滚的原因

出现空回滚的原因是一个分支事务所在的服务器宕机或者网络发生异常,此分支事务调用失败,此时并未执行此分支事务Try阶段的方法。当服务器或者网络恢复后,TCC分布式事务执行回滚操作,会调用分支事务的Cancel阶段的方法,如果Cancel阶段的方法不能处理这种情况,就会出现空回滚问题

  • 空回滚问题的解决方案

识别是否出现了空回滚操作的方法是判断是否执行了Try阶段的方法。如果执行了Try阶段的方法,就没有空回滚,否则就会出现空回滚
具体解决方案是在主业务方发起全局事务时,生成全局事务记录,并为全局事务记录生成一个全局唯一的ID,叫作全局事务ID。这个全局事务ID会贯穿整个分布式事务的执行流程。在创建一张分支事务记录表,用于记录分支事务,将全局事务ID和分支事务ID保存到分支事务表中。执行Try阶段的方法时,会向分支事务记录表插入一条记录,包含全局事务ID和分支事务ID,表示执行了Try阶段。当事务回滚执行Cancel阶段的方法时,首先读取分支事务表中的数据,如果存在Try阶段插入的数据,则执行正常操作回滚事务,否则为空回滚,不做任何操作

  1. 幂等问题
  • 幂等问题出现的原因

由于服务器宕机或者网络异常等原因,可能会出现方法调用超时的情况,为了保证方法的正常执行,往往会在TCC方案中加入超时重试机制。因为超时重试有可能导致数据不一致问题,所以需要保证分支事务的执行以及TCC方案的Confirm阶段和Confirm阶段具备幂等性

  • 幂等问题的解决方案

解决方案是在分支事务记录表中增加事务的执行状态,每次执行分支事务以及Confirm阶段和Cancel阶段的方法时,都查询此事务的执行状态,以此判断事务的幂等性

  1. 悬挂问题
  • 悬挂问题出现的原因

在TCC分布式事务中,通过RPC调用分支事务Try阶段的方法时,会先注册分支事务,再执行RPC调用。如果此时发生服务器宕机或者网络异常等情况,RPC调用就会超时,事务管理器会通知对应的资源管理器回滚事务。可能资源管理器回滚完事务后,RPC请求达到了参与分布式事务所在的业务方法,因为此时事务已经回滚,所以在Try阶段预留的资源就无法释放。这种情况,就称为悬挂。总之,悬挂问题就是预留业务资源后,无法继续往下处理。

  • 解决悬挂问题的方案

解决方案的思路是如果执行了Confirm阶段或者Cancel阶段的方法,则Try阶段的方法就不能在执行。具体方案在执行Try阶段的方法时,判断分支事务记录表中是否已经存在同一全局事务下的Confirm阶段或者Cancel阶段的事务记录,如果存在,则不再执行Try 阶段方法

4.5. 可靠消息最终一致性解决方案

可靠消息最终一致性分布式事务解决方案是指事务的发起方执行完本地事务之后,发出一条消息,事务的参与方(消息的消费者)一定能够收到这条消息并处理成功。这个方案强调的是只要事务发起方将消息发送给事务参与方,事务参与方就一定能够执行成功,数据达到最终一致性的状态

4.5.1. 执行流程

可靠消息最终一致性解决方案中,事务发起方执行完本地事务后,通过可靠消息服务将消息发送给事务参与方,事务参与方接收到消息后,一定能够执行成功。这里可靠消息服务可以通过本地消息表实现,也可以通过消息中间件实现,如:RocketMQ

首先事务发起方将消息发送给可靠消息服务,这里的可靠消息服务可以基于本地数据表实现,也可以基于消息队列中间件实现。然后事务参与方从可靠消息服务中接收消息,事务发起方和可靠消息服务之间、可靠消息服务和事务参与方之间都是通过网络进行通信的。由于网络本身的不稳定性,可能会造成分布式事务问题,因此在实现上,需要引入消息确认服务和消息恢复服务
消息确认服务会定期检测事务发起方业务的执行状态和消息库中的数据,如果发现事务发起方业务的执行状态与消息库中的数据不一致,消息确认服务就会同步事务发起方的业务数据和消息库中的数据,保证数据一致性,确保事务发起方业务完成本地事务后消息一定会发送成功
消息恢复服务会定期检测事务参与方业务的执行状态和消息库中的数据,如果发现事务参与方业务的执行状态和消息库中的数据不一致(这里的不一致,通常指的是事务参与方消费消息后,执行本地事务操作失败,导致事务参与方本地事务的执行状态与消息库中的数据不一致),消息恢复服务就会恢复消息库中消息的状态,使消息的状态回滚为事务发起方发送消息成功,但未被事务参与方消费的状态

4.5.2. 方案优缺点

消息最终一致性方案的可靠消息服务可以基于本地消息表和消息队列中间件两种方式实现

  1. 基于本地消息表实现的最终消息一致性方案
  • 优点

在业务应用中实现了消息的可靠性,减少了对消息中间的依赖

  • 缺点

绑定了具体的业务场景,耦合性太高,不可公用和扩展
消息(本地可靠消息)和数据库在同一个数据库中,占用了业务系统的资源
消息数据可能会受到数据库并发性的影响

  1. 基于消息列队中间件实现的最终消息一致性方案
  • 优点

消息数据独立于业务数据,与具体的业务数据库解耦
消息的并发性和吞吐量优于本地消息表方案

  • 缺点

发送一次消息需要完成两次网络交互,一次是消息的发送,一次是消息提交或者回滚
需要实现消息的会查接口,增加开发成本

4.5.3. 注意问题

使用可靠消息最终一致性方案解决分布式事务问题时,需要注意本地事务与消息发送的原子性问题、事务参与方接收消息的可靠性和幂等问题

  1. 本地事务与消息发送的原子性问题
  • 原子性问题产生的原因

可靠消息最终一致性要求事务发起方的本地事务与消息发送的操作具有原子性。简而言之本地事务和消息发送要么一起成功,要么一起失败。

  • 原子性问题的解决方案

在实际的解决方案中,可以通过消息确认服务解决本地事务和消息发送的原子性问题

  1. 事务参与方接收消息的可靠性问题
  • 可靠性问题产生的原因

由于服务器宕机、服务奔溃或者网络异常等原因,导致事务参与方不能正常的接收到消息,或者接收消息后处理事务的过程发生异常,无法将结果正确回传到消息库中。此时就会产生消息可靠性问题

  • 可靠性问题的解决方案

可以通过消息恢复服务保证事务参与方的消息可靠性

  1. 事务参与方接收消息的幂等性问题
  • 幂等问题产生的原因

由于某种原因,可靠消息可能会多次向事务参与方发送消息,如果事务参与方的方法不具备幂等性,就会造成消息重复消费的问题,这就是典型的幂等性问题

  • 幂等性问题的解决方案

事务参与方的方法实现要具备幂等性,只要参数相同,无论调用多少次事务参与方的方法,返回的结果和第一次调用返回的结果一样

4.5.4. 可靠性保证

消息可靠性分布式事务解决方案最大的问题是如何保证消息能够正常的发送出去,业务参与方如何能够正确的接收到消息。
通过消息发送的一致性、消息接收的一致性、消息可靠性三个方面来保证消息的可靠性

4.5.5. 消息发送的一致性

消息发送的一致性是指事务发起方执行本地事务和发送消息的原子性问题,也就是说事务发起方执行本地事务成功,就一定要保证消息成功的发送出去

4.5.5.1. 消息发送与确认机制

消息发送的一致性设计消息的发送与确认机制。常规消息中间件的消息发送与确认机制如下:

  1. 消息生产者生成消息并将消息发送给消息中间件。这里可以通过同步或者异步的方式发送
  2. 消息中间件接收到消息后,将消息数据持久化存储到磁盘。这里可以根据配置调整存储策略
  3. 消息中间件向生产者返回消息的发送结果。这里返回的可以是消息发送的状态,也可以是异常信息
  4. 消息消费者监听消息中间件并消费指定主题中的数据
  5. 消息消费者获取消息中间件中的数据后,执行本地的业务逻辑
  6. 消息消费者对已经成功消费的消息向消息中间件进行确认,消息中间件收到消费者反馈的确认消息后,将确认后的消息从消息中间件中删除

一般情况下,常规的消息中间件对消息的处理流程无法实现消息发送的一致性,因此直接使用现成的消息中间件无法完全实现消息发送的一致性。在实现分布式事务时,需要手动开发消息发送与确认机制来满足消息发送的一致性
分布式事务-第 16 页.jpg

4.5.5.2. 如何保证消息发送的一致性

要保证消息发送的一致性,就要实现消息的发送与确认机制。事务发起方向消息中间件成功发送消息后,消息中间件向事务发起方返回消息发送成功的状态。当事务参与方接收到消息并处理完事务操作后,需要向消息中间件发送确认信息。
消息发送一致性流程

  1. 事务发起方向消息中间件发送待确认消息
  2. 消息中间件接收到事务发起方发送过来的消息,将消息存储到本地数据库,此时并不会向事务参与方投递消息
  3. 消息中间件向事务发起方返回消息存储结果,事务发起方根据返回的结果确定执行业务逻辑。当消息中间件向事务发起方返回结果为“存储成功”时,事务发起方会执行后续的业务逻辑。否则事务发起方不在执行后面的业务逻辑。
  4. 事务发起方完成业务处理后,把业务处理的结果发送给消息中间件
  5. 消息中间件收到事务发送方送过来的结果数据后,根据结果确定后续的处理逻辑。如果事务发起方送过来的结果为“成功”,消息中间件会更新本地数据库中的消息状态为“待发送”。否则将本地数据库中的消息状态标记为“已删除”,或者直接删除数据库中相应的消息记录
  6. 事务参与方监听消息中间件,并接收状态为“待发送”的消息,当收到消息中间件的消息后,会执行对应的业务逻辑,消息中间件中对应的记录变更为“已发送”
  7. 事务参与方的业务操作完成后,会向消息中间件发送确认消息,表示事务参与方已经收到消息并且执行完对应的业务逻辑,消息中间件会将消息从本地数据库中删除
  8. 为了保证事务发起方一定能够将消息发送出去,在事务发起方的应用服务器中需要暴漏一个回调查询接口。消息服务在后台开启一个线程,定时扫描服务中状态为“待发送”的消息,回调事务发起方提供回调查询接口,根据消息服务中的业务参数回查事务发起方本地事务的执行状态。如果消息服务查询到事务发起方的事务状态为“执行成功”,同时消息中间件的消息状态为“待发送”,则将对应的消息投递出去,并且将对应的消息记录更新为“已发送”。如果消息服务查询到事务发起方的执行状态为“实行失败”,则消息服务会删除消息中间件中对应的消息,不在投递
  9. 消息中间件也会根据状态向事务发起方投递事务参与方的执行状态,事务发起方会根据状态执行对应的操作,比如事务回滚等

分布式事务-第 17 页.jpg

4.6. 最大努力通知解决方案

最大努力通知型分布式事务解决方案,适用于跨越多个系统,尤其是跨越不同企业之间的事务时使用比较合适。

4.6.1. 执行流程

在最大努力通知型分布式事务解决方案在执行过程中,允许丢失消息,但需要业务主动方提供事务状态查询接口,以便业务被动方主动调用并恢复丢失的业务数据。
最大努力通知事务解决方案需要实现如下功能:

  1. 业务主动方在完成业务处理后,会向业务被动方发送消息通知。发送消息通知时允许消息丢失(可靠消息一致性事务解决方案是不允许消息丢失的)
  2. 业务主动方可以设置时间阶梯通知规则,在消息发送失败后,可以按照规则再次通知,知道达到最大通知次数为止
  3. 业务主动方需要提供查询接口供被动方查询,用于恢复丢失消息,也可以理解为消息丢失后的一种补偿机制

4.6.2. 方案优缺点

  1. 优点
  • 能够实现跨企业、跨系统的数据不一致性问题
  • 业务被动方的处理结果不会影响业务主动方的处理结果
  • 能够快速接入其他业务系统,达到业务数据一致性
  1. 缺点
  • 只适用于时间敏感度低的场景
  • 业务主动方发送的消息可能丢失,造成业务被动方收不到消息
  • 需要业务主动方提供查询接口,业务被动方需要按照主动方的接口查询数据来恢复数据的一致性,增加了开发成本

4.6.3. 注意问题

业务被动方需要保证接收通知的方法的幂等性,关键是要业务主动方通过一定的机制最大限度地将业务的处理结果通知给业务被动方,因此需要解决两个问题

  1. 消息重复通知产生的问题
  • 消息重复通知产生的原因

由于业务主动方发送消息通知后,业务被动方不一定能够接收到消息,因此需要按照一定时间阶梯重复向业务被动方发送消息通知。此时就出现了消息重复通知的情况,因为业务被动方的方法被执行多次,有可能造成数据不一致的问题

  • 消息重复通知的解决方案

保证业务被动方接收消息通知的方法具备幂等性,则在业务上就能够解决消息重复通知的问题

  1. 消息通知丢失的问题
  • 消息通知丢失产生的原因

如果业务主动方尽最大努力都没有把消息发送给业务被动方,或者业务被动方接收到消息后,需要再次获取消息。此时,业务主动已经删除对应的通知消息,不再向业务被动发发送消息通知。也就是说消息丢失

  • 消息通知丢失的解决方案

业务主动方提供查询消息的接口来满足业务被动方主动查询消息的需求,用来恢复丢失的业务。业主主动方再设计接口时要考虑到接口的安全性和并发性

4.7 最大努力通知和可靠消息最终一致性的区别

最大努力通知型方案和可靠消息一致性事务解决方案有着本质的不同,主要体现在设计不同,业务场景和解决方案不同3个方面

  1. 设计不同
  • 可靠消息最终一致性方案需要事务发起方一定要将消息发送成功
  • 最大努力通知型方案中,业务主动方尽最大努力将消息发送给业务被动方,但消息可能丢失,业务被动方不一定能够接收到消息
  1. 业务场景不同
  • 可靠消息适用于时间敏感度高的场景,以异步的方式达到事务的最终一致性
  • 最大努力型通知方案适用于时间敏感度低的场景,业务主动方只需要将处理结果通知出去
  1. 解决的问题不同
  • 可靠消息最终一致性方案解决的是消息是从事务发起方发出,到事务参与方接收的一致性,并且事务参与方接收到消息后,能够正确的执行事务操作,达到最终数据一致性
  • 最大努力通知型方案虽然无法保证消息能够从业务主动方发送到业务被动方的一致性,但是能够提供消息接收的可靠性。这里的可靠性包括业务被动方能够接收到业务主动方通知的消息和业务被动方能够主动查询业务主动方提供的消息回查接口,来恢复丢失的业务

5. XA强一致性分布式事务

使用Atomikos 框架实现XA强一致性分布式事务,模拟跨库转账业务场景,不同账户之间转账操作通过同一个项目程序完成。

5.1. 业务说明

转账服务不会直接连接数据库进行转账操作,而是通过Atomikos框架对数据库连接进行封装,通过Atomikos 框架操作不同的数据库。由于Atomikos 框架内部实现了XA 分布式事务协议,因此转账逻辑处理不用关心分布式事务是如何实现的,只需要关心具体业务逻辑
分布式事务-第 15 页.jpg

5.2. 程序模块说明

  1. 数据库:转出金额数据库tx-xa-01, 转入金额数据库tx-xa-02
  2. MySQL:MySQL 8.0.20
  3. JDK:64位JDK 1.8.0_212
  4. SpringBoot:spring-boot-2.2.6.RELEASE
  5. Atomikos框架:spring-boot-2.2.6.RELEASE 整合版本

5.3. 数据库设计

我们创建两个数据库,一个是转出金额数据库tx-xa-01,一个是转入金额数据库tx-xa-02。两个数据库的表名和表结构一样。数据库表名user_account
分别在转出、转入数据库中创建数据库表

  1. CREATE TABLE `user_account` (
  2. `account_no` varchar(64) NOT NULL DEFAULT '' COMMENT '账户编号',
  3. `account_name` varchar(50) DEFAULT '' COMMENT '账户名称',
  4. `account_balance` decimal(10,2) DEFAULT '0.00' COMMENT '账户余额',
  5. PRIMARY KEY (`account_no`)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

在转出数据库tx-xa-01数据库中插入一条记录

  1. INSERT INTO `tx-xa-01`.`user_account` (`account_no`, `account_name`, `account_balance`) VALUES('1001', '张三', '1000.0');

在转入数据库tx-xa-02数据库中插入一条数据

  1. INSERT INTO `tx-xa-02`.`user_account` (`account_no`, `account_name`, `account_balance`) VALUES('1002', '王五', '1000.0');

5.4. 程序实现

访问Gitee