面试常用九种分布式事务解决方案

本文已收录《后端面试小册子》 https://github.com/zas023/csnotes。同时所有文章已整理成册,目前共十三章节,总计约二十万字,欢迎👏🏻微信搜索【程序猿周周】获取电子版。如果觉得文章还对你有所帮助,赶紧点个免费的 star 支持一下吧!

1 前言

说到事务想必大家一定都不陌生,日常使用最多的也是关系型数据库的本地事务以保证业务中的一些强一致性操作。但随着行业的高速发展,业务益加复杂化,分布式或者微服务成为了大家的公认选择,但随之而来的就是一个大难题:如何在分布式系统中保持各节点的状态一致,即实现分布式事务

2 基础理论

在此之前,我们先来了解两个相关的理论基础,这是实现分布式事务的重要理论支撑。

2.1 CAP 理论

CAP 是计算机科学家 Eric Brewer 在 2000 年提出的理论猜想,并在同年被证明成为分布式计算领域公认的定理,其理论的基本观念是,在分布式系统中不可能同时满足以下三个特性:

  1. C:consistency 一致性
  2. A:Availability 可用性
  3. P:Partition Tolerance 分区容错性

由于分布式系统天然需要分区容错性(只要自身服务可用则依旧对外提供服务,无论其他服务状态),故我们只能在一致性和可用性中二选一:

  • AP:放弃一致性,追求分区容忍性和可用性,这也是大多数分布式系统所选择的,如 Redis;
  • CP:放弃可用性,追求一致性和分区容错性,如 Zookeeper。

ref 分布式系统CAP理论解析

2.2 BASE 理论

BASE 是 Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。

  1. 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网站交易付款出现问题了,商品依然可以正常浏览。
  2. 软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态,这个状态不影响系统可用性,如订单的“支付中”、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。
  3. 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的“支付中”状态,最终会变 为“支付成功”或者“支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。

BASE 理论是对 CAP 中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。

3 解决方案

3.1 X/Open DTP 事务模型

X/Open DTP(X/Open Distributed Transaction Processing Reference Model)是由 X/Open 组织定义的一套分布式事务的标准,它定义了规范的 API 接口,并由各个厂商进行具体的实现。这个标准提出了使用二阶段提交(2PC:Two-Phase-Commit)来保证分布式事务的完整性。后来 J2EE 也遵循了 X/OpenDTP 规范,设计并实现了 Java 里的分布式事务编程接口规范 JTA。

在 X/OpenDPT 模型中,有三个角色:

  1. AP:Application,应用程序,也就是我们的业务系统;
  2. RM:Resource Manager,资源管理器,管理一些共享资源,通常就是数据库;
  3. TM:Transaction Manager,事务管理器、事务协调者,负责接收来自用户程序(AP)发起的 XA 事务指令,并调度和协调参与事务的所有 RM 以确保事务正确完成。

在这里插入图片描述

DTP 的主要流程也非常简单:

  1. 参与分布式事务的应用程序(AP)先到事务管理器(TM)上注册全局事务;
  2. 然后各个 AP 直接在相应的 RM 上进行事务操作;
  3. 操作完成以后,各个 AP 将事务的处理结果反馈到 TM;
  4. TM 收到所有 AP 的反馈以后,通过 RM 提供的 XA 接口进行事务提交或者回滚操作。

3.2 2PC

2PC 即两阶段提交,它将整个事务流程分为两个阶段:准备阶段(prepare phase)和提交阶段(commit phase)。

在这里插入图片描述

  1. 准备阶段:RM 执行实际的业务操作,但不提交事务,锁定资源;
  2. 提交阶段:TM 会接受 RM 在准备阶段的执行结果,如果所有的 RM 都返回成功,TM 将会通知所有 RM 提交事务,如果所有的 RM 都提交成功了,则分布式事务成功。否则,只要有任一个 RM 执行失败,TM 会通知所有 RM 执行回滚操作,分布式事务回滚。
  • 优点

2PC 原理简单,实现方便且成本低,尽量保证了数据的**强一致。**两阶段提交协议虽然不是在 XA 规范中提出,但是 XA 规范对其进行了优化,现在在各大主流数据库都有自己实现,又叫做 XA Transactions。比如 MySQL 5.7 中关于 XA 分布式事务的介绍:https://dev.mysql.com/doc/refman/5.7/en/xa.html

  • 缺点
  1. 同步阻塞:在阶段一里执行 prepare 操作会占用资源直到整个分布式事务完成,这个过程中会阻塞需要访问这个资源的其它 AP;
  2. 单点故障:TM 是个单点,如果 TM 在 commit 出现故障,那么其它参与者将一直处于锁定状态;
  3. 事务状态丢失:即使我们把 TM 做成一个支持自动选举的集群,但如果 TM 挂掉的同时接收到 commit 消息的某个库也挂了,此时重新选举的 TM 也不知道这个分布式事务当前的状态;
  4. 数据不一致问题:在 commit 阶段,当协调者向所有的参与者发送 commit 请求后发生了网络异常导致协调者在还未发完 commit 请求之前崩溃,可能会导致只有部分的参与者接收到 commit 请求,也就可能导致数据不一致的问题。

