---
icon: creative
title: 分布式事务详解
---

# 分布式事务

## 简介

### 事务

事务是应用程序中一系列严密的操作，所有操作必须成功完成，否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性，一个事务中的一系列的操作要么全部成功，要么一个都不做。事务应该具有 4 个属性：原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。

### 分布式事务

分布式事务是指事务的参与者，支持事务的服务器，资源服务器以及事务管理器分别位于分布式系统的不同节点之上。通常一个分布式事务中会涉及对多个数据源或业务系统的操作。分布式事务也可以被定义为一种嵌套型的事务，同时也就具有了ACID事务的特性。

### 强一致性、弱一致性、最终一致性

**强一致性**

任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程，看到的操作顺序，都和全局时钟下的顺序一致。简言之，在任意时刻，所有节点中的数据是一样的。

**弱一致性**

数据更新后，如果能容忍后续的访问只能访问到部分或者全部访问不到，则是弱一致性。

**最终一致性**

不保证在任意时刻任意节点上的同一份数据都是相同的，但是随着时间的迁移，不同节点上的同一份数据总是在向趋同的方向变化。简单说，就是在一段时间后，节点间的数据会最终达到一致状态。

由于分布式事务方案，无法做到完全的ACID的保证，没有一种完美的方案，能够解决掉所有业务问题。因此在实际应用中，会根据业务的不同特性，选择最适合的分布式事务方案。

## 分布式事务的基础

### CAP理论

**Consistency**（一致性）：数据一致更新，所有数据变动都是同步的（强一致性）。

**Availability**（可用性）：好的响应性能。

**Partition tolerance**（分区容错性） ：可靠性。

定理：任何分布式系统**只可同时满足二点**，没法三者兼顾。

CA系统（放弃P）：指将所有数据（或者仅仅是那些与事务相关的数据）都放在一个分布式节点上，就不会存在网络分区。所以强一致性以及可用性得到满足。

CP系统（放弃A）：如果要求数据在各个服务器上是强一致的，然而网络分区会导致同步时间无限延长，那么如此一来可用性就得不到保障了。坚持事务ACID（原子性、一致性、隔离性和持久性）的传统数据库以及对结果一致性非常敏感的应用通常会做出这样的选择。

AP系统（放弃C）：这里所说的放弃一致性，并不是完全放弃数据一致性，而是放弃数据的强一致性，而保留数据的最终一致性。如果即要求系统高可用又要求分区容错，那么就要放弃一致性了。因为一旦发生网络分区，节点之间将无法通信，为了满足高可用，每个节点只能用本地数据提供服务，这样就会导致数据不一致。一些遵守BASE原则数据库，（如：Cassandra、CouchDB等）往往会放宽对一致性的要求（满足最终一致性即可），一次来获取基本的可用性。

### BASE理论

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

1. 基本可用:分布式系统在出现故障时，允许损失部分可用功能，保证核心功能可用。
2. 软状态:允许系统中存在中间状态，这个状态不影响系统可用性，这里指的是CAP中的不一致。
3. 最终一致:最终一致是指经过一段时间后，所有节点数据都将会达到一致。

BASE解决了CAP中理论没有网络延迟，在BASE中用软状态和最终一致，保证了延迟后的一致性。BASE和 ACID 是相反的，它完全不同于ACID的强一致性模型，而是通过牺牲强一致性来获得可用性，并允许数据在一段时间内是不一致的，但最终达到一致状态。

## 分布式事务解决方案

分布式事务的实现主要有以下 6 种方案：

- 2PC 方案
- TCC 方案
- 本地消息表
- MQ事务
- Saga事务
- 最大努力通知方案

### 2PC方案

2PC方案分为两阶段:

第一阶段：事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作，并反映是否可以提交.

第二阶段：事务协调器要求每个数据库提交数据，或者回滚数据。

优点： 尽量保证了数据的强一致，实现成本较低，在各大主流数据库都有自己实现，对于MySQL是从5.5开始支持。

缺点:

- 单点问题:事务管理器在整个流程中扮演的角色很关键，如果其宕机，比如在第一阶段已经完成，在第二阶段正准备提交的时候事务管理器宕机，资源管理器就会一直阻塞，导致数据库无法使用。
- 同步阻塞:在准备就绪之后，资源管理器中的资源一直处于阻塞，直到提交完成，释放资源。
- 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计，但仍然存在数据不一致性的可能，比如在第二阶段中，假设协调者发出了事务commit的通知，但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作，其余的参与者则因为没有收到通知一直处于阻塞状态，这时候就产生了数据的不一致性。

总的来说，2PC方案比较简单，成本较低，但是其单点问题，以及不能支持高并发（由于同步阻塞）依然是其最大的弱点。

### TCC

TCC 的全称是：`Try`、`Confirm`、`Cancel`。

- **Try 阶段**：这个阶段说的是对各个服务的资源做检测以及对资源进行 **锁定或者预留**。
- **Confirm 阶段**：这个阶段说的是在各个服务中执行实际的操作。
- **Cancel 阶段**：如果任何一个服务的业务方法执行出错，那么这里就需要 **进行补偿**，就是执行已经执行成功的业务逻辑的回滚操作。（把那些执行成功的回滚）
- 

举个简单的例子如果你用100元买了一瓶水， Try阶段:你需要向你的钱包检查是否够100元并锁住这100元，水也是一样的。

如果有一个失败，则进行cancel(释放这100元和这一瓶水)，如果cancel失败不论什么失败都进行重试cancel，所以需要保持幂等。

