# 分布式事务

### 一、什么是分布式事务
分布式事务关注的是分布式场景下如何处理事务，是指事务的参与者、支持事务操作的服务器、存储等资源分别位于分布式系统的不同节点之上。

简单来说，分布式事务就是一个业务操作，是由多个细分操作完成的，而这些细分操作又分布在不同的服务器上；事务，就是这些操作要么全部成功执行，要么全部不执行。

### 二、什么时候会出现分布式事务
分布式事务是伴随着系统拆分出现的
- 存储层的拆分,例如订单库,商品库,用户库等等
- 服务层的拆分,例如订单服务,商品服务,营销服务,支付服务等等

### 三、分布式事务的普遍缺点
1. 长时间锁定数据库资源，导致系统的响应不快，并发上不去。
2. 网络抖动出现脑裂情况，导致事物参与者，不能很好地执行协调者的指令，导致数据不一致
3. 单点故障：例如事物协调者，在某一时刻宕机，虽然可以通过选举机制产生新的Leader，但是这过程中，必然出现问题，而TCC，只有强悍的技术团队，才能支持开发，成本太高。

### 四、分布式事务的解决方法
**核心**: 需要统一的一个协调者来作统一的管理。<br />
**角色**: 协调者(Coordinator)、参与者(Participants)

#### 1.两阶段提交(2PC)
Two-Phase Commit，2PC
:::note
二阶段提交算法的成立是基于以下假设的:
1. 在该分布式系统中，存在一个节点作为协调者(Coordinator)，其他节点作为参与者(Participants)，且节点之间可以进行网络通信；
2. 所有节点都采用预写式日志，日志被写入后被保存在可靠的存储设备上，即使节点损坏也不会导致日志数据的丢失；
3. 所有节点不会永久性损坏，即使损坏后仍然可以恢复。
:::
![](/software-engineer/distributed/2pc.png)
> 该图片来自互联网

###### 提交请求阶段(Commit-request)
在提交请求阶段，协调者将通知事务参与者准备提交事务，然后进入表决过程。在表决过程中，参与者将告知协调者自己的决策：同意(事务参与者本地事务执行成功)或取消(本地事务执行故障)，在第一阶段，参与节点并没有进行Commit操作。
> 参与者成功的话,其对应的资源就在占用着

###### 提交阶段(Commit)
在提交阶段，协调者将基于第一个阶段的投票结果进行决策：提交或取消这个事务。这个结果的处理和前面基于半数以上投票的一致性算法不同，必须当且仅当所有的参与者同意提交，协调者才会通知各个参与者提交事务，否则协调者将通知各个参与者取消事务。

参与者在接收到协调者发来的消息后将执行对应的操作，也就是本地 Commit 或者 Rollback。

:::tip
两阶段提交(2PC，Two-phase Commit Protocol)是非常经典的强一致性、中心化的原子提交协议，在各种事务和一致性的解决方案中，都能看到两阶段提交的应用。
:::


##### 2PC缺点
![](/software-engineer/distributed/2pc_problem.png)
> 该图片来自互联网

- 资源被同步阻塞: 在执行过程中，所有参与节点都是事务独占状态，当参与者占有公共资源时，那么第三方节点访问公共资源会被阻塞。
- 协调者可能出现单点故障: 一旦协调者发生故障，参与者会一直阻塞下去。
- 在 Commit 阶段出现数据不一致: 在第二阶段中，假设协调者发出了事务 Commit 的通知，但是由于网络问题该通知仅被一部分参与者所收到并执行 Commit，其余的参与者没有收到通知，一直处于阻塞状态，那么，这段时间就产生了数据的不一致性。

> 因此在某些高度可用性和性能要求较高的分布式系统中可能不是最佳选择。

#### 2.三阶段提交(3PC)
Three-Phase Commit，3PC

为了解决二阶段协议中的同步阻塞等问题，三阶段提交协议在协调者和参与者中都引入了超时机制，并且把两阶段提交协议的第一个阶段拆分成了两步：询问，然后再锁资源，最后真正提交。

三阶段中的 Three Phase 分别为 CanCommit、PreCommit、DoCommit 阶段。
![](/software-engineer/distributed/3pc.png)
> 该图片来自互联网


