你所需要了解的分布式事务——解决方案篇

221 阅读17分钟

背景

上篇文章你所需要了解的分布式事务——基础篇介绍了分布式事务的基础理论与概念,本篇文章就分布式事务的几种解决方案进行展开。

分布式事务的解决方案

分类

根据对一致性的要求分两类:

  1. 刚性事务,也被称为强一致性事务,指的是需要在分布式系统中确保数据的一致性和完整性,即所有参与的节点要么同时提交事务,要么同时回滚事务。
  2. 柔性事务,也称为最终一致性事务或弱一致性事务,是相对于刚性事务(强一致性事务)的一种分布式事务处理方式。它不要求所有参与者在同一时间点上保持一致,而是允许在一定的时间内达到一致性。柔性事务更适合那些对数据一致性要求不那么严格,但对系统性能和可用性要求较高的场景。

下面就来介绍一些常用的分布式事务解决方案。

XA协议

在分布式事务中,XA协议全称"eXtended Architecture", 又称扩展事务协议,是由X/Open组织专门为分布式事务制定的处理标准,旨在分布式事务中实现事务的ACID特性。

XA协议中定义了三种角色,一个应用程序(Applicaion Program, AP),一个全局事务管理器(Transaction Manager, TM),多个资源管理器(Resource Manager, RM),这里 TM 、RM 概念在后面的解决方案中十分关键

应用程序(Applicaion Program, AP): 应用程序即需要进行分布式事务操作的程序,

全局事务管理器(Transaction Manager, TM): 事务的协调者(如购物车下单中的购物系统) ,事务管理器负责协调分布式事务的整个生命周期和一致性,发起事务后,TM会生成全局事务记录即会有一个全局事务的 ID 贯穿整个分布式事务调用链条。

资源管理器(Resource Manager, RM): 事务的参与者(如购物车下单中的订单系统、库存系统) ,资源管理器通常是数据库管理系统(DBMS)、消息队列或其他需要参与分布式事务的资源,而大部分情况下,个人理解,RM操作都是独立的子事务。

三者的调用关系大致如下:

AP 负责发起和控制事务的流程,它通过是 TM 启动并管理分布式事务,并通过 RM 对程序资源进行操作。

TM 负责管理全局事务的开始、提交、回滚以及故障恢复,通过与 RM 通信来下达事务进行各种操作。

RM 负责对资源进行实际操作以及执行提交或回滚操作,通过接收并处理来自 TM 的请求操作。

image.png

那么,在TM与RM的通信过程中如何保证一致性呢?XA协议采用2PC作为核心机制来保障。

2PC

两阶段提交 (Two-Phase Commit, 2PC )协议是一种确保分布式系统中事务一致性的协议。它由协调者(XA协议中的TM)与多个参与者(XA协议中的RM)通信组成,并分成了两个阶段。

Prepare Phase

第一阶段称为Prepare Phase(准备阶段),其主要过程如下:

  1. 程序启动事务

    1.   AP 发起一个事务,并向事务的协调者即 TM 发出请求,要求执行该全局事务
  2. 协调者发送Prepare请求

    1.   TM 接收到执行请求之后,会向所有事务的参与者即 RMs 发送 Prepare请求,询问是否可以提交事务。
  3. 参与者执行准备

    1.   RM 收到请求后执行必要的操作(例如,写日志、锁定资源等)来准备提交子事务。
    2.   RM 要么返回“准备好”(Prepared)状态,要么返回“失败”(Failed)状态。如果 RM 准备好提交事务,它会写入日志以便在故障恢复时使用。
  4. 协调者收集响应

    1.   TM 收集所有 RMs 的响应。如果所有 RMs 都返回“Prepared”状态,TM 在第二阶段提交请求;如果有任何 RM 返回了“Failed”状态,TM 在第二阶段回滚请求。

image.png

Commit Phase

第二阶段称为Commit Phase(提交阶段),第一个阶段TM会根据收集到"Prepared"请求的响应会进入第二阶段提交/回滚事务,其主要过程如下:

  1. 协调者决策并发送通知

    1. 如果第一阶段 RMs 都准备好,TM 向 RMs 发送Commit请求。
    2. 否则,TM 向RMs 发送 Rollback请求。
  1. 参与者提交或回滚操作

    1. 如果收到提交请求,RMs 提交事务,释放资源,并向 TM 返回确认。
    2. 如果收到回滚请求,RMs 撤销先前的操作,释放资源,并向 TM 返回确认。
  2. 协调者完成事务

    1. TM 在收到所有 RMs 的确认后, 如果是提交操作,事务被确认提交;
    2. TM 在收到所有 RMs 的确认后, 如果是回滚操作,事务被撤销。

