无法启动分布式事务_分布式事务通用解决方案

先上结论,再分别介绍分布式事务的各种实现方式。

  • 如果业务场景需要强一致性,那么尽量避免将它们放在不同服务中,也就是尽量使用本地事务,避免使用强一致性的分布式事务。
  • 如果业务场景能够接受最终一致性,那么最好是使用基于消息的最终一致性的方案(异步确保型)来解决。
  • 如果业务场景需要强一致性,并且只能够进行分布式服务部署,那么最好是使用 TCC 方案而不是 2PC 方案来解决。

选择的建议

在面临数据一致性问题的时候,首先要从业务需求的角度出发,确定我们对于 3 种一致性模型的接受程度,再通过具体场景来决定解决方案。

从应用角度看,分布式事务的现实场景常常无法规避,在有能力给出其他解决方案前,2PC 也是一个不错的选择。

对购物转账等电商和金融业务,中间件层的 2PC 最大问题在于业务不可见,一旦出现不可抗力或意想不到的一致性破坏,如数据节点永久性宕机,业务难以根据 2PC 的日志进行补偿。金融场景下,数据一致性是命根,业务需要对数据有百分之百的掌控力,建议使用 TCC 这类分布式事务模型,或基于消息队列的柔性事务框架,这两种方案都在业务层实现,业务开发者具有足够掌控力,可以结合 SOA 框架来架构,包括 Dubbo、Spring Cloud 等。

在说分布式事务之前,我们先从数据库事务说起。数据库事务可能大家都很熟悉,在开发过程中也会经常使用到。但是即使如此,可能对于一些细节问题,很多人仍然不清楚。比如很多人都知道数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性(Isolation)和持久性(Durabilily),简称就是 ACID。但是再往下比如问到隔离性指的是什么的时候可能就不知道了,或者是知道隔离性是什么但是再问到数据库实现隔离的都有哪些级别,或者是每个级别他们有什么区别的时候可能就不知道了。

本文并不打算介绍这些数据库事务的这些东西,有兴趣可以搜索一下相关资料。不过有一个知识点我们需要了解,就是假如数据库在提交事务的时候突然断电,那么它是怎么样恢复的呢? 为什么要提到这个知识点呢? 因为分布式系统的核心就是处理各种异常情况,这也是分布式系统复杂的地方,因为分布式的网络环境很复杂,这种“断电”故障要比单机多很多,所以我们在做分布式系统的时候,最先考虑的就是这种情况。这些异常可能有机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失、其他异常等等…

我们接着说本地事务数据库断电的这种情况,它是怎么保证数据一致性的呢?我们使用 SQL Server 来举例,我们知道我们在使用 SQL Server 数据库是由两个文件组成的,一个数据库文件和一个日志文件,通常情况下,日志文件都要比数据库文件大很多。数据库进行任何写入操作的时候都是要先写日志的,同样的道理,我们在执行事务的时候数据库首先会记录下这个事务的 redo 操作日志,然后才开始真正操作数据库,在操作之前首先会把日志文件写入磁盘,那么当突然断电的时候,即使操作没有完成,在重新启动数据库时候,数据库会根据当前数据的情况进行 undo 回滚或者是 redo 前滚,这样就保证了数据的强一致性。

接着,我们就说一下分布式事务。

分布式的几个理论

当我们的单个数据库的性能产生瓶颈的时候,我们可能会对数据库进行分区,这里所说的分区指的是物理分区,分区之后可能不同的库就处于不同的服务器上了,这个时候单个数据库的 ACID 已经不能适应这种情况了,而在这种 ACID 的集群环境下,再想保证集群的 ACID 几乎是很难达到,或者即使能达到那么效率和性能会大幅下降,最为关键的是再很难扩展新的分区了,这个时候如果再追求集群的 ACID 会导致我们的系统变得很差,这时我们就需要引入一个新的理论原则来适应这种集群的情况,就是 CAP 原则或者叫 CAP 定理,那么 CAP 定理指的是什么呢?

CAP定理

CAP 定理是由加州大学伯克利分校 Eric Brewer 教授提出来的,他指出 WEB 服务无法同时满足一下 3 个属性:

dc59028cef247088d0837ee2a278bd71.png
  • 一致性(Consistency) :客户端知道一系列的操作都会同时发生(生效)
  • 可用性(Availability) :每个操作都必须以可预期的响应结束
  • 分区容错性(Partition tolerance) :即使出现单个组件无法可用,操作依然可以完成

