# 分布式事务原理与设计

# 分布式事务的概念

以往咱们在单机环境里边,事务基本上是靠数据库自身提供的特性来实现的。我们不用操心,但是一旦到了分布式环境,光靠数据库就兜不住了。 不仅是跨进程、跨节点,最主要是还有一个不靠谱的网络。很可能导致部分服务成功而另一部分服务失败,整个业务流程就出问题,一堆脏数据。 在微服务架构下,由于数据库和应用服务的拆分,导致原本一个事务单元中的多个DML操作,变成了跨进程或者跨数据库的多个事务单元的多个DML操作, 而传统的数据库事务无法解决这类的问题,所以就引出了分布式事务的概念。

分布式事务本质上要解决的就是跨网络节点的多个事务的数据一致性问题,业内常见的解决方法有两种:

  1. 强一致性,就是所有的事务参与者要么全部成功,要么全部失败,全局事务协调者需要知道每个事务参与者的执行状态,再根据状态来决定数据的提交或者回滚!
  2. 最终一致性,也叫弱一致性,也就是多个网络节点的数据允许出现不一致的情况,但是在最终的某个时间点会达成数据一致。

基于CAP定理我们可以知道,强一致性方案对于应用的性能和可用性会有影响,所以对于数据一致性要求不高的场景,就会采用最终一致性算法。

在分布式事务的实现上,对于强一致性,我们可以通过基于XA协议下的二阶段提交来实现,对于弱一致性,可以基于TCC事务模型、可靠性消息模型等方案来实现。 市面上有很多针对这些理论模型实现的分布式事务框架,我们可以在应用中集成这些框架来实现分布式事务。 而Seata就是其中一种,它是阿里开源的分布式事务解决方案,提供了高性能且简单易用的分布式事务服务。

# 数据一致性作用

数据一致性是指在分布式系统中,多个节点中存储的数据副本之间保持相同的状态和值,以确保数据的准确性和可靠性。 在分布式系统中,由于数据的复制、分片和分布式事务的并发执行等因素,可能会出现数据不一致的情况,这时候就需要保证数据的一致性。

# 数据一致性的类型有哪些?

  1. 强一致性:在进行写操作后,所有节点必须立即同步,确保所有节点都具有相同的数据值。这种一致性保证了数据的完全一致性,但会降低系统的性能和可用性。强一致性通常用于对数据一致性要求极高的应用场景,如金融交易、电子商务等。
  2. 弱一致性:在进行写操作后,数据不会立即同步,但会在一定时间内达到一致状态。这种一致性保证了系统的性能和可用性,但数据的一致性有时不能得到完全保障。弱一致性通常用于对数据一致性要求不是特别高的应用场景,如社交网络、游戏等。
  3. 最终一致性:在进行写操作后,数据可能出现一段时间内的不一致,但最终会达到一致状态。这种一致性是弱一致性的一种形式,保证了系统的性能和可用性,同时也保证了数据的一致性。最终一致性通常用于对数据一致性要求相对较高,但可以接受一定延迟的应用场景,如云计算、大数据等
  4. 读写一致性:在进行读操作时,读取到的数据必须是最近一次写操作后的数据。这种一致性要求对读操作的响应时间非常快,通常用于对数据一致性要求非常高的应用场景,如金融交易等
  5. 会话一致性: 在同一个会话中,读操作必须读取到最近一次写操作的数据。这种一致性要求对会话的管理和跟踪非常重要,通常用于对数据一致性要求较高的应用场景,如在线编辑、在线协作等

# 数据一致性的重要性是什么?

  1. 数据可靠性: 数据一致性可以保证数据的可靠性,确保多个节点中存储的数据副本之间保持相同的状态和值,从而避免数据的丢失或错误。
  2. 系统可用性: 数据一致性可以提高分布式系统的可用性,确保多个节点中存储的数据副本之间保持一致,从而避免系统出现故障或不可用的情况。
  3. 业务可靠性: 数据一致性可以提高业务的可靠性,确保不同的业务操作之间的数据保持一致,从而避免业务出现错误或不一致的情况。
  4. 用户体验: 数据一致性可以提高用户的体验,确保用户在使用分布式系统时,数据的正确性和一致性,从而提高用户的满意度。
  5. 合规性要求: 一些行业或法规对数据的一致性有着严格的要求,如金融、医疗等行业。数据一致性可以保证企业符合行业和法规的要求。

