分布式事务

151 阅读14分钟

Java.jpeg

1、事务

事务是逻辑上的一组操作,要么都执行,要么都不执行。

1.1、数据库事务

大多数情况下,我们在谈论事务的时候,如果没有特指分布式事务,往往指的就是数据库事务。数据库事务在我们日常开发中接触的最多了。如果你的项目属于单体架构的话,你接触到的往往就是数据库事务了。

1.1.1、数据库事务作用

简单来说,数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行 。

1.1.2、四大特性ACID

  1. 原子性(Atomicity) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性(Consistency): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
  3. 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. 持久性(Durabilily): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

补充:只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!

1.1.3、数据事务的实现原理

我们这里以 MySQL 的 InnoDB 引擎为例来简单说一下。MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。MySQL InnoDB 引擎通过锁机制、MVCC 等手段来保证事务的隔离性(默认支持的隔离级别是 REPEATABLE-READ )。

1.2、分布式事务

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

那么如何保证这一组操作要么都执行成功,要么都执行失败呢?这个时候单单依靠数据库事务就不行了!我们就需要引入 分布式事务这个概念了!实际上,只要跨数据库的场景都需要用到引入分布式事务。比如说单个数据库的性能达到瓶颈或者数据量太大的时候,我们需要进行 分库。分库之后,同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。一言蔽之,分布式事务的终极目标就是保证系统中多个相关联的数据库中的数据的一致性!\

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

1.2.1、一致性的三种级别

我们可以把对于系统一致性的要求分为下面 3 种级别:

  1. 强一致性 :系统写入了什么,读出来的就是什么。
  2. 弱一致性 :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
  3. 最终一致性 :弱一致性的升级版。系统会保证在一定时间内达到数据一致的状态, 除了上面这 3 个比较常见的一致性级别之外,还有读写一致性、因果一致性等一致性模型。业界比较推崇是最终一致性,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。

1.2.2、柔性事务

互联网应用最关键的就是要保证高可用, 计算式系统几秒钟之内没办法使用都有可能造成数百万的损失。在此场景下,一些大佬们在 CAP 理论和 BASE 理论的基础上,提出了 柔性事务 的概念。 柔性事务追求的是最终一致性。
实际上,柔性事务就是 BASE 理论 +业务实践。 柔性事务追求的目标是:我们根据自身业务特性,通过适当的方式来保证系统数据的最终一致性。 像 TCC、 Saga、MQ 事务 、本地消息表 就属于柔性事务。

1.2.3、刚性事务

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

2、分布式事务解决方案

分布式事务的解决方案有很多,比如:2PC、3PC、TCC、本地消息表、MQ 事务(Kafka 和 RocketMQ 都提供了事务相关功能) 、Saga 等等。这些方案的适用场景有所区别,我们需要根据具体的场景选择适合自己项目的解决方案。

2.1、2PC(两阶段提交协议)

两阶段提交协议:2PC(Two-Phase Commit)这三个字母的含义:
●2 -> 指代事务提交的 2 个阶段
●P-> Prepare (准备阶段)
●C ->Commit(提交阶段)

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

2.1.1、准备阶段(Prepare)

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

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

2.1.2、提交阶段(Commit)

提交阶段的核心是“询问”事务参与者提交事务是否成功。
(1)、当所有事务参与者都是“就绪”状态的话:

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

(2)、当任一事务参与者是“未就绪”状态的话:

  1. 事务协调者/管理者 向所有参与者发送消息:“你们可以执行回滚操作了!”(rollback 消息)。
  2. 事务参与者 接收到 rollback 消息 后执行 本地数据库事务回滚 执行完成之后 释放整个事务期间所占用的资源。
  3. 事务参与者 回复:“事务已经回滚” (ack 消息)。
  4. 事务协调者/管理者 收到所有 事务参与者 的 ack 消息 之后,取消事务。

2.1.3、总结

  1. 准备阶段的主要目的是,测试事务参与者,能否执行本地数据库事务操作(!!!注意:这一步并不会提交事务)。
  2. 提交阶段中,事务协调者/管理者会根据准备阶段中,事务参与者的消息来决定是执行事务提交还是回滚操作。
  3. 提交阶段之后一定会结束当前的分布式事务。

优点

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

缺点

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

2.2、3PC(三阶段提交协议)

3PC 是人们在 2PC 的基础上做了一些优化得到的。3PC 把 2PC 中的 准备阶段(Prepare) 做了进一步细化,分为 2 个阶段:
●询问阶段(CanCommit) :这一步 不会执行事务操作,只会询问事务参与者能否执行本地数据库事操作。
●准备阶段(PreCommit) :当所有事物参与者都返回“可执行”之后, 事务参与者才会执行本地数据库事务预操作比如写 redo log/undo log 日志。