优缺点

优点

  1. 一致性:2PC 确保所有参与者要么一起提交事务,要么一起回滚,保持数据一致性。
  2. 标准化:2PC 是分布式事务处理中广泛使用的标准协议。

缺点

  1. 性能开销:无论是准备阶段还是提交阶段,协调者都需要等待所有参与者的响应,那么就意味着如果 TM 和 RM 通信延迟过高、RM数量过多等等时,参与者会处于同步阻塞态, 这会影响到分布式事务的并发度

  2. 单点故障:TM、RM出现单点故障都会影响整个事务的处理

  3. 宕机问题:

    1. 协调者宕机

      1.     在准备阶段和提交阶段之间,如果事务管理器(TM)宕机,所有参与者(RMs)将保持阻塞状态,因为参与者并不知道应该提交还是回滚事务。
    2. 参与者宕机

      1. 在准备阶段,如果某个参与者宕机,事务管理器可能无法得到所有参与者的准备状态,从而无法决定是否提交或回滚事务。
      2. 在提交阶段,如果某个参与者宕机,它在恢复后需要能够重启并根据事务管理器的最终决定(提交或回滚)进行处理。但如果事务管理器宕机,参与者也可能长时间处于不确定状态。

2PC的进阶版本是3PC,CanCommit、PreCommit、DoCommit,前两个流程与2PC有点相似,不过CanCommit阶段不锁定资源,不阻塞参与者的状态,而只是询问是否提交,并且在PreCommit阶段允许子事务提交失败,如果失败就会DoCommit阶段发送abort请求中断事务。

TCC

TCC是Try-Confirm-Cancel三个词的缩写。TCC概念最早可以追溯至Pat Helland2007年的论文《Life beyond Distributed Transactions:an Apostate’s Opinion》,论文中TCC还是Tentative-Confirmation-Cancellation。

TCC,它是一种应用层面2PC分布式事务方案。相比2PC,TCC提供了更高的灵活性和性能,它强调通过应用层面的控制来实现事务的一致性,而不是依赖底层的分布式事务协议。它允许开发者定义每个阶段的具体操作逻辑,从而更好地适应不同的业务需求。

在实际使用TCC模式时,业务必须实现三个接口Try()、Confirm()、Cancel(),并遵循一定的设计规范与要点。

Try phase

准备阶段,TM 向 RMs 发送Try请求,RM此时需要预留资源或进行业务检查,确保后续的 Confirm 和 Cancel 操作能够顺利执行,例如,在预定机票的场景中,Try 阶段可以会将座位标记为预留或者也可以锁定座位,但不会实际扣款。与XA协议中的准备阶段锁住资源不同,在该阶段业务可以不加锁。

image.png

Commit Phase

  1. 如果TM 收到 RMs 的Try请求 都Ready后,TM 会向 RMs 发送Confirm请求,RMs 此时才会去执行真正的事务逻辑。
  2. 如果TM 收到 RMs 的Try请求 有RM Failed后,TM 会向 RMs 发送Cancel请求,RMs 此时会释放Try阶段的资源预留/锁定。

在该阶段,如果 RM Confirm()/Cancel() 返回失败,TM则会不断发起相应的请求不断重试,因此Confirm/Cancel()接口必须是 幂等 ,如果 RM 的 Confirm()、Cancel() 一直出错,那么这个时候最好引入人工去处理。

image.png

至此,可以看到Try阶段+Commit阶段才是完整的业务逻辑操作,业务需要能够将业务逻辑较好的分拆成Try逻辑和Commit逻辑。

异常处理

  1. 空回滚

空回滚指的是如果 RM 没有收到 TM 的Try阶段请求,但是收到了Cancel请求,执行Cancel()。

出现空回滚的情景主要是TM发送Try请求时 RM 发生了宕机、网络故障或者延迟过高导致超时,TM 认定Try请求失败并进入提交阶段发送Cancel请求,RM 故障恢复接收到了Cancel请求,执行Cancel()。

针对空回滚,RM Cancel()应当能够识别出并直接返回成功。

  1. 非幂等