# 数据一致性在数据库系统中的作用是什么?

数据的准确性: 数据库中的数据一致性可以保证数据库中的数据的准确性,避免出现数据错误或不一致的情况。 数据的完整性: 数据库中的数据一致性可以保证数据库中的数据的完整性,避免出现数据缺失或重复的情况。 事务的正确性: 数据库中的数据一致性可以保证事务的正确性,避免出现事务操作的错误或不一致的情况。 数据的可靠性: 数据库中的数据一致性可以保证数据的可靠性,确保多个节点中存储的数据副本之间保持相同的状态和值,避免数据的丢失或错误。 数据的安全性: 数据库中的数据一致性可以提高数据的安全性,确保数据库中的数据不会被恶意篡改或破坏。

# 2PC阶段提交协议的基本原理

2PC,两阶段提交,将事务的提交过程分为资源准备和资源提交两个阶段,并且由事务协调者来协调所有事务参与者, 如果准备阶段所有事务参与者都预留资源成功,则进行第二阶段的资源提交,否则事务协调者回滚资源。

第一阶段:准备阶段

由事务协调者询问通知各个事务参与者,是否准备好了执行事务,具体流程图如下:

img.png

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

第二阶段:提交阶段

协调者收到各个参与者的准备消息后,根据反馈情况通知各个参与者commit提交或者rollback回滚

事务提交:

当第一阶段所有参与者都反馈同意时,协调者发起正式提交事务的请求,当所有参与者都回复同意时,则意味着完成事务,具体流程如下:

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

所以,正常提交时,事务的完整流程图如下:

img.png

事务回滚:

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

  1. 协调者向所有参与者发出 rollback 回滚操作的请求
  2. 参与者利用阶段一写入的undo信息执行回滚,并释放在整个事务期间内占用的资源
  3. 参与者在完成事务回滚之后,向协调者发送回滚完成的ACK消息
  4. 协调者收到所有参与者反馈的ACK消息后,取消事务

所以,事务回滚时,完整流程图如下:

img.png