如果都成功，则进行confirm,确认这100元扣，和这一瓶水被卖，如果confirm失败无论什么失败则重试(会依靠活动日志进行重试)。

这种方案说实话几乎很少人使用，但是也有使用的场景。因为这个**事务回滚实际上是严重依赖于你自己写代码来回滚和补偿**了，会造成补偿代码巨大。

### 本地消息表

本地消息表的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列，再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景，通过对账系统对事后问题的处理。

![](http://img.topjavaer.cn/img/本地消息表.png)

对于本地消息队列来说核心是把大事务转变为小事务。还是举上面用100元去买一瓶水的例子。

1.当你扣钱的时候，你需要在你扣钱的服务器上新增加一个本地消息表，你需要把你扣钱和写入减去水的库存到本地消息表放入同一个事务(依靠数据库本地事务保证一致性。

2.这个时候有个定时任务去轮询这个本地事务表，把没有发送的消息，扔给商品库存服务器，叫他减去水的库存，到达商品服务器之后这个时候得先写入这个服务器的事务表，然后进行扣减，扣减成功后，更新事务表中的状态。

3.商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器，扣钱服务器本地消息表进行状态更新。

4.针对一些异常情况，定时扫描未成功处理的消息，进行重新发送，在商品服务器接到消息之后，首先判断是否是重复的，如果已经接收，在判断是否执行，如果执行在马上又进行通知事务，如果未执行，需要重新执行需要由业务保证幂等，也就是不会多扣一瓶水。

本地消息队列是BASE理论，是最终一致模型，适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等。

### MQ事务

基于 MQ 的分布式事务方案其实是对本地消息表的封装，将本地消息表基于 MQ 内部，其他方面的协议基本与本地消息表一致。

MQ事务方案整体流程和本地消息表的流程很相似，如下图：

![](http://img.topjavaer.cn/img/MQ事务方案.png)

从上图可以看出和本地消息表方案唯一不同就是将本地消息表存在了MQ内部，而不是业务数据库中。

那么MQ内部的处理尤为重要，下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。

在本地消息表方案中，保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务，RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口，方案如下：

**正常情况：事务主动方发消息**

![](http://img.topjavaer.cn/img/事务主动方发消息.png)

这种情况下，事务主动方服务正常，没有发生故障，发消息流程如下：

- 发送方向 MQ 服务端(MQ Server)发送 half 消息。
- MQ Server 将消息持久化成功之后，向发送方 ack 确认消息已经发送成功。
- 发送方开始执行本地事务逻辑。
- 发送方根据本地事务执行结果向 MQ Server 提交二次确认（commit 或是 rollback）。
- MQ Server 收到 commit 状态则将半消息标记为可投递，订阅方最终将收到该消息；MQ Server 收到 rollback 状态则删除半消息，订阅方将不会接受该消息。

**异常情况：事务主动方消息恢复**

![](http://img.topjavaer.cn/img/事务主动方消息恢复.png)

在断网或者应用重启等异常情况下，图中 4 提交的二次确认超时未到达 MQ Server，此时处理逻辑如下：

- MQ Server 对该消息发起消息回查。
- 发送方收到消息回查后，需要检查对应消息的本地事务执行的最终结果。
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认。
- MQ Server基于 commit/rollback 对消息进行投递或者删除。

**优点**

相比本地消息表方案，MQ 事务方案优点是：

- 消息数据独立存储 ，降低业务系统与消息系统之间的耦合。
- 吞吐量大于使用本地消息表方案。

**缺点**

- 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
- 业务处理服务需要实现消息状态回查接口。

### Saga事务

Saga是由一系列的本地事务构成。每一个本地事务在更新完数据库之后，会发布一条消息或者一个事件来触发Saga中的下一个本地事务的执行。如果一个本地事务因为某些业务规则无法满足而失败，Saga会执行在这个失败的事务之前成功提交的所有事务的补偿操作。

Saga的实现有很多种方式，其中最流行的两种方式是：

- **基于事件的方式**。这种方式没有协调中心，整个模式的工作方式就像舞蹈一样，各个舞蹈演员按照预先编排的动作和走位各自表演，最终形成一只舞蹈。处于当前Saga下的各个服务，会产生某类事件，或者监听其它服务产生的事件并决定是否需要针对监听到的事件做出响应。
- **基于命令的方式**。这种方式的工作形式就像一只乐队，由一个指挥家（协调中心）来协调大家的工作。协调中心来告诉Saga的参与方应该执行哪一个本地事务。

### 最大努力通知方案

最大努力通知也称为定期校对，是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口，如果事务被动方没有接收到消息，此时可以调用事务主动方提供的消息校对的接口主动获取。

最大努力通知的整体流程如下图：

![](http://img.topjavaer.cn/img/最大努力通知方案.png)

在可靠消息事务中，事务主动方需要将消息发送出去，并且消息接收方成功接收，这种可靠性发送是由事务主动方保证的；

但是最大努力通知，事务主动方尽最大努力（重试，轮询....）将事务发送给事务接收方，但是仍然存在消息接收不到，此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费，这种通知的可靠性是由事务被动方保证的。

最大努力通知适用于业务通知类型，例如微信交易的结果，就是通过最大努力通知方式通知各个商户，既有回调通知，也有交易查询接口。

## 参考文章

https://www.pdai.tech/md/arch/arch-z-transection.html

https://juejin.cn/post/6844903647197806605#heading-15
