分布式事务


在微服务架构盛行的情况下,在分布式的多个服务中保证业务的一致性,即分布式事务就显得尤为重要。本文将讲述分布式事务及其解决方案,有 XA 协议、TCC 和 Saga 事务模型、本地消息表、事务消息和阿里开源的 Seata。

[

概要](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==∣=2247487551&idx=1&sn=18f64ba49f3f0f9d8be9d1fdef8857d9&scene=21#wechat_redirect)
在微服务架构盛行的情况下,在分布式的多个服务中保证业务的一致性,即分布式事务就显得尤为重要。本文将讲述分布式事务及其解决方案,有 XA 协议、TCC 和 Saga 事务模型、本地消息表、事务消息和阿里开源的 Seata。

[

分布式事务](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==∣=2247487551&idx=1&sn=18f64ba49f3f0f9d8be9d1fdef8857d9&scene=21#wechat_redirect)
聊什么是分布式事务前,先聊一下我们熟悉的单机事务。所谓单机事务是相对分布式事务来说的,即数据库事务。大家都知道数据库事务有 ACID 这四个特性:

  • A(Atomicity):指单个事务中的操作要不都执行,要不都不执行
  • C(Consistency):指事务前后数据的完整性必须保持一致
  • I(Isolation):指多个事务对数据可见性的规则
  • D(Durability):指事务提交后,就会被永久存储下来

既然数据库事务有这四个特性的,那么分布式事务也不例外,应该具备这四个特性。
在微服务架构下,服务之间通过 RPC 远程调用,相对单机事务来说,多了“网络通信”这一不确定因素,使得本来服务的调用只有“成功”和“失败”这两种返回结果,变为“成功”、“失败”和“未知”三种返回结果。系统之间的通信可靠性从单一系统中的可靠变成了微服务架构之间的不可靠,分布式事务其实就是在不可靠的通信下实现事务的特性。一般因为网络导致的异常可能有机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失、其他异常等等。

分布式事务方案

2PC/3PC

2PC 即二阶段提交 :

二阶段提交(英语:Two-phase Commit)是指在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,二阶段提交也被称为是一种协议(Protocol)。

2PC 是一种协议,它的作用保证在分布式系统中每个节点要不都提交事务,要么都取消事务。这个跟 ACID 中的 A 原子性的定义很像。
2PC 引入一个第三方的节点协调者,即 Coordinator,其他参与事务的节点为参与者,即 Participants。协调者统筹整个事务行为,负责通知参与者进行 Commit 还是 Rollback 操作。
2PC 的过程比较简单,分为两个阶段:

  1. 准备阶段
    协调者分别给每个参与者发送 Prepare 消息,每个参与者收到消息后,进行“预提交”操作(不是实际的提交操作),把操作的结果(成功或失败)返回给协调者。

  2. 提交阶段
    协调者根据准备阶段收到的参与者的返回结果进行判断,如果所有的参与者都返回成功,那么分别给每个参与者发送 Commit 消息,否则发送 Rollback 消息。


2PC 是一个强一致性协议,同时它在实际应用中还存在几个问题

  • 同步阻塞,2PC 的两个阶段中,协调者和参与者的通信都是同步的,这会导致整个事务的长时间阻塞
  • Coordinator 的单点问题
  • 数据不一致,在 Commit 阶段,可能存在只有部分参与者收到 Commit 消息(或处理成功)的情况

3PC

3PC 即三阶段提交,它比 2PC 多了一个阶段,即把原来 2PC 的准备阶段拆分成 CanCommit 和 PreCommit 两个阶段,同时
引入超时机制来解决 2PC 的同步阻塞问题。

但是在我看来 3PC 并没有解决 2PC 的根本问题,它只是在 2PC 的基础上做了一些优化,它增加了一个阶段(也增加了 1 个 RTT)来提高对方可用性的概率,这本质跟 TCP 的三次握手一样,同样也改为四次握手,五次握手等等。

XA

XA 是一种基于 2PC 协议实现的规范。在 2PC 中没有明确资源是什么,以及资源是怎么提交的等等,而 XA 就是数据库实现 2PC 的规范,已知常用的支持 XA 的关系型数据库有 Mysql、Oracle 等。

本地消息表

本地消息表方案应该是业界内使用最为广泛的,因为它使用简单,成本比较低。
本地消息表的方案最初是由 eBay 提出(完整方案),核心思路是将分布式事务拆分成本地事务进行处理。
它的处理流程如下:

  • 事务发起方把要处理的业务事务和写消息表这两个操作放在同一个本地事务里
  • 事务发起方有一个定时任务轮询消息表,把没处理的消息发送到消息中间件
  • 事务被动方从消息中间件获取消息后,返回成功
  • 事务发起方更新消息状态为已成功

从处理流程来看,本地消息表方案是一个基于消息中间件的可靠性来达到事务的最终一致性的方案。
一些分析:

  • 把业务处理和写消息表放在同一个事务是为了失败/异常后可以同时回滚

  • 为什么不直接发消息,而是先写消息表?
    试想,如果发送消息超时了,即不确定消息中间件收到消息没,那么你是重试还是抛异常回滚事务呢?回滚是不行的,因为可能消息中间件已经收到消息,接收方收到消息后做处理,导致双方数据不一致了;重试也是不行的,因为有可能会一直重试失败,导致事务阻塞。

  • 基于上述分析,消息的接收方是需要做幂等操作的

本地消息表方案整体来说还是比较简单、可用的,但是也有以下缺点:

  • 消息数据和业务数据耦合,消息表需要根据具体的业务场景制定,不能公用。就算可以公用消息表,对于分库的业务来说每个库都是需要消息表的。
  • 只适用于最终一致的业务场景。例如在 A -> B 场景下,在不考虑网络异常、宕机等非业务异常的情况下,A 成功的话,B 肯定也会成功的。

事务消息

事务消息是通过消息中间件来解耦本地消息表和业务数据表,适用于所有对数据最终一致性需求的场景。现在支持事务消息的消息中间件只有 RocketMQ,这个概念最早也是 RocketMQ 提出的。
通过事务消息实现分布式事务的流程如下:

  1. 发起方发送半事务消息会给 RocketMQ ,此时消息的状态 prepare,接受方还不能拉取到此消息
  2. 发起方进行本地事务操作
  3. 发起方给 RocketMQ 确认提交消息,此时接受方可以消费到此消息了


步骤 1 和 3 失败/异常该如何处理:
RocketMQ 会定期扫描还没确认的消息,回调给发送方,询问此次事务的状态,根据发送方的返回结果把这条消息进行取消还是提交确认。
可以看出事务消息的本质的借鉴了二阶段提交的思想,它跟本地消息表的做法也很像,事务消息做的事情其实就是把消息表的存储和扫描消息表这两个事情放到消息中间件来做,使得消息表和业务表解耦。

TCC

TCC (Try-Confirm-Cancel)事务模型采用的是补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿操作。
相当于 XA 来说,TCC 可以不依赖于资源管理器,即数据库,它是通过业务逻辑来控制确认和补偿操作的,所以它用了’Cancel’而非’Rollback’的字眼。它是一个应用层面的 2PC。
TCC 分为三个阶段:

  • Try 阶段,对业务资源进行检测和预留
  • Confirm 阶段,对 Try 阶段预留的资源进行确认提交,Try 阶段执行成功是 Confirm 阶段执行成功的前提
  • Cancel 阶段,对 Try 阶段预留的资源进行撤销或释放


看上去 TCC 跟 2PC/3PC 可能有点像,但是 TCC 强调的是补偿,而且对于对资源的“预留”,“确认”,“释放”,TCC 并没有明确说要如何做,这个具体是要业务来定义的。
例如在转账的场景,“预留”操作可能就是对账号里的部分资金进行冻结,这样这个资金只能是当前事务才能用,别的事务用不了。
另外,对于异常的场景,TCC 也没有说要怎么做,因为 Try、Confirm、Cancel 都是业务定义的,这三个阶段中发生了异常,那么就由业务来做相应的处理。一般都有以下几种处理:

  • 如果 Try 成功了,那么 Confirm 阶段异常了就一直重试,直到成功
  • Try、Confirm、Cancel 三个阶段都有相应的资源及事务日志,应用根据日志(异步)来做重试或补偿
  • TCC 的实现依赖底层数据库,异常后直接利用数据库的事务机制回滚

其中现在使用比较多的 TCC 框架 ByteTCC、tcc-transaction 的原理都是基于第三点
同时,在实现 TCC 时要注意以下三个问题

  • 允许空回滚
    在 Try 没有真正执行的情况下,触发了 Cancel 操作,这时要允许 Cancel 成功

  • 防悬挂控制
    Cancel 操作比 Try 操作先执行(网络延迟原因),后面的 Try 操作不能执行成功

  • 幂等控制

TCC 其实是把控制事务的逻辑放在业务应用层面,而非资源管理器,这样实现起来就会相对灵活很多,但相对对数据一致性的保证可能没那么强(具体看怎么实现 Try),整体来说 TCC 还有以下缺点:

  • 对于 Confirm 和 Cancel 阶段失败后要完全靠业务应用自己去处理
  • 每个业务都需要实现 Try、Confirm、Cancel 三个接口,代码量比较多
  • 如果是基于现有的业务想使用 TCC 会比较困难。一是对于原来的接口要拆分为三个接口,入侵性比较大;二是因为要做“预留”资源的操作,有可能需要对原来的业务模型进行改造。

Saga

Saga 事务模型又叫做长时间运行的事务(Long-running-transaction), 它是由普林斯顿大学的 H.Garcia-Molina 等人提出,它描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题。Saga 的论文。
该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Saga 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么 Saga 工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。
Saga 也是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。

你可以看到 Saga 跟 TCC 很像,但是 Saga 更加宽松,一致性更弱,在 Saga 看来,在一阶段直接做提交/确认操作就好了,有问题再做补偿。这样的话,Saga 可以拥有比 XA 和 TCC 更好的性能(XA、TCC 需要锁定资源或预留资源),而且 Saga 强调通过事件驱动异步处理,实现高吞吐。
可以看出 Saga 是对 TCC 的一种“妥协”,从 TCC 的三个接口变为两个接口,一阶段直接提交缺少对资源的隔离(如果一阶段提交后,后面发现需要做补偿,但是补偿操作执行前有另外的事务更改了数据,这时数据已经变“脏”了,那么这时该如何处理是一个问题。在 TCC 没有这个问题,因为资源已经被 hold 住了),因此对使用者也是比较宽松的,对于现有业务的改造也会比较简单。
Saga 实现分两种,一种是 Saga 状态机实现,一种是 Saga AOP Proxy 实现。Saga 状态机实现,在关于参与者服务编排实现又有集中式和协同式两种分支。这点就不展开了。

TCC vs Saga

TCC 和 Saga 都属于补偿型事务模型,Saga 没有 Try,直接 Commit,所有会产生实际的事务痕迹,而补偿做的是反向操作。TCC 是二阶段的广义实现,利用了数据的中间态,Cancel 是中间状态的数据进行撤销,从而不存在数据污染问题。
使用场景对比:

  • TCC 适用于执行时间确定且较短、对一致性要求比较高、数据隔离强的业务
  • Saga 适用于业务流程长、业务流程多的业务,在银行业金融机构使用广泛
  • TCC 对现有业务改造较大,Saga 则相对少点

Seata

Seata 是一个由阿里做背书的分布式事务框架,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

AT 模式

AT 模式是 Seata 通过拦截、解释用户的 SQL,对业务数据进行加锁、回滚等操作的基于二阶段协议的一个实现。
它的特点是对业务无入侵,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

TCC 模式

Seata 的 TCC 模式跟上面讲的 TCC 事务模型差不多

Saga 模式

Saga 模式也是上面讲的 Saga 事务模型差不多。在 Seata 中对服务的编排引入了状态机引擎, 使得对业务流程的定义更加标准化,提高可读性,不过相对来说配置会比较复杂繁琐。同时支持注解的方式,这个在开发上会简单一点,但功能可能少一点。

分布式事务一致性与 Paxos 一致性的思考

首先要明确一点的就是对于上述提到的分布式事务解决方案,如 TCC、Saga、本地消息表等,其本质都是 2PC。
Paxos 算法解决的问题是一个分布式系统如何就某个值(决议)达成一致。
咋看起来 2PC 和 Paxos 都是解决关于“一致性”的问题,其实细想它们解决的问题不在一个层面。
2PC 要求分布式系统中的每个节点要不全部成功,要不全部失败,强调的是原子性。
Paxos 要求多个副本之间的数据一致性,其实这里用“一致性”并不准确,应该用“共识(Consensus)”才对。
例如 2PC 中的协调者单点的问题可以用 Paxos 算法通过选举出新的协调者来解决。

总结

总得看来,分布式事务的解决方案都很难做到有高一致性的同时,也有高性能,同时在实现上也有一定的难度。在业务允许的情况下,我们通常处理分布式事务的一般原则应是:业务规避 > 最终一致 > 强一致。


文章作者: sbwit
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 sbwit !
  目录