3.3 3PC

由于二阶段提交存在着诸如同步阻塞、单点问题等缺陷,所以研究者们在二阶段提交的基础上做了改进,提出了三阶段提交(3PC,three-phase-commitment)。3PC 将事务分为三个阶段:

  1. CanCommit 阶段:TM 向各个 RM 发送 CanCommit 消息,然后各个 RM 返回结果。这一阶段主要是确定分布式事务的参与者是否具备了完成 commit 的条件,并不会执行事务操作;
  2. PreCommit 阶段:TM 根据 RM 的反馈情况来决定是否继续执行事务的 PreCommit 操作。如果各个数据库都返回成功,则进入 PreCommit 阶段,TM 发送 PreCommit 消息给各个 RM(即 2PC 的准备阶段),RM 执行业务操作但不提交。如果其中一个 RM 失败,则TM 发送 Abort 消息给各个 RM 结束这次分布式事务;
  3. DoCommit 阶段:如果各个 RM 在 PreCommit 阶段都返回了成功,那么 TM 发送 DoCommit 消息给所有 RM 提交事务。如果有 RM 对 DoCommit 返回失败或超时没返回,那么 TM 认为分布式事务失败,也直接发 Abort 消息给各个 RM 回滚,RM 回滚成功之后通知 TM,分布式事务完成回滚。

在这里插入图片描述

  • 优点

3PC 相比于 2PC 新增了 CanCommit 阶段,降低了阻塞范围

同时在 DoCommit 阶段,各 RM 自己也有超时机制。如果一个 RM 收到了 PreCommit 还返回成功,过了一段时间还没收到 TM 的消息则直接判定 TM 可能出故障,于是执行 DoCommit 操作提交事务,避免了协调者单点问题时资源一直阻塞问题

  • 缺点

当 TM 故障导致脑裂时,有些 RM 没接收到 Abort 消息,仍会因为超时执行 commit 操作,导致数据不一致性问题。

故无论 2PC 还是 3PC 都没法完全保证分布式事务的正确性

3.4 TCC

TCC(Try Confirm Cancel)是应用层的两阶段提交,所以对代码有较强的侵入性。其核心思想是针对每个操作都要实现对应的确认和补偿机制,也就是业务逻辑的每个分支都需要实现 try、confirm、cancel 三个操作:

  1. 第一阶段由业务代码编排来调用 try 接口进行资源预留;
  2. 第二阶段根据第一阶段的结果决定是执行 confirm 还是 cancel 操作;

当所有参与者的 try 接口都成功了,事务协调者提交事务,并调用参与者的 confirm 接口真正提交业务操作,否则调用每个参与者的 cancel 接口回滚事务,并且由于 confirm 或者 cancel 有可能会重试,因此对应的部分需要支持幂等。

在这里插入图片描述

  • 优点

TCC 相比于前面介绍的二阶段提交机制有以下优点:

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

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

3.5 本地消息表

本地消息表最早由 eBay 公司提出并实现,其核心思路是将分布式事务拆分成本地事务进行处理。事务主动发起方需要额外新建事务消息表,并在本地事务中完成业务处理和记录事务消息,并轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。

在这里插入图片描述

  1. 系统 A 在自己本地一个事务里操作同时,插入一条数据到消息表;
  2. 接着系统 A 将这个消息发送到 MQ 中;
  3. 系统 B 接收到消息之后在一个事务里往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么这个事务回滚保证幂等;
  4. 系统 B 执行成功之后会更新本地消息表的状态以及系统 A 消息表的状态;
  5. 如果系统 B 处理失败,那么就不会更新消息表状态,系统 A 会定时扫描自己的消息表,将没有处理的消息再次发送到 MQ 中通知系统 B 再次处理。
  • 优点

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

  • 缺点
  1. 与具体的业务场景绑定,耦合性强,不可共用;
  2. 业务系统严重依赖于关系型数据库,消息服务性能会受到关系型数据库并发性能的局限。

3.6 可靠消息

可靠消息最终一致性方案的本质是对本地消息表的封装,整体流程与本地消息表一致,唯一不同的就是将本地消息表存在了 MQ 内部(如 RocketMQ 或 RabbitMQ),而不是业务数据库中。

在这里插入图片描述

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

3.7 最大努力通知

