分布式事务

分布式事务

微服务架构下,一个系统被拆分为多个小的微服务。每个微服务都可能存在不同的机器上,并且每个微服务可能都有一个单独的数据库供自己使用。这种情况下,一组操作可能会涉及到多个微服务以及多个数据库。举个例子:电商系统中,你创建一个订单往往会涉及到订单服务(订单数加一)、库存服务(库存减一)等等服务,这些服务会有供自己单独使用的数据库。

这个时候单单依靠数据库事务就不行了,我们就需要引入分布式事务这个概念了。

实际上,只要跨数据库的场景都需要用到引入分布式事务。比如说单个数据库的性能达到瓶颈或者数据量太大的时候,我们需要进行分库,同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了

分布式事务的终极目标就是保证系统中多个相关联的数据库中的数据的一致性!

那既然分布式事务也属于事务,理论上就应该准守事物的 ACID 四大特性。但是,考虑到性能、可用性等各方面因素,我们往往是无法完全满足 ACID 的,能选择一个比较折中的方案。针对分布式事务,又诞生了一些新的理论。

分布式事务基础理论

CAP理论

CAP 也就是 Consistency(一致性)Availability(可用性)Partition Tolerance(分区容错性) 这三个单词首字母组合。

image-20240530103738690

在理论计算机科学中,CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个:

  • 一致性(Consistency) : 所有节点访问同一份最新的数据副本
  • 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
  • 分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务

什么是网络分区?

分布式系统中,多个节点之间的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫 网络分区

image-20240417165918347

并非三选二

当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。

简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。

因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。 比如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。

为啥不可能选择 CA 架构呢?

举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。

选择 CP 还是 AP 的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP

另外,需要补充说明的一点是:如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。

BASE理论

BASEBasically Available(基本可用)Soft-state(软状态)Eventually Consistent(最终一致性) 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。

BASE 理论的核心思想

即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。

BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。

BASE 理论三要素

image-20240417204106942

基本可用

基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。

什么叫允许损失部分可用性呢?

  • 响应时间上的损失: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。
  • 系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。

软状态