除此之外,3PC 还引入了 超时机制 来避免事务参与者一直阻塞占用资源。

2.3、TCC(补偿事务)

2.3.1、介绍

TCC 属于目前比较火的一种柔性事务解决方案。简单来说,TCC是Try、Confirm、Cancel三个词的缩写,它分为三个阶段:

  1. Try(尝试)阶段 : 尝试执行。完成业务检查,并预留好必需的业务资源。
  2. Confirm(确认)阶段 :确认执行。当所有事务参与者的 Try 阶段执行成功就会执行 Confirm ,Confirm 阶段会处理 Try 阶段预留的业务资源。否则,就会执行 Cancel 。
  3. Cancel(取消)阶段 :取消执行,释放 Try 阶段预留的业务资源。

比如转账场景:

  1. Try(尝试)阶段 : 在转账场景下,Try 要做的事情是就是检查账户余额是否充足,预留的资源就是转账资金。
  2. Confirm(确认)阶段 : 如果 Try 阶段执行成功的话,Confirm 阶段就会执行真正的扣钱操作。
  3. Cancel(取消)阶段 :释放 Try 阶段预留的转账资金。

当我们使用TCC模式的时候,需要自己实现try, confirm, cancel这三个方法,来达到最终一致性,也就是说,正常情况下会执行try, confirm。

2.3.2、总结

TCC模式不需要依赖于底层数据资源的事务支持,但是需要我们手动实现更多的代码,属于侵入业务代码的一种分布式解决方案。针对TCC的实现,业界也有一些不错的开源框架。不同的框架对于TCC的实现可能略有不同,不过大致思想都一样。

  1. ByteTCC : ByteTCC 是基于 Try-Confirm-Cancel(TCC)机制的分布式事务管理器的实现。 相关阅读:关于如何实现一个 TCC 分布式事务框架的一点思考
  2. Seata :Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
  3. Hmily : 金融级分布式事务解决方案。

2.3、MQ事务

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

2.3.1、RocketMQ

20200519215814547.png

  1. MQ 发送方(比如物流服务)在消息队列上开启一个事务,然后发送一个“半消息”给 MQ Server/Broker。事务提交之前,半消息对于 MQ 订阅方/消费者(比如第三方通知服务)不可见
  2. “半消息”发送成功的话,MQ 发送方就开始执行本地事务。
  3. MQ 发送方的本地事务执行成功的话,“半消息”变成正常消息,可以正常被消费。MQ 发送方的本地事务执行失败的话,会直接回滚。 从上面的流程中可以看出,MQ 的事务消息使用的是两阶段提交(2PC),简单来说就是咱先发送半消息,等本地事务执行成功之后,半消息才变为正常消息。

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

RocketMQ 中的 Broker 会定期去 MQ 发送方上反查这个事务的本地事务的执行情况,并根据反查结果决定提交或者回滚这个事务。事务反查机制的实现依赖于我们业务代码实现的对应的接口,比如你要查看创建物流信息的本地事务是否执行成功的话,直接在数据库中查询对应的物流信息是否存在即可。

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

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

2.3.1.3、总结

RocketMQ事务主要可以解决高并发的问题,异步处理。并发不高的系统,也可以通过代码的顺序,加数据库事务,再配合定时任务、重试等机制,实现分布式事务。

2.3.2、QMQ

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

我们维护一个本地消息表用来存放消息发送的状态,保存消息发送情况到本地消息表的操作和业务操作要在一个事务里提交。这样的话,业务执行成功代表消息表也写入成功。然后,我们再单独起一个线程定时轮询消息表,把没处理的消息发送到消息中间件。消息发送成功后,更新消息状态为成功或者直接删除消息。

RocketMQ 的事务消息方案中,如果消息队列挂掉,数据库事务就无法执行了,整个应用也就挂掉了。QMQ 的事务消息方案中,即使消息队列挂了也不会影响数据库事务的执行。因此,QMQ 实现的方案能更加适应于大多数业务。不过,这种方法同样适用于其他消息队列,只能说QMQ封装的更好,开箱即用罢了!

2.4、Saga

Saga绝对可以说是历史非常悠久了,Saga事务理论在1987年Hector&Kenneth在ACM发表的论文 《Sagas》 中就被提出了,早于分布式事务概念的提出。Saga 属于长事务解决方案,其核心思想史将长事务拆分为多个本地短事务(本地短事务序列)。

3、其它

  1. ServiceComb Pack :微服务应用的数据最终一致性解决方案。
  2. Seata :Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

WechatIMG29838.jpg