最大努力通知方案也称为定期校对,是对可靠消息方案的进一步优化。它在事务主动方增加了消息校对的接口,事务主动方仅仅是尽最大努力(如重试、轮询机制)将事务发送给事务接收方,所以存在事务被动方接收不到消息的情况,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种事务的可靠性是由事务被动方保证的

在这里插入图片描述

3.8 Saga

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

Saga 有两种不同的实现方式:

  1. 基于事件的方式:即每个服务执行成功后发布个事件,下一个服务会监听到这个事件,然后继续执行。优点是去中心化,缺点是多个服务连续调用会导致对消息的监听非常复杂。
  2. 基于命令的方式:即通过一个 Saga 事务流程管理器来负责依次调用和执行各个服务,如果某个服务调用失败,就对之前调用成功的服务依次执行补偿接口。优点是系统比较简单,缺点是整个系统严重于Saga事务流程管理器。

Saga 将每个接口拆分为两个接口,一个是业务接口,一个是补偿接口。对于异常的事务 Saga 也有两种不同的恢复策略:

  1. 向后恢复:当执行事务失败时,补偿所有已完成的事务,使得整个 Saga 的执行结果撤销;
  2. 向前恢复:对于执行不通过的事务,会尝试重试事务,这里有一个假设就是每个子事务最终都会成功,这种方式适用于必须要成功的场景。

由于 Saga 模型没有 Prepare 阶段,因此事务间不能保证隔离性。当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发。

3.9 Seata XA

Seata(官网地址:http://seata.io)是阿里开源的一站式分布式事务解决方案,提供了四种不同的分布式事务解决方案:

  1. **XA 模式:**强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入;
  2. TCC 模式:最终一致的分阶段事务模式,有业务侵入;
  3. AT 模式:最终一致的分阶段事务模式,无业务侵入;
  4. SAGA 模式:长事务模式,有业务侵入。

其中 AT 是 Seata 的默认模式,是对 XA 事务的改进,所以本质还是两阶段提交(两阶段行锁):

  1. 允许本地事务先提交,但是由于此时全局事务还没有提交,仍要禁止其他事务修改本地事务变更的行,所以行锁需要在 RM 之外单独维护。直到全局事务提交或者回滚,才释放这些修改的行锁;
  2. 因为本地事务已经提交,所以在本地事务中除了修改数据,还需要将变更前后内容记录下来,以便全局事务回滚使用;
  3. 同时单独部署一个全局事务协调器(Transaction Coordinator),在 TC 当中维护全局的行锁。如果全局事务提交则释放所有分支事务持有的行锁;否则全局事务回滚,根据分支事务保存的变更记录来回滚分支事务,然后再释放对应的行锁。
  • 优点
  1. 不受单个 AP 故障影响,事务一致性更有保障;
  2. 两阶段行锁全部收敛在 TC 内维护,还可以做主动死锁检测,而不只是超时等待。
  • 缺点
  1. 默认的隔离级别是读未提交,可能读到事务执行过程中的脏数据,这是对原子性和性能的折衷;如果希望做到读已提交,需要显式使用 for update;
  2. 一个 RM 只要有一个操作使用了 Seata AT 事务,那这个 RM 上的所有更新操作就都需要使用 Seata AT 事务。否则相当于绕过了 TC 维护的行锁直接更新数据,会导致事务无法回滚,从而可能进入错误的状态。
  3. 对于 SQL 的支持有限制,可参考 https://tech.antfin.com/docs/2/71384

4 方案选择

本文通过结合分布式事务的理论基础介绍了九种业内常见解决方案,但在实践中又该如何选择呢?

  1. Seata AT 模式的性能和 XA 理论上没有太大差异,适合并发度不高的场景,简化业务逻辑,提高开发速度,适合于 toB 类的场景;
  2. TCC 模式可以根据业务场景做很多水平扩展,性能可以优化到很高,适合于并发度高,toC 类的场景,如支付、交易场景;
  3. 可靠(事务)消息可以算是 TCC 在特定场景下的扩展,性能比较高,适合于可以异步操作的场景,如积分或优惠券下发。

ref

  1. https://blog.csdn.net/weixin_70730532/article/details/125029945
  2. https://blog.csdn.net/a745233700/article/details/122402303

PS:*********************************************************************************************************。文中所有内容都会在 Github 开源,项目地址 csnotes,如文中存在错误,欢迎指出。如果觉得文章还对你有所帮助,赶紧点个免费的 star 支持一下吧!

#23届找工作求助阵地##Java##秋招##春招#
全部评论

相关推荐

Pandaileee:校友加油我现在也只有一个保底太难了
点赞 评论 收藏
分享
SinyWu:七院电话面的时候问我有没有女朋友,一听异地说你赶紧分。我:???
点赞 评论 收藏
分享
废铁汽车人:秋招真是牛鬼蛇神齐聚一堂
点赞 评论 收藏
分享
1 9 评论
分享
牛客网
牛客企业服务