# 2PC的缺点:

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

  1. 性能问题:执行过程中,所有参与节点都是事务阻塞性的,当参与者占有公共资源时,其他第三方节点访问公共资源就不得不处于阻塞状态,为了数据的一致性而牺牲了可用性,对性能影响较大,不适合高并发高性能场景
  2. 可靠性问题:2PC非常依赖协调者,当协调者发生故障时,尤其是第二阶段,那么所有的参与者就会都处于锁定事务资源的状态中,而无法继续完成事务操作(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
  3. 数据一致性问题:在阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
  4. 二阶段无法解决的问题:协调者在发出 commit 消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了,那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

# 2pc协议中如何处理参与者与协调者的通信故障?给一些解决方案?

  1. 超时机制:在2PC协议中,每个阶段都有一个预定的超时时间。如果在超时时间内没有收到响应,协调者将会进行相应的处理。例如,如果在第一阶段中协调者无法收到参与者的响应,它可以将参与者视为失败,并通知所有其他参与者回滚事务。
  2. 心跳机制:协调者可以定期向参与者发送心跳消息,以检测参与者的状态。如果协调者在一段时间内没有收到参与者的响应,它可以将参与者视为失败并进行相应的处理。
  3. 预备性提交∶在第一阶段中,协调者可以请求参与者进行预备性提交,并在得到所有参与者的预备性提交确认后,将事务提交请求发给所有参与者。如果在第二阶段中,协调者无法收到某个参与者的确认消息,则可以向该参与者发送回滚请求。
  4. 备份协调者︰在2PC中,可以使用备份协调者来提高系统的可靠性。备份协调者可以监控协调者的状态,并在协调者失效时接替其工作。
  5. 消息队列∶参与者可以将事务日志写入消息队列,协调者可以从消息队列中获取事务日志,并进行相应的处理。如果协调者在处理事务时失效,备份协调者可以从消息队列中获取未处理的事务日志,并继续进行处理。

# 3PC阶段提交协议的基本原理

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

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

所以3PC会分为3个阶段,CanCommit 准备阶段、PreCommit 预提交阶段、DoCommit 提交阶段,处理流程如下:

img.png

阶段一:CanCommit 准备阶段

协调者向参与者发送 canCommit 请求,参与者如果可以提交就返回Yes响应,否则返回No响应,具体流程如下:

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

阶段二:PreCommit 阶段

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

执行事务:

假如所有参与者均反馈 yes,协调者预执行事务,具体如下:

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

img.png

中断事务

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

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

img.png

阶段三:doCommit阶段

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

提交事务:

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

img.png

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

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

img.png

进入doCommit阶段后,无论协调者出现问题,或者协调者与参与者之间的网络出现问题,都会导致参与者无法接收到协调者发出的 doCommit 请求或 abort 请求。 此时,参与者都会在等待超时之后,继续执行事务提交。这其实基于概率来决定的,当进入第三阶段时,说明第一阶段收到所有参与者的CanCommit响应都是Yes, 意味着大家都同意修改了,并且第二阶段所有的参与者对协调者的PreCommit请求也都是同意的。所以,一句话概括就是,当进入第三阶段时, 由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大

# 3PC的优缺点

  1. 与2PC相比,3PC降低了阻塞范围,并且在等待超时后,协调者或参与者会中断事务,避免了协调者单点问题,阶段三中协调者出现问题时,参与者会继续提交事务。
  2. 数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者因为网络问题无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
  3. 2PC和3PC都无法保证数据绝对的一致性,一般为了预防这种问题,可以添加一个报警,比如监控到事务异常的时候,通过脚本自动补偿差异的信息。

# TCC事务原理

TCC(Try Confirm Cancel)是应用层的两阶段提交,所以对代码的侵入性强,其核心思想是:针对每个操作,都要实现对应的确认和补偿操作, 也就是业务逻辑的每个分支都需要实现 try、confirm、cancel 三个操作,第一阶段由业务代码编排来调用Try接口进行资源预留, 当所有参与者的 Try 接口都成功了,事务协调者提交事务,并调用参与者的 confirm 接口真正提交业务操作,否则调用每个参与者的 cancel 接口回滚事务, 并且由于 confirm 或者 cancel 有可能会重试,因此对应的部分需要支持幂等

TCC的执行流程可以分为两个阶段,分别如下:

  1. 第一阶段:Try,业务系统做检测并预留资源 (加锁,锁住资源),比如常见的下单,在try阶段,我们不是真正的减库存,而是把下单的库存给锁定住
  2. 第二阶段:根据第一阶段的结果决定是执行confirm还是cancel。
    • Confirm:执行真正的业务(执行业务,释放锁)
    • Cancle:是对Try阶段预留资源的释放(出问题,释放锁)

img.png

# TCC如何保证最终一致性

  1. TCC事务机制以Try 为中心的,Confirm 确认操作和 Cancel 取消操作都是围绕 Try 而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有 Cancel 取消操作可以将其执行结果撤销。
  2. Try阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的,也就是说只要 Try 成功,Confirm 一定成功(TCC设计之初的定义)
  3. Confirm 与 Cancel 如果失败,由TCC框架进行重试补偿
  4. 在极低概率在CC环节彻底失败,则需要定时任务或人工介入

# TCC的注意事项

允许空回滚

空回滚出现的原因是Try超时或者丢包,导致TCC分布式事务二阶段的 回滚,触发 Cancel 操作,此时事务参与者未收到Try,但是却收到了Cancel 请求,

img.png

所以 cancel 接口在实现时需要允许空回滚,也就是 Cancel 执行时如果发现没有对应的事务 xid 或主键时,需要返回回滚成功,让事务服务管理器认为已回滚。

防悬挂控制

悬挂指的是二阶段的 Cancel 比 一阶段的Try 操作先执行,出现该问题的原因是 Try 由于网络拥堵而超时,导致事务管理器生成回滚,触发 Cancel 接口, 但之后拥堵在网络的 Try 操作又被资源管理器收到了,但是 Cancel 比 Try 先到。但按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功, 所以此时应该拒绝执行空回滚之后到来的 Try 操作**,否则会产生数据不一致。因此我们可以在 Cancel 空回滚返回成功之前,先记录该条事务 xid 或业务主键,** 标识这条记录已经回滚过,Try 接口执行前先检查这条事务xid或业务主键是否已经标记为回滚成功,如果是则不执行 Try 的业务操作。

img.png

幂等控制

由于网络原因或者重试操作都有可能导致Try - Confirm - Cancel 3个操作的重复执行所以使用 TCC 时需要注意这三个操作的幂等控制,通常我们可以使用事务xid或业务主键判重来控制。

# TCC方案的优缺点:

TCC事务机制相比于上面介绍的 XA 事务机制,有以下优点:

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

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

# Saga事务原理

Saga 事务核心思想是将长事务拆分为多个本地短事务并依次正常提交,如果所有短事务均执行成功,那么分布式事务提交如果出现某个参与者执行本地事务失败, 则由 Saga 事务协调器协调根据相反顺序调用补偿操作,回滚已提交的参与者,使分布式事务回到最初始的状态。Saga 事务基本协议如下:

  1. 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
  2. 每一个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。

与TCC事务补偿机制相比,TCC有一个预留(Try)动作,相当于先报存一个草稿,然后才提交;Saga事务没有预留动作,直接提交。

# Saga事务的实现方式

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

事件编排(Event Choreographyo)

命令协调方式基于中央协调器实现,所以有单点风险,但是事件编排方式没有中央协调器。事件编排的实现方式中, 每个服务产生自己的时间并监听其他服务的事件来决定是否应采取行动。

在事件编排方法中,第一个服务执行一个事务,然后发布一个事件,该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。 当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。

img.png

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

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

img.png

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

中央协调器 OSO 必须事先知道执行整个事务所需的流程,如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚, 基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。

# Saga的恢复策略

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

向后恢复(backward recovery):

当执行事务失败时,补偿所有已完成的事务,是“一退到底”的方式,这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。如下图:

img.png

从上图可知事务执行到了支付事务T3,但是失败了,因此事务回滚需要从C3,C2,C1依次进行回滚补偿,对应的执行顺序为:T1,T2,T3,C3,C2,C1。

向前恢复(forward recovery):

对于执行不通过的事务,会尝试重试事务,这里有一个假设就是每个子事务最终都会成功,这种方式适用于必须要成功的场景,事务失败了重试,不需要补偿。流程如下图:

img.png

# saga事件编排设计的优缺点

优点:

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

缺点:

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

# saga命令协调设计的优缺点

优点:

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

缺点

  1. 中央协调器处理逻辑容易变得庞大复杂,导致难以维护。
  2. 存在协调器单点故障风险。

# 本地消息表实现分布式事务

本地消息表的核心思路就是将分布式事务拆分成本地事务进行处理,在该方案中主要有两种角色:事务主动方和事务被动方。 事务主动发起方需要额外新建事务消息表,并在本地事务中完成业务处理和记录事务消息,并轮询事务消息表的数据发送事务消息, 事务被动方基于消息中间件消费事务消息表中的事务。

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

  1. 业务处理成功、事务消息发送失败
  2. 业务处理失败、事务消息发送成功

本地消息表的执行流程

img.png

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

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

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

# 本地消息表的优缺点:

优点:

  1. 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
  2. 方案轻量,容易实现。

缺点

  1. 与具体的业务场景绑定,耦合性强,不可公用
  2. 消息数据与业务数据同库,占用业务系统资源
  3. 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限

# MQ事务消息

基于MQ的分布式事务方案本质上是对本地消息表的封装,整体流程与本地消息表一致,唯一不同的就是将本地消息表存在了MQ内部,而不是业务数据库中,如下图:

img.png

由于将本地消息表存在了MQ内部,那么MQ内部的处理尤为重要,下面主要基于 RocketMQ4.3 之后的版本介绍 MQ 的分布式事务方案

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

img.png

正常情况

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

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

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

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

# MQ事务消息的优缺点:

优点:

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

  1. 消息数据独立存储 ,降低业务系统与消息系统之间的耦合
  2. 吞吐量大于使用本地消息表方案

缺点:

  1. 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
  2. 业务处理服务需要实现消息状态回查接口。

# 最大努力通知

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

img.png

在可靠消息事务中,事务主动方需要将消息发送出去,并且让接收方成功接收消息,这种可靠性发送是由事务主动方保证的; 但是最大努力通知,事务主动方仅仅是尽最大努力(重试,轮询....)将事务发送给事务接收方,所以存在事务被动方接收不到消息的情况, 此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。

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

# 分布式事务各方案常见使用场景总结

  1. 2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
  2. TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务
  3. 本地消息表/MQ事务:适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底
  4. Saga 事务:由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 由于缺少预提交动作,导致补偿动作的实现比较麻烦, 例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。所以,Saga 事务较适用于补偿动作容易处理的场景

# 如何解决TCC中的悬挂问题

TCC是分布式事务问题里面的解决方案,一般在应聘互联网公司的时候问的比较多。实际上,在TCC这个事务解决方案里面,除了悬挂问题以外,还有空回滚、幂等性需要考虑。 但是我们在应用的时候都是采用一些成熟的框架,比如Seata,这些框架本身就帮我们解决了。所谓TCC,其实就是(Try-Confirm-Cancel),也就是把一个事务拆分成两个阶段,类似于传统的XA事务模型。

  • Try这个阶段,是实现业务的检查,预留必要的业务资源。
  • Confirm,真正执行业务逻辑,只需要使用try阶段预留的业务资源进行处理就行。
  • Cancel,如果事务执行失败,就通过cancel方法释放try阶段预留的资源。

img.png

在TCC事务模式下,我们通过一个事务协调器来管理多个事务,每个事务先执行try方法。 当所有事务参与者的try方法执行成功,就执行confirm方法完成真正逻辑的执行,一旦任意一个事务参与者出现异常,就通过cancel接口触发事务回滚,释放Try阶段占用的资源。

img.png

很显然,这是一个最终一致性的实现方案,因此当Try执行成功,就必须确保Confirm执行成功。当Try执行失败,就必须确保Cancel实现资源释放。 而面试题中提到悬挂问题,指的是TCC执行Try接口出现网络超时时候,使得TCC触发Cancel接口回滚,但可能在回滚之后,这个超时的Try接口才被真正执行,也就导致Cancel接口比Try接口先执行。 从而造成Try接口预留的资源一直无法释放,这种情况就是悬挂。以上就是TCC悬挂问题的背景,它确实是每个成熟的高级开发必须要了解的细节。因为有可能会造成比较严重的生产事故。

对于悬挂问题,我认为只需要保证Cancel接口执行完以后,Try接口不允许在执行就可以了。所以,我们可以在Try接口里面,先判断Cancel接口有没有执行过,如果已经执行过,就不再执行。 是否执行过的这个判断,可以在事务控制表里面插入一条事务控制记录来标记这个事务的回滚状态。然后在Try接口中只需要读取这个状态来判断就行了。

# 分布式事务的原理与实现?

在微服务架构下,由于数据库和应用服务的拆分,导致原本一个事务单元中的多个DML操作,变成了跨进程或者跨数据库的多个事务单元的多个DML操作, 而传统的数据库事务无法解决这类的问题,所以就引出了分布式事务的概念。

分布式事务本质上要解决的就是跨网络节点的多个事务的数据一致性问题,业内常见的解决方法有两种:

  1. 强一致性,就是所有的事务参与者要么全部成功,要么全部失败,全局事务协调者需要知道每个事务参与者的执行状态,再根据状态来决定数据的提交或者回滚!
  2. 最终一致性,也叫弱一致性,也就是多个网络节点的数据允许出现不一致的情况,但是在最终的某个时间点会达成数据一致。

基于CAP定理我们可以知道,强一致性方案对于应用的性能和可用性会有影响,所以对于数据一致性要求不高的场景,就会采用最终一致性算法。

在分布式事务的实现上,对于强一致性,我们可以通过基于XA协议下的二阶段提交来实现,对于弱一致性,可以基于TCC事务模型、可靠性消息模型等方案来实现。 市面上有很多针对这些理论模型实现的分布式事务框架,我们可以在应用中集成这些框架来实现分布式事务。而Seata就是其中一种,它是阿里开源的分布式事务解决方案,提供了高性能且简单易用的分布式事务服务。

Seata中封装了四种分布式事务模式,分别是:

**AT模式,**是一种基于本地事务+二阶段协议来实现的最终数据一致性方案,也是Seata默认的解决方案

img.png

TCC模式,TCC事务是Try、Confirm、Cancel三个词语的缩写,简单理解就是把一个完整的业务逻辑拆分成三个阶段, 然后通过事务管理器在业务逻辑层面根据每个分支事务的执行情况分别调用该业务的Confirm或者Cacel方法。

img.png

Saga模式,Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者。

img.png

XA模式,XA可以认为是一种强一致性的事务解决方法,它利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。

img.png

从这四种模型中不难看出,在不同的业务场景中,我们可以使用Seata的不同事务模型来解决不同业务场景中的分布式事务问题,因此我们可以认为Seata是一个一站式的分布式事务解决方案。