###### CanCommit阶段
3PC 的 CanCommit 阶段其实和 2PC 的准备阶段很像。协调者向参与者发送 Can-Commit 请求，参与者如果可以提交就返回 Yes 响应，否则返回 No 响应。

###### PreCommit阶段
协调者根据参与者的反应情况来决定是否可以继续事务的 PreCommit 操作。根据响应情况，有以下两种可能。

A. 假如协调者从所有的参与者获得的反馈都是 Yes 响应，那么就会进行事务的预执行：
- 发送预提交请求，协调者向参与者发送 PreCommit 请求，并进入 Prepared 阶段；
- 事务预提交，参与者接收到 PreCommit 请求后，会执行事务操作；
- 响应反馈，如果参与者成功执行了事务操作，则返回 ACK 响应，同时开始等待最终指令。

B. 假如有任何一个参与者向协调者发送了 No 响应，或者等待超时之后，协调者都没有接到参与者的响应，那么就中断事务：
- 发送中断请求，协调者向所有参与者发送 abort 请求；
- 中断事务，参与者收到来自协调者的 abort 请求之后，执行事务的中断。

###### DoCommit阶段
该阶段进行真正的事务提交，也可以分为以下两种情况。

A. 执行提交
- 发送提交请求。协调者接收到参与者发送的 ACK 响应后，那么它将从预提交状态进入到提交状态，并向所有参与者发送 doCommit 请求。
- 事务提交。参与者接收到 doCommit 请求之后，执行正式的事务提交，并在完成事务提交之后释放所有事务资源。
- 响应反馈。事务提交完之后，向协调者发送 ACK 响应。
- 完成事务。协调者接收到所有参与者的 ACK 响应之后，完成事务。

B. 中断事务: 协调者没有接收到参与者发送的 ACK 响应，可能是因为接受者发送的不是 ACK 响应，也有可能响应超时了，那么就会执行中断事务。

C.超时提交: 参与者如果没有收到协调者的通知，超时之后会执行 Commit 操作。

##### 三阶段提交做了哪些改进
1. 引入超时机制   
在 2PC 中，**只有协调者拥有超时机制**，如果在一定时间内没有收到参与者的消息则默认失败，3PC 同时在协调者和参与者中都引入超时机制。
2. 添加预提交阶段   
在 2PC 的准备阶段和提交阶段之间，插入一个准备阶段，使 3PC 拥有 CanCommit、PreCommit、DoCommit 三个阶段，PreCommit 是一个缓冲，保证了在最后提交阶段之前各参与节点的状态是一致的。
3. 三阶段提交协议存在的问题   
三阶段提交协议同样存在问题，具体表现为，在阶段三中，如果参与者接收到了 PreCommit 消息后，出现了不能与协调者正常通信的问题，在这种情况下，参与者依然会进行事务的提交，在这段时间内就出现了数据的不一致性。

:::tip[两阶段和三阶段提交的实际应用]
两阶段提交是一种比较精简的一致性算法/协议，很多关系型数据库都是采用两阶段提交协议来完成分布式事务处理的，典型的比如 MySQL 的 XA 规范。

在事务处理、数据库和计算机网络中，两阶段提交协议提供了分布式设计中的数据一致性的保障，整个事务的参与者要么一致性全部提交成功，要么全部回滚。MySQL Cluster 内部数据的同步就是用的 2PC 协议。

MySQL 的主从复制
在 MySQL 中，二进制日志是 server 层，主要用来做主从复制和即时点恢复时使用的；而事务日志(Redo Log)是 InnoDB 存储引擎层，用来保证事务安全的。

在数据库运行中，需要保证 Binlog 和 Redo Log 的一致性，如果顺序不一致， 则意味着 Master-Slave 可能不一致。

在开启 Binlog 后，如何保证 Binlog 和 InnoDB redo 日志的一致性呢？MySQL 使用的就是二阶段提交，内部会自动将普通事务当做一个 XA 事务(内部分布式事务)来处理：

Commit 会被自动的分成 Prepare 和 Commit 两个阶段；
Binlog 会被当做事务协调者(Transaction Coordinator)，Binlog Event 会被当做协调者日志。
:::


#### 3. 基于消息补偿的最终一致性
异步化在分布式系统设计中随处可见，基于消息队列的最终一致性就是一种异步事务机制，在业务中广泛应用。