软状态指允许系统中的数据存在中间状态(CAP 理论中的数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

最终一致性

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

分布式一致性的 3 种级别:

  1. 强一致性:系统写入了什么,读出来的就是什么。
  2. 弱一致性:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
  3. 最终一致性:弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。

业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。

那实现最终一致性的具体方式是什么呢? 《分布式协议与算法实战》 中是这样介绍:

  • 读时修复 : 在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 Read Repair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据。
  • 写时修复 : 在写入数据,检测数据的不一致时,进行修复。比如 Cassandra 的 Hinted Handoff 实现。具体来说,Cassandra 集群的节点之间远程写数据的时候,如果写失败 就将数据缓存下来,然后定时重传,修复数据的不一致性。
  • 异步修复 : 这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。

比较推荐 写时修复,这种方式对性能消耗比较低。

柔性事务

互联网应用最关键的就是要保证高可用,计算式系统几秒钟之内没办法使用都有可能造成数百万的损失。在此场景下,一些大佬们在 CAP 理论和 BASE 理论的基础上,提出了 柔性事务 的概念。柔性事务追求的是最终一致性。

实际上,柔性事务就是 BASE 理论 +业务实践。 柔性事务追求的目标是:我们根据自身业务特性,通过适当的方式来保证系统数据的最终一致性。像 TCC、 Saga、MQ 事务、本地消息表就属于柔性事务。

刚性事务

与柔性事务相对的就是刚性事务 了。前面我们说了,柔性事务追求的是最终一致性 。那么,与之对应刚性事务追求的就是 强一致性。像2PC、3PC 就属于刚性事务。

分布式事务解决方案

分布式事务的解决方案有很多,比如:2PC、3PC、TCC、本地消息表、MQ 事务(Kafka和 RocketMQ 都提供了事务相关功能)、Saga 等等:

2PC、3PC 属于业务代码无侵入方案,都是基于 XA 规范衍生出来的实现,XA 规范是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。TCC、Saga属于业务侵入方案,MQ事务依赖于使用消息队列的场景,本地消息表不支持回滚。

这些方案的适用场景有所区别,我们需要根据具体的场景选择适合自己项目的解决方案。

2PC(两阶段提交协议)

image-20240526231347823

TM(Transaction Manager):事务管理器,负责管理全局事务,分配事务唯一标识,监控事务,执行进度,并负责事务的提交回滚、失败恢复等。

RM(Resource Manager):资源管理器,也就是事务的参与者,绝大部分情况下就是指数据库,一个分布式事务往往涉及到多个 RM。(后文会以关系型数据库为例)

2PC 将事务的提交过程分为2个阶段:准备阶段 和 提交阶段

准备阶段(Prepare)
准备阶段的核心是“询问”事务参与者执行本地数据库事务操作是否成功。

准备阶段的工作流程:

  1. 事务协调者/管理者(后文简称 TM)向所有涉及到的事务参与者(后文简称 RM)发送消息询问:“你是否可以执行事务操作呢?”,并等待其答复。
  2. RM 接收到消息之后,开始执行本地数据库事务预操作比如写redolog/undo log 日志,此时并不会提交事务
  3. RM 如果执行本地数据库事务操作成功,那就回复“Yes”表示我已就绪,否则就回复“No”表示我未就绪。

提交阶段(Commit)
提交阶段的核心是“询问”事务参与者提交本地事务是否成功。

当所有事务参与者都是“就绪”状态的话:

  1. TM 向所有参与者发送消息:“你们可以提交事务了”(Commit 消息)
  2. RM 接收到 Commit 消息 后执行 提交本地数据库事务 操作,执行完成之后 释放整个事务期间所占用的资源。
  3. RM 回复:“事务已经提交”(ACK 消息)
  4. TM 收到所有 事务参与者 的 ACK 消息 之后,整个分布式事务过程正式结束

成功情况

image-20240526232348955

失败情况

image-20240526232328855

2PC 的优点:

  • 实现起来非常简单,各大主流数据库比如 MySQL、Oracle 都有自己实现。
  • 针对的是数据强一致性。不过,仍然可能存在数据不一致的情况。

2PC 存在的问题:

  • 同步阻塞:事务参与者会在正式提交事务之前会一直占用相关的资源。比如用户小明转账给小红,那其他事务也要操作用户小明或小红的话,就会阻塞,直到收到事务管理器的commit然后把事务提交
  • 数据不一致:由于网络问题或者TM宕机都有可能会造成数据不一致的情况。比如在第2阶段(提交阶段),部分网络出现问题导致部分参与者收不到Commit/Rollback 消息的话,就会导致数据不一致。
  • 单点问题: TM在其中也是一个很重要的角色,如果TM在准备(Prepare)阶段完成之后挂掉的话,事务参与者就会一直卡在提交(Commit)阶段。

3PC(三阶段提交协议)

3PC是人们在 2PC的基础上做了一些优化得到的。3PC把 2PC中的准备阶段(Prepare) 做了进一步细化,分为2个阶段:

  • 准备阶段(CanCommit)
  • 预提交阶段(PreCommit)
image-20240530104945881

准备阶段(CanCommit)

准备阶段 RM 不会执行事务操作,TM 只是向 RM 发送 准备请求 ,顺便询问一些信息比如事务参与者能否执行本地数据库事务操作。RM 回复“Yes”“No”或者直接超时未回复。

预提交阶段(PreCommit)

如果准备阶段所有的 RM 回复“Yes”的话,TM 就会向所有的 RM 发送 PreCommit 消息(预提交请求),RM 收到消息之后会执行本地数据库事务预操作比如写redo log/undo log 日志。

如果准备阶段有任一RM 回复“NO”或者直接超时未回复的话,TM 就会给所有 **RM 发送 Abort 消息(中断请求)**,RM 收到消息后直接中断事务。这样其实对 RM 来说损失并不大,因为本质上 RM 到现在还并没有实际做什么事情。

如果 RM 成功的执行了事务预操作,就返回“YES”。否则,返回“No”(最后的反悔机会)

预提交阶段 TM 与 RM 都引入了超时机制,如果 参与者 没有收到 TM 的 PreCommit 消息,或者 TM 没有收到参与者返回的预执行结果状态,那么在超过等待时间后,事务就会中断,这就避免了事务的阻塞。

执行事务提交阶段(DoCommit)

执行事务提交(DoCommit)阶段就开始进行真正的事务提交。

如果预提交阶段所有的 RM 回复“YES”的话,TM 就会向所有的 RM 发送 DoCommit 消息(执行事务提交请求),RM 收到消息之后会执行本地数据库事务提交,并在完成后释放占用的资源。当事务提交成功后,RM 会返回“YES”

如果预提交阶段有任一 RM 回复“NO”或者直接超时未回复的话,TM 就会给所有 **RM 发送 Abort 消息(中断请求)**,RM 收到消息后会进行事务回滚,释放资源,中断本次事务。

如果 RM 在设定时间内并没有 TM 的 DoCommit 消息,RM 会认为 TM 可能发生了故障,会直接进行事务提交。
只要预提交阶段阶段所有参与者都返回了 Yes,那么只要进入第三阶段,事务基本上都能执行成功的。

3PC除了将2PC中的准备阶段(Prepare)做了进一步细化之外,还做了哪些改进?

超时机制:3PC还同时在事务管理者和事务参与者中引入了超时机制 ,如果在一定时间内没有收到事务参与者的消息就默认失败,进而避免事务参与者一直阻塞占用资源。2PC中只有事务管理者才拥有超时机制,当事务参与者长时间无法与事务协调者通讯的情况下(比如协调者挂掉了),就会导致无法释放资源阻塞的问题。

不过,3PC并没有完美解决 2PC的阻塞问题,引入了一些新问题比如性能糟糕,而且,依然存在数据不一致性问题。因此,3PC的实际应用并不是很广泛,多数应用会选择通过复制状态机解决 2PC的阻塞问题。

TCC(补偿事务)

TCC 属于目前比较火的一种柔性事务解决方案。简单来说,TCC是 Try、Confirm、Cancel 三个词的缩写,它分为三个阶段。TCC 模式不需要依赖于底层数据资源的事务支持,但是需要我们手动实现更多的代码,属于 侵入业务代码 的一种分布式解决方案。

1.Try(尝试)阶段:尝试执行。完成业务检查,并预留好必需的业务资源。

2.Confirm(确认)阶段:确认执行。当所有事务参与者的Try 阶段执行成功就会执行 Confirm,Confirm 阶段会处理 Try 阶段预留的业务资源。否则,就会执行 Cancel 。

3.Cancel(取消)阶段:取消执行,释放 Try 阶段预留的业务资源。

每个阶段由业务代码控制,这样可以避免长事务,性能更好。

正常情况下,会执行 try,confirm 方法

image-20240526235251641

出现异常的话,会执行try,cancel 方法。

image-20240526235331333

Try 阶段出现问题的话,可以执行 Cancel。那如果 Confirm 或者 Cancel 阶段失败了怎么办呢?

TCC 会记录事务日志并持久化事务日志到某种存储介质上比如本地文件、关系型数据库、Zookeeper,事务日志包含了事务的执行状态,通过事务执行状态可以判断出事务是提交成功了还是提交失败了,以及具体失败在哪一步。如果发现是 Confirm 或者 Cancel 阶段失败的话,会进行重试,继续尝试执行 Confirm或者 Cancel 阶段的逻辑。重试的次数通常为6次,如果超过重试的次数还未成功执行的话,就需要人工介入处理了。如果代码没有特殊 Bug 的话,Confirm 或者 Cancel 阶段出现问题的概率是比较小的。

事务日志会被删除吗?

会的。如果事务提交成功(没有抛出任何异常),就可以删除对应的事务日志,节省资源。

RocketMQ

RocketMQ、 Kafka、Pulsar 、QMQ 都提供了事务相关的功能。事务允许事件流应用将消费,处理,生产消息整个过程定义为一个原子操作。

以RocketMQ为例

image-20240527000818547

  1. MQ 发送方(比如物流服务)在消息队列上开启一个事务,然后发送一个“半消息”给 MQServer/Broker。事务提交之前,半消息对于 MQ 订阅方/消费者(比如第三方通知服务)不可见
    1. “半消息”发送成功的话,MQ 发送方就开始执行本地事务,再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令

  2. MQ 发送方的本地事务执行成功的话,“半消息”变成正常消息,可以正常被消费。MQ发送方的本地事务执行失败的话,会直接回滚。

如果 MQ 发送方提交或者回滚事务消息时失败怎么办?

RocketMQ 中的 Broker 会定期去 MQ 发送方上反査这个事务的本地事务的执行情况,并根据反查结果决定提交或者回滚这个事务。

事务反查机制的实现依赖于我们业务代码实现的对应的接口,比如你要查看创建物流信息的本地事务是否执行成功的话,直接在数据库中查询对应的物流信息是否存在即可。

如果正常消息没有被正确消费怎么办呢?

消息消费失败的话,RocketMQ 会自动进行消费重试。如果超过最大重试次数这个消息还是没有正确消费,RocketMQ 就会认为这个消息有问题,然后将其放到死信队列。

进入死信队列的消费一般需要人工处理,手动排查问题

QMQ

QMQ 的事务消息就没有 RocketMQ 实现的那么复杂了,它借助了数据库自带的事务功能。其核心思想其实就是 eBay 提出的本地消息表方案,将分布式事务拆分成本地事务进行处理。

本地消息就是利用了关系型数据库的事务能力,会在数据库中存放一张本地事务消息表,在进行本地事务操作中加入了本地消息表的插入,即将业务的执行和将消息放入到消息表中的操作放在同一个事务中提交。

这样本地事务执行成功的话,消息肯定也插入成功,然后再调用其他服务,如果其他服务调用成功就修改这条本地消息的状态。如果失败也不要紧,会有一个后台线程扫描,发现这些状态的消息,会一直调用相应的服务,一般会设置重试的次数,如果一直不行则特殊记录,待人工介入处理。

可以看到,本地事务消息表还是很简单的,也是一种最大努力通知的思想。

image-20240527001820985

面试官:RocketMQ 分布式事务消息的缺点?

总结

可以看出 2PC 和 3PC 是一种强一致性事务,不过还是有数据不一致,阻塞等风险,而且只能用在数据库层面

而 TCC 是一种补偿性事务思想,适用的范围更广,在业务层面实现,因此对业务的侵入性较大,每一个操作都需要实现对应的三个方法

RocketMQ,QMQ其实都是最终一致性事务,因此适用于一些对时间不敏感的业务

分布式事务开源项目

Seata:Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。经历过双 11的实战考验。

Hmily : Hmily 是一款高性能,零侵入,金融级分布式事务解决方案,目前主要提供柔性事务的支持,包含 TCC,TAC(自动生成回滚 SQL)方案,未来还会支持XA等方案。个人开发项目,目前在京东数科重启,未来会成为京东数科的分布式事务解决方案。

Raincat:2 阶段提交分布式事务中间件。

Myth:采用消息队列解决分布式事务的开源框架,基于Java 语言来开发(JDK1.8),支持 DubboSpringCloud,Motan 等rpc 框架进行分布式事务。


分布式事务
http://example.com/2024/05/21/项目/微服务/分布式事务/
作者
PALE13
发布于
2024年5月21日
许可协议