简单理解幂等指的是每次请求拿到的返回结果都是一致的,非幂等则与之相反。

前面已经提到在提交阶段 TM 向 RM 发送请求会有重试机制,因此为了保证重试机制下数据的一致性,Confirm/Cancel()接口必须是幂等的。

  1. 防悬挂

与空回滚类似,悬挂是指由于网络问题,TM 第一次 发送的Try请求 没有被RM收到,此时 TM 认定Try失败,进入提交阶段发送Cancel请求,RM 在接收 Cancel 请求执行Cancel()之后,才收到第一次发送的Try请求并执行了Try()。此时产生的问题就是,RM 会再次预留或者锁住相关的资源。

针对悬挂,业务需要通过记录Cancel()执行的子事务,当确认该子事务已经回滚,直接忽略掉这次的Try请求,进而实现防悬挂。

从异常处理也可以看出,TCC 模式其实十分依赖对事务状态的精确管理。因此事务的状态需要被记录下来,以便在系统故障恢复时可以正确地恢复和继续事务操作。

适用场景

在TCC模式下,RM 在Try阶段对资源"上锁"的粒度是由业务自行控制的,这就使得TCC有较好的并发性能。

同时TCC模式下,TCC是在Try阶段对资源进行预留/加锁,所以TCC的隔离性应当是读已提交(Read Commited),隔离性较好。

因此,TCC模式比较适合并发性与隔离性要求较高同时需要保证事务操作强一致性的多步骤业务场景,如电商订单、供应链管理、跨境支付等系统业务场景。

Saga

Saga模式是出自Hector Garcia-Molina和Kenneth Salem于1987年发表的论文《Sagas》,主要是为了处理long lived transaction(长活事务)。Saga的核心思想是将事务拆分成多个独立子事务,并在执行失败后,通过一系列的补偿(compensating)操作来回滚操作。比如,购物下单支付成功但出库失败的流程:订单下单 ----> 支付成功 ----> 出库失败 ----> 退款 ----> 订单回退 ----> 通知下单失败已被取消,请重新提交。

流程

  1. Saga 将 RM 提供的 执行操作/正向操作(T1 T2 T3...) 组成一条正向的有序事务链。同时Saga要求 RM 提供与正向操作对应的补偿操作/逆向操作(C1 C2 C3...)组成一条逆向的补偿事务链用于逆序回滚。
  2. 每个子事务执行成功后,请求下一个子事务的执行。
  3. 某个子事务执行失败后,就按逆序向前请求 RM 提供的补偿操作。

image.png

要注意的是,Saga中的补偿操作是不完全补偿,因为正向的事务链中执行成功的事务已经被提交了,补偿操作无法保证每个事务都可以被回滚,同时 RM提供的操作同样需要是幂等接口。

同时saga其实提供了两种实现方式:

  • 编排模式(Orchestration):提供一个中心协调器,负责按顺序执行Saga的每个子事务,如果某个子事务失败,则负责会按逆序执行相应的补偿操作。
  • 事务参与者模式(Choreography):由事务参与者通过事件驱动机制彼此通信,并根据接收到的事件决定下一步操作。

适用场景

由于Saga的事务参与者提交自身事务,不需要等待其他事务的完成,因此系统可并发执行Saga事务的量相较传统方案会更大。

除此之外,系统处理Saga事务是不需要加上分布式锁,Sage事务下的独立子事务执行失败会有相应的补偿操作,而业务为Saga单独实现补偿操作的成本很低,因此Saga在保持了较高的并发性能的同时并未增加接入的成本。

但是由于Saga的事务参与者执行成功便提交了事务,Saga的隔离性为读未提交(Read UnCommited),隔离性较差。

综上,Saga模式比较适合于对并发性能和业务接入成本要求比较高,但是对于有隔离性要求不高的场景。

Seata

Seata是阿里巴巴主导开发并开源的分布式事务,旨在简化在微服务架构下的分布式事务管理。它支持多种事务模式,包括AT模式、两阶段提交(2PC)、TCC模式、Saga模式以及XA模式。不过Seata小泉还没去深入了解,这期先略过,等后续小泉学习后再聊一聊,这里就附上Seata的介绍,大家了解一下,seata.apache.org/zh-cn/docs/…

本地消息表