具体地讲在分布式系统中,在任何数据库设计中,一个 WEB 应用至多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。

这个定理在迄今为止的分布式系统中都是适用的!为什么这么说呢?这个时候有同学可能会把数据库的 2PC(两阶段提交)搬出来说话了。OK,我们就来看一下数据库的两阶段提交。

对数据库分布式事务有了解的同学一定知道数据库支持的 2PC,又叫做 XA Transactions。 MySQL 从 5.5 版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。 其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:

  • 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交
  • 第二阶段:事务协调器要求每个数据库提交数据

其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。这样做的缺陷是什么呢?咋看之下我们可以在数据库分区之间获得一致性。 如果 CAP 定理是对的,那么它一定会影响到可用性。

如果说系统的可用性代表的是执行某项操作相关所有组件的可用性的和。那么在两阶段提交的过程中,可用性就代表了涉及到的每一个数据库中可用性的和。我们假设两阶段提交的过程中每一个数据库都具有 99.9% 的可用性,那么如果两阶段提交涉及到两个数据库,这个结果就是 99.8%。根据系统可用性计算公式,假设每个月 43200 分钟,99.9% 的可用性就是 43157 分钟, 99.8% 的可用性就是 43114 分钟,相当于每个月的宕机时间增加了 43 分钟。

以上,可以验证出来,CAP 定理从理论上来讲是正确的,CAP 我们先看到这里,等会再接着说。

事务补偿机制

在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。

幂等性

简单的说,业务操作支持重试,不会产生不利影响。常见的实现方式: 为消息额外增加唯一 ID。

BASE 理论

在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是 BASE 理论,它是用来对 CAP 定理进行进一步扩充的。

BASE 理论指的是:

  • Basically Available(基本可用)
  • Soft state(软状态)
  • Eventually consistent(最终一致性)

BASE 理论是对 CAP 中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

一致性模型

数据的一致性模型可以分成以下 3 类:

  1. 强一致性:数据更新成功后,任意时刻所有副本中的数据都是一致的,一般采用同步的方式实现。
  2. 弱一致性:数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承诺具体多久之后可以读到。
  3. 最终一致性:弱一致性的一种形式,数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值。

分布式系统数据的强一致性、弱一致性和最终一致性可以通过 Quorum NRW 算法分析。

缓存数据最终一致性

在我们的业务系统中,缓存(Redis 或者 Memcached)通常被用在数据库前面,作为数据读取的缓冲,使得 I/O 操作不至于直接落在数据库上。以商品详情页为例,假如卖家修改了商品信息,并写回到数据库,但是这时候用户从商品详情页看到的信息还是从缓存中拿到的过时数据,这就出现了缓存系统和数据库系统中的数据不一致的现象。 要解决该场景下缓存和数据库数据不一致的问题我们有以下两种解决方案:

  1. 为缓存数据设置过期时间。当缓存中数据过期后,业务系统会从数据库中获取数据,并将新值放入缓存。这个过期时间就是系统可以达到最终一致的容忍时间。
  2. 更新数据库数据后同时清除缓存数据。数据库数据更新后,同步删除缓存中数据,使得下次对商品详情的获取直接从数据库中获取,并同步到缓存。

柔性事务 vs. 刚性事务

刚性事务是指严格遵循 ACID 原则的事务,例如单机环境下的数据库事务。柔性事务是指遵循 BASE 理论的事务,通常用在分布式环境中,常见的实现方式有: 两阶段提交(2PC),TCC 补偿型提交,基于消息的异步确保型,最大努力通知型。通常对本地事务采用刚性事务,分布式事务使用柔性事务。

有了以上理论之后,我们来看一下分布式事务的问题。

分布式事务的几种解决方案

在分布式系统中,要实现分布式事务,无外乎那几种解决方案。

一、两阶段提交(2PC)

和上一节中提到的数据库 XA 事务一样,两阶段提交就是使用 XA 协议的原理,我们可以从下面这个图的流程来很容易的看出中间的一些比如 commit 和 abort 的细节。

3f5f8fffd08887c60146a067f287fee7.png

两阶段提交这种解决方案属于牺牲了一部分可用性来换取的一致性。

2PC 的核心原理是通过提交分阶段和记日志的方式,记录下事务提交所处的阶段状态,在组件宕机重启后,可通过日志恢复事务提交的阶段状态,并在这个状态节点重试。