**核心实现逻辑:** 基于第三方可靠的消息队列 + 本地消息表 + 补偿机制(例如定时扫表)

> 这种一致性方案,自己在多家公司中实践

#### 4. 最大努力通知
<div style="color:red">不保证最终一致性的柔性事务,这种方式适合可以接受部分不一致的业务场景。</div>

最大努力通知也称为定期校对，是对可靠消息服务的进一步优化。它引入了本地消息表来记录错误消息，然后加入失败消息的定期校对功能，来进一步保证消息会被下游服务消费。

本地消息表的方案最初是由 ebay 的工程师提出，核心思想是将分布式事务拆分成本地事务进行处理，通过消息日志的方式来异步执行。

本地消息表是一种业务耦合的设计，消息生产方需要额外建一个事务消息表，并记录消息发送状态，消息消费方需要处理这个消息，并完成自己的业务逻辑，另外会有一个异步机制来定期扫描未完成的消息，确保最终一致性。



#### 5.TCC(Try Confirm Cancel)
**TCC是业务层面的分布式事务，最终一致性，不会一直持有资源的锁。**   
即: TCC的特别之处在于它不依赖资源管理器(RM)对XA的支持，而是通过对业务逻辑(由业务系统提供的)的调度来实现分布式事务

它是属于**补偿型分布式事务**。它的核心思想是针对每个操作，都要注册一个与其对应的确认和补偿(撤销)操作

TCC在保证强一致性的同时，最大限度提高系统的可伸缩性与可用性。

###### TCC(Try Confirm Cancel):
- Try：尝试待执行的业务。 这个过程并未执行业务，只是完成所有业务的一致性检查，并预留好执行所需的所有资源
- Confirm：确认执行业务。 确认执行业务的操作，不做任何业务检查，只使用Try阶段预留的业务资源。通常情况下，采用TCC则会认为 Confirm 阶段是不会出错的。只要 Try 成功，则 Confirm 一定成功。如果 Confirm 出错了，则需要引入重试机制或人工处理
- Cancel：取消待执行的业务。 取消 Try 阶段预留的业务资源。通常情况下，采用 TCC 则认为 Cancel 阶段也是一定能成功的，若 Cancel 阶段真的出错了，也要引入重试机制或人工处理


TCC 是**业务层面**的分布式事务，最终一致性，不会一直持有资源的锁。
- 优点: 把数据库层的二阶段提交上提到了**应用层**来实现，规避了数据库的2PC性能低下问题,TCC会根据具体业务来实现控制资源锁的粒度变小，不会锁定整个资源
- 缺点: TCC 的 Try、Confirm 和 Cancel 操作功能需业务提供，开发成本高。TCC 对业务的侵入较大和业务紧耦合，需要根据特定的场景和业务逻辑来设计相应的操作

###### TCC在实现时需要考虑到以下几个设计要点:
1. 空回滚

    如果协调者的Try()请求因为网络超时失败，那么协调者在阶段二时会发送Cancel()请求，而这时这个事务参与者实际上之前并没有执行Try()操作而直接收到了Cancel()请求。

    针对这个问题，TCC模式要求在这种情况下Cancel()能直接返回成功，也就是要允许「空回滚」

2. 防悬挂

    接着上面的问题1，Try()请求超时，事务参与者收到Cancel()请求而执行了空回滚，但就在这之后网络恢复正常，事务参与者又收到了这个Try()请求，所以Try()和Cancel()发生了悬挂，也就是先执行了Cancel()后又执行了Try()

    针对这个问题，TCC模式要求在这种情况下，事务参与者要记录下Cancel()的事务ID，当发现Try()的事务ID已经被回滚，则直接忽略掉该请求。

3. 幂等性

    Confirm()和Cancel()的实现必须是幂等的。当这两个操作执行失败时协调者都会发起重试。



<!-- ### 全局事务
- 全局事务是基于DTP模型(分布式事务处理 Distributed Transaction Processing)实现的，它规定了要实现分布式事务需要三种角色
- AP(Application)：应用系统(微服务)
- TM(Transaction Manager)：事务管理器(全局事务管理)
- RM(Resource Manager)：资源管理器(数据库) -->