本地消息表(Local Message Table)特别适用于那些不允许或无法使用分布式事务管理器(如XA协议)的场景。它通过将消息存储在本地数据库中来确保事务的原子性,从而在分布式系统中实现最终一致性。

image.png

流程

  1. 服务A发起本地事务1,先执行业务操作,随后将需要发送的消息存储在本地数据库的消息表中,确保业务操作和消息存储在同一个本地事务中,提交事务1。
  2. 服务A使用一个定时任务或后台线程定期轮询消息表,将未发送的消息发送到消息中间件(MQ等等),发送成功后,将消息标记为已发送,发送失败则重试。
  3. 服务 B 消费消息中间件中的消息,发起本地事务2,并在事务2中处理业务逻辑,如果非业务逻辑导致的本地事务失败,则会在继续消费消息中间件中的消息进行重试,如果是业务逻辑上的失败,则可以通知服务 A 进行回滚/补偿操作,或告警通知人工干预。

本地消息表以及后面的消息事务等方案的核心都是异步通知。所以这种方案比较适用于支持异步场景的业务。

MQ事务消息通知

这种分布式事务解决方案是基于消息队列的特性提出的,它的核心是依靠消息队列提供的事务消息保证消息的生产和消费与数据库操作的原子性,依靠2PC协议来保证消息的可靠传递和一致性,以RocketMQ举例如下:

image.png

流程

  1. 服务A预提交事务消息(Prepare)到MQ,此时MQ还无法投递消息

  2. 接着服务A执行本地事务。

  3. 根据本地事务的执行成功与否,服务A发送不同消息给到MQ。

    1. 如果Confirm消息,MQ则可以开始投递消费消息。
    2. 如果是Rollback消息,那么MQ就会将步骤1中的消息删除。

这里只展示了简单的事务消息流程,实际流程还有事务的回查等逆向流程。MQ事务消息通知最大的优点就是解耦了业务逻辑与消息存储,吞吐量优于本地消息表。

最大努力通知

最大努力通知(Best-Effort Notification,BEN)的核心思想是在事务提交后,尽最大努力通知相关参与方,但不强制要求所有参与方都收到通知。这种方案适用于对一致性要求相对较低的场景。

image.png

流程

  1. 服务A 启动事务1,并执行本地事务。
  2. 服务A 提交事务,并生成事务消息。
  3. 服务A 尽最大努力将事务消息发送给通知系统。
  4. 如果通知系统发送失败或 服务B 处理失败,通知服务可以重试机制,尝试多次发送通知。同时,服务B需要实现幂等控制,以防止多次响应,同时实施补偿机制来纠正事务操作的影响。

因为不需要等待所有参与方的确认,事务提交速度快,最大努力通知性能会比较高,并且相对于两阶段提交(2PC)等复杂的分布式事务协议,最大努力通知实现起来相对简单。但它的缺点也很明显,不保证所有参与方都能收到通知也就意味着无法保证数据的一致性,因此它只适用于一致性要求很低的场景。

总结

分布式事务的解决方案有很多种,基本上分为两大类型,一类比如2PC、3PC、TCC模式、Saga模式、Seata AT模式等等都可以看成是遵守XA协议或是XA协议的变种。而另一类则是基于消息通知的分布式事务方案。每种类型的实现或多或少都有着相通之处。

参考

十八张图,看懂八种分布式事务机制的全貌! - 掘金

分布式事务(四)本地消息表和消息事务(RocketMQ详细实现) - 掘金

分布式事务之解决方案(最大努力通知)

基于本地消息表的分布式事务解决方案 - 掘金

基于RocketMQ实现分布式事务(半消息事务)

讨论

异步通信方案适用场景

与组里小伙伴咨询讨论了一下,放在这里仅供讨论,大家也可以说一说自己的看法。单就分布式事务解决方案,本地消息表、MQ事务消息和最大努力通知这种异步通信方案最好不要用在有关涉及到数据相关的处理,如果失败,那么回滚/补偿的实现与维护都十分复杂,同时不利于问题的追踪解决。它们比较适合用在一些对用户透明无需感知或者无需阻塞主流程的逻辑。

以购物车下单的Saga链路场景为例子。

如果购物下单支付成功但出库失败的流程:订单下单 ----> 支付成功 ----> 出库失败 ----> 退款 ----> 订单回退 + 短信通知“下单失败已被取消,请重新提交”。

image.png

欢迎大家提出不同的意见与看法,交流碰撞中能产生更多的火花。