如 Coordinator 重启后,通过日志可以确定提交处于 Prepare 还是 PrepareAll 状态,若是前者,说明有节点可能没有 Prepare 成功,或所有节点 Prepare 成功但还没有下发 Commit,状态恢复后给所有节点下发 RollBack;若是 PrepareAll 状态,需要给所有节点下发 Commit,数据库节点需要保证 Commit 幂等。

优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能 100% 保证强一致)

缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景

二、三段式提交

显然,三段式提交协议是基于两段式提交而生的,为了解决两段式提交带来的阻塞等待问题,三段式提交引入 TIMEOUT 机制,可在超时后自动释放资源。

和两段式提交一样,三段式提交协议有两类角色,协调者(Coordinator)和参与者(Participants),由三个阶段构成。询问阶段、预提交阶段、正式提交阶段,在预提交阶段协调者就做出了决定并发送给参与者,在第三阶段正式执行;

第一个阶段:询问阶段。协调者询问每个参与者是否可以进行提交,这时候会出现多种情况。参与者明确自己是否能提交,可以给出“YES or NO”的准确回答,也有可能因为各种因素,导致不能确定,直到此次询问超时,返回“NO”。

第二个阶段:预提交阶段。根据上阶段得到的应答,协调者决定事务 Commit or Abort,将投票最终结果发送给各个参与者,参与者收到此决定后再继续下面的操作,只不过到了此阶段,双方都有超时机制了。协调者也有可能因为各种原因不能及时做出决定,超时后就自动给出了 Abort 决定,与此同时,参与者收到了协调者的决定,需要回传 ACK 信息以确定,如果没有在规定的时间窗口内确认,协调者认为事务应该 Abort。

第三个阶段:正式提交阶段。在上一个阶段,各个参与者已经收到了事务 Commit or Abort 的确认信息,其实这个阶段可以认为是一个二次确认阶段,协调者会发送一个 DoCommit 指令,参与者才真正开始进行事务的操作,并给协调者回复一个 ACK。如果此时协调者接收 ACK 超时,协调者也会 Abort 整个事务。值得注意的是,如果协调者本身发送 DoCommit 就超时了,参与者也不会直接 Abort 事务,而是按照第二个阶段的结果执行。

下面附上两段式提交与三段式提交的框架图:

0afcfa9f22646701d3fe5c7478fd830f.png

三、补偿事务(TCC)

TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。TCC 的概念属于国产,因为支付宝的技术布道而广为人知。 其实,TCC 算是一种编程模型,通常被理解为是一种柔性事务解决方案。

adcefbe90d9124a6fc69d516a160dc13.png

它分为三个阶段:

  • Try 阶段主要是对业务系统做检测及资源预留
  • Confirm 阶段主要是对业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 一定成功。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

举个例子,假如 Bob 要向 Smith 转账,思路大概是: 我们有一个本地方法,里面依次调用

  1. 首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。
  2. 在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
  3. 如果第 2 步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。

如果在 Try 阶段,任何一个服务失败,将会调用这些服务对应的 cancel 方法; 如果 Try 阶段正常完成,则进入 Confirm 阶段

优点:跟 2PC 比起来,实现以及流程相对简单了一些,但数据的一致性比 2PC 也要差一些

缺点:缺点还是比较明显的,在 2,3 步中都有可能失败。TCC 属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用 TCC 不太好定义及处理。

四、本地消息表(eBay 事件队列方案——保证最终一致性—异步确保)

本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于 ebay。我们可以从下面的流程图中看出其中的一些细节:

87283b5a81a91258845a491e5be666af.png

基本思路就是:消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过 MQ 发送到消息的消费方。如果消息发送失败,会进行重试发送。

消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

这种方案遵循 BASE 理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像 2PC 那样复杂的实现(当调用链很长的时候,2PC 的可用性是非常低的),也不会像 TCC 那样可能出现确认或者回滚不了的情况。

优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。 缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

五、MQ 事务消息

有一些第三方的 MQ 是支持事务消息的,比如 RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的 MQ 都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。

以阿里的 RocketMQ 中间件为例,其思路大致为:

第一阶段 Prepared 消息,会拿到消息的地址。 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。

也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了 RocketMQ 会定期扫描消息集群中的事务消息,这时候发现了 Prepared 消息,它会向消息发送者确认,所以生产方需要实现一个 check 接口,RocketMQ 会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

优点:实现了最终一致性,不需要依赖本地数据库事务。

缺点:实现难度大,主流 MQ 不支持

https://cloud.tencent.com/developer/article/1117449

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值