|
|
@ -4684,9 +4684,13 @@ SendResult sendResult = producer.send(msg);
|
|
|
|
|
|
|
|
|
|
|
|
# 分布式事务
|
|
|
|
# 分布式事务
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
一般来讲,99%的分布式接口调用不需要做分布式事务,通过监控(邮件、短信告警)、记录日志,就可以事后快速定位问题,然后就是排查、出解决方案、修复数据。因为用分布式事务一定是有成本的,而且这个成本会比较高,特别是对于一些中小型公司。同时,引入分布式事务后,代码复杂度、开发周期会大幅上升,系统性能和吞吐量会大幅下跌,这就导致系统更加更加脆弱,更容易出Bug。当然,如果有资源能够持续投入,分布式事务做好了的话,好处就是可以100%保证数据一致性不会出错。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**什么是分布式事务?**
|
|
|
|
**什么是分布式事务?**
|
|
|
|
|
|
|
|
|
|
|
|
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。一个大的操作由N多的小的操作共同完成。而这些小的操作又分布在不同的服务上。针对于这些操作,要么全部成功执行,要么全部不执行 。
|
|
|
|
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。一个大的操作由N多的小的操作共同完成。而这些小操作又分布在不同的服务上。针对于这些操作,要么全部成功执行,要么全部不执行 。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -5404,7 +5408,7 @@ ZAB 协议的消息广播过程使用的是一个原子广播协议,类似一
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 两阶段提交/XA(2PC)
|
|
|
|
## 两阶段提交(2PC/XA)
|
|
|
|
|
|
|
|
|
|
|
|
**核心思路**
|
|
|
|
**核心思路**
|
|
|
|
|
|
|
|
|
|
|
@ -5412,73 +5416,126 @@ ZAB 协议的消息广播过程使用的是一个原子广播协议,类似一
|
|
|
|
|
|
|
|
|
|
|
|
![二阶段提交协议](images/Solution/2pc.png)
|
|
|
|
![二阶段提交协议](images/Solution/2pc.png)
|
|
|
|
|
|
|
|
|
|
|
|
熟悉MySQL的同学对两阶段提交应该颇为熟悉,MySQL的事务就是通过**「日志系统」** 来完成两阶段提交的。两阶段协议可以用于单机集中式系统,由事务管理器协调多个资源管理器;也可以用于分布式系统,**「由一个全局的事务管理器协调各个子系统的局部事务管理器完成两阶段提交」** 。
|
|
|
|
MySQL的事务就是通过**日志系统** 来完成两阶段提交的。两阶段协议可以用于单机集中式系统,由事务管理器协调多个资源管理器;也可以用于分布式系统,**由一个全局的事务管理器协调各个子系统的局部事务管理器完成两阶段提交** 。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
两阶段提交(Two-Phase Commit,简称2PC)是将事务实际分为两部分:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **第一阶段:预执行阶段(Prepare)**
|
|
|
|
|
|
|
|
- **第二阶段:确认阶段(Commit或Rollback)**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**伪代码**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
|
|
执行代码{
|
|
|
|
|
|
|
|
//一阶段
|
|
|
|
|
|
|
|
aStatus=参与者A.prepare()
|
|
|
|
|
|
|
|
bStatus=参与者B.prepare()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//二阶段
|
|
|
|
|
|
|
|
if aStatus and bStatus:
|
|
|
|
|
|
|
|
参与者A.commit()
|
|
|
|
|
|
|
|
参与者B.commit()
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
参与者A.rollback()
|
|
|
|
|
|
|
|
参与者B.rollback()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**存在的问题**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **同步阻塞**:整个消息链路是串行的,要等待响应结果,响应时间较长,不适合高并发的场景
|
|
|
|
|
|
|
|
- **单点故障**:一旦事务协调者出现故障,参与者会一直阻塞下去(第二阶段会导致所有参与者都处于锁定事务资源状态中)
|
|
|
|
|
|
|
|
- **数据不一致**:第二阶段由于网络终端等原因,部分参与者执行commit,部分参与者没有执行commit,数据最终不一致
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 第一阶段:投票阶段
|
|
|
|
|
|
|
|
|
|
|
|
### 预执行阶段(Prepare)
|
|
|
|
|
|
|
|
|
|
|
|
![2PC第一阶段](images/Solution/2PC第一阶段.jpg)
|
|
|
|
![2PC第一阶段](images/Solution/2PC第一阶段.jpg)
|
|
|
|
|
|
|
|
|
|
|
|
这个协议有 **「两个角色」** ,A节点是事务的协调者,B和C是事务的参与者。事务的提交分成两个阶段:
|
|
|
|
这个协议有 **两个角色** ,A节点是事务的协调者,B和C是事务的参与者。主要流程如下:
|
|
|
|
|
|
|
|
|
|
|
|
- 第一个阶段是 **「投票阶段」**
|
|
|
|
- 协调者首先将命令 **写入日志**
|
|
|
|
- 协调者首先将命令 **「写入日志」**
|
|
|
|
- **发一个prepare命令** 给B和C节点这两个参与者
|
|
|
|
- **「发一个prepare命令」** 给B和C节点这两个参与者
|
|
|
|
- B和C收到消息后,根据自己的实际情况,**判断自己的实际情况是否可以提交**
|
|
|
|
- B和C收到消息后,根据自己的实际情况,**「判断自己的实际情况是否可以提交」**
|
|
|
|
- 将处理结果 **记录到日志** 系统
|
|
|
|
- 将处理结果 **「记录到日志」** 系统
|
|
|
|
- 将结果 **返回** 给协调者
|
|
|
|
- 将结果 **「返回」** 给协调者
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 第二阶段:决定阶段
|
|
|
|
### 确认阶段(Commit/Rollback)
|
|
|
|
|
|
|
|
|
|
|
|
![2PC第二阶段](images/Solution/2PC第二阶段.jpg)
|
|
|
|
![2PC第二阶段](images/Solution/2PC第二阶段.jpg)
|
|
|
|
|
|
|
|
|
|
|
|
- 第二个阶段是 **「决定阶段」**
|
|
|
|
当A节点收到B和C参与者所有的确认消息后的执行流程如下:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **判断** 所有协调者 **是否都可以提交**
|
|
|
|
|
|
|
|
- 如果可以则 **写入日志** 并且发起commit命令
|
|
|
|
|
|
|
|
- 在网络正常,数据库正常的情况下,过了第一阶段的数据操作,肯定能commit成功(原理可了解数据库事务和锁)
|
|
|
|
|
|
|
|
- 有一个不可以则 **写入日志** 并且发起rollback命令
|
|
|
|
|
|
|
|
- 第一阶段出现问题(比如数据逻辑问题),取消事务的提交
|
|
|
|
|
|
|
|
- 参与者收到协调者发起的命令,**执行命令**
|
|
|
|
|
|
|
|
- 将执行命令及结果 **写入日志**
|
|
|
|
|
|
|
|
- **返回结果** 给协调者
|
|
|
|
|
|
|
|
|
|
|
|
当A节点收到B和C参与者所有的确认消息后
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **「判断」** 所有协调者 **「是否都可以提交」**
|
|
|
|
|
|
|
|
- 如果可以则 **「写入日志」** 并且发起commit命令
|
|
|
|
|
|
|
|
- 有一个不可以则 **「写入日志」** 并且发起abort命令
|
|
|
|
|
|
|
|
- 参与者收到协调者发起的命令,**「执行命令」**
|
|
|
|
|
|
|
|
- 将执行命令及结果 **「写入日志」**
|
|
|
|
|
|
|
|
- **「返回结果」** 给协调者
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 三阶段提交(3PC)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
三阶段提交(Two-Phase Commit,3PC),原理是**减少因网络等异常造成的长时间阻塞**。
|
|
|
|
|
|
|
|
|
|
|
|
### 两阶段提交缺点
|
|
|
|
- 第一阶段:CanCommit阶段
|
|
|
|
|
|
|
|
- 第二阶段:PreCommit阶段
|
|
|
|
|
|
|
|
- 第三阶段:DoCommit阶段
|
|
|
|
|
|
|
|
|
|
|
|
- **单点故障**:一旦事务管理器出现故障,整个系统不可用
|
|
|
|
|
|
|
|
- **数据不一致**:在阶段二,如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分参与者接收到 commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致
|
|
|
|
|
|
|
|
- **响应时间较长**:整个消息链路是串行的,要等待响应结果,不适合高并发的场景
|
|
|
|
|
|
|
|
- **不确定性**:当事务管理器发送 commit 之后,并且此时只有一个参与者收到了 commit,那么当该参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**伪代码**
|
|
|
|
|
|
|
|
|
|
|
|
### 无法解决的问题
|
|
|
|
```java
|
|
|
|
|
|
|
|
执行代码{
|
|
|
|
|
|
|
|
// 一阶段
|
|
|
|
|
|
|
|
参与者A.ping()
|
|
|
|
|
|
|
|
参与者B.ping()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 二阶段
|
|
|
|
|
|
|
|
aStatus=参与者A.prepare().timeout(seconds).returnFalse()
|
|
|
|
|
|
|
|
bStatus=参与者B.prepare().timeout(seconds).returnFalse()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 三阶段
|
|
|
|
|
|
|
|
if aStatus and bStatus:
|
|
|
|
|
|
|
|
参与者A.commit().timeout(seconds).returnFalse()
|
|
|
|
|
|
|
|
参与者B.commit().timeout(seconds).returnFalse()
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
参与者A.rollback().timeout(seconds).returnFalse()
|
|
|
|
|
|
|
|
参与者B.rollback().timeout(seconds).returnFalse()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
当协调者和参与者同时出现故障时,两阶段提交无法保证事务的完整性。如果调者在发出commit消息之后宕机,而唯一接收到commit消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,因为没人知道事务是否已经被提交。
|
|
|
|
**改进后的效果**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 在prepare前,增加一个阶段,用于检查网络等资源是否可用
|
|
|
|
|
|
|
|
- 在prepare阶段,增加超时机制
|
|
|
|
|
|
|
|
- 数据不一致的问题,依旧没有解决,只是缓解了阻塞和单点问题
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 三阶段提交(3PC)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3PC针对2PC做了改进:
|
|
|
|
3PC针对2PC做了改进:
|
|
|
|
|
|
|
|
|
|
|
|
- **引入超时机制**:在2PC中,只有协调者拥有超时机制,3PC同时在协调者和参与者中都引入超时机制
|
|
|
|
- **引入超时机制**:同时在协调者和参与者中都引入超时机制。最多等待N秒,然后直接commit或rollback
|
|
|
|
- **在2PC的第一阶段和第二阶段中插入一个准备阶段**:保证了在最后提交阶段之前各参与节点的状态是一致的
|
|
|
|
- **在2PC的第一阶段和第二阶段中插入一个准备阶段**:保证了在最后提交阶段前确认各参与方是否可执行事务
|
|
|
|
|
|
|
|
|
|
|
|
![三阶段提交协议](images/Solution/3pc.png)
|
|
|
|
![三阶段提交协议](images/Solution/3pc.png)
|
|
|
|
|
|
|
|
|
|
|
|
### 第一阶段:CanCommit
|
|
|
|
### CanCommit阶段
|
|
|
|
|
|
|
|
|
|
|
|
协调者向参与者发送事务执行请求CanCommit,参与者如果可以提交就返回YES响应,否则就返回NO响应。
|
|
|
|
协调者向参与者发送事务执行请求CanCommit,参与者如果可以提交就返回YES响应,否则就返回NO响应。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 第二阶段:PreCommit
|
|
|
|
### PreCommit阶段
|
|
|
|
|
|
|
|
|
|
|
|
协调者根据参与者反馈的结果来决定是否继续执行事务的PreCommit操作,根据协调者反馈的结果,有以下两种可能:
|
|
|
|
协调者根据参与者反馈的结果来决定是否继续执行事务的PreCommit操作,根据协调者反馈的结果,有以下两种可能:
|
|
|
|
|
|
|
|
|
|
|
@ -5492,7 +5549,7 @@ ZAB 协议的消息广播过程使用的是一个原子广播协议,类似一
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 第三阶段:DoCommit
|
|
|
|
### DoCommit阶段
|
|
|
|
|
|
|
|
|
|
|
|
- **执行提交**
|
|
|
|
- **执行提交**
|
|
|
|
- **发送提交请求**:协调者收到ACK之后,向所有的参与者发送DoCommit请求
|
|
|
|
- **发送提交请求**:协调者收到ACK之后,向所有的参与者发送DoCommit请求
|
|
|
@ -5506,28 +5563,76 @@ ZAB 协议的消息广播过程使用的是一个原子广播协议,类似一
|
|
|
|
|
|
|
|
|
|
|
|
## 补偿机制(TCC)
|
|
|
|
## 补偿机制(TCC)
|
|
|
|
|
|
|
|
|
|
|
|
两阶段提交(2PC)和三阶段提交(3PC)并不适用于并发量大的业务场景。TCC事务机制相比于2PC、3PC,不会锁定整个资源,而是通过引入补偿机制,将资源转换为业务逻辑形式,锁的粒度变小。**核心思想**:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。TCC分为三个阶段:
|
|
|
|
**核心思想**:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
两阶段提交(2PC)和三阶段提交(3PC)并不适用于并发量大的业务场景。TCC事务机制相比于2PC/3P而已,不会锁定整个资源,而是通过引入**补偿机制**,将资源转换为业务逻辑形式,锁的粒度变小。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**伪代码:**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
|
|
执行代码{
|
|
|
|
|
|
|
|
//try阶段,类似信用卡预授权,先冻结额度,未实际扣除
|
|
|
|
|
|
|
|
aStatus=参与者A.冻结资源().commit()
|
|
|
|
|
|
|
|
bStatus=参与者B.冻结资源().commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//Confirm阶段,实际扣除(并解冻)
|
|
|
|
|
|
|
|
if aStatus and bStatus:
|
|
|
|
|
|
|
|
(参与者A.扣除资源().commit()).异步执行()
|
|
|
|
|
|
|
|
(参与者B.扣除资源().commit()).异步执行()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
//Cancel阶段,取消冻结
|
|
|
|
|
|
|
|
(参与者A.解冻资源().commit()).异步执行()
|
|
|
|
|
|
|
|
(参与者B.解冻资源().commit()).异步执行()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**TCC优点:**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TCC 事务机制相比于上面的2PC,解决了以下几个问题:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **协调者单点**:由主业务方发起并完成这个业务活动。业务活动管理器(即业务微服务)也变成多点,引入集群
|
|
|
|
|
|
|
|
- **同步阻塞**:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小
|
|
|
|
|
|
|
|
- **数据一致性**:有了补偿机制之后,由业务活动管理器控制一致性
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**TCC缺点:**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **应用侵入性强**:TCC由于基于在业务层面,至使每个操作都需要有 `try`、`confirm`、`cancel`三个接口
|
|
|
|
|
|
|
|
- **开发难度大**:代码开发量很大,为了要保证数据一致性 `confirm` 和 `cancel` 接口还必须实现幂等性
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Try-Confirm-Cancel](images/Solution/Try-Confirm-Cancel.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Try阶段
|
|
|
|
|
|
|
|
|
|
|
|
- **Try**:这个阶段对各个服务的资源做检测以及对资源进行锁定或者预留
|
|
|
|
这个阶段对各个服务的资源做检测以及对资源进行锁定或者预留。
|
|
|
|
- **Confirm** :执行真正的业务操作,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作要求具备幂等设计,Confirm失败后需要进行重试
|
|
|
|
|
|
|
|
- **Cancel**:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,即执行回滚操作,释放Try阶段预留的业务资源 ,Cancel操作要求具备幂等设计,Cancel失败后需要进行重试
|
|
|
|
|
|
|
|
![Try-Confirm-Cancel](images/Solution/Try-Confirm-Cancel.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TCC 事务机制相比于上面介绍的2PC,解决了其几个缺点:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **协调者单点**。由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群
|
|
|
|
|
|
|
|
- **同步阻塞**。引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小
|
|
|
|
|
|
|
|
- **数据一致性**。有了补偿机制之后,由业务活动管理器控制一致性
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
总之,TCC 就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,并且很大程度的**增加**了业务代码的**复杂度**,因此,这种模式并不能很好地被复用。
|
|
|
|
### Confirm阶段
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
执行真正的业务操作,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作要求具备幂等设计,Confirm失败后需要进行重试。
|
|
|
|
|
|
|
|
|
|
|
|
**TCC案例场景**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TCC将一次事务操作分为三个阶段:Try、Confirm、Cancel,我们通过一个订单/库存的示例来理解。假设我们的分布式系统一共包含4个服务:订单服务、库存服务、积分服务、仓储服务,每个服务有自己的数据库,如下图:
|
|
|
|
|
|
|
|
|
|
|
|
### Cancel阶段
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,即执行回滚操作,释放Try阶段预留的业务资源 ,Cancel操作要求具备幂等设计,Cancel失败后需要进行重试。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### TCC案例场景
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
我们通过一个订单/库存的示例来理解。假设我们的分布式系统一共包含4个服务:订单服务、库存服务、积分服务、仓储服务,每个服务有自己的数据库,如下图:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![TCC案例](images/Solution/TCC案例.png)
|
|
|
|
|
|
|
|
|
|
|
|
![TCC案例场景-订单服务](images/Solution/TCC案例场景-订单服务.png)
|
|
|
|
![TCC案例场景-订单服务](images/Solution/TCC案例场景-订单服务.png)
|
|
|
|
|
|
|
|
|
|
|
|
从正常流程上讲,TCC仍是一个两阶段提交协议。但在执行出现问题的时候,有一定的自我修复能力,如果任何一个事务参与者出现了问题,协调者可以通过执行逆操作来取消之前的操作,达到最终的一致状态(比如冲正交易、查询交易)。从TCC的执行流程也可以看出,服务提供方需要提供额外的补偿逻辑,那么原来一个服务接口,引入TCC后可能要改造成3种逻辑:
|
|
|
|
从正常流程看,TCC仍是一个两阶段提交协议。但在执行出现问题时,有一定的自我修复能力,若任何一个事务参与者出现问题,协调者可通过执行逆操作来取消之前的操作,达到最终的一致状态(比如冲正交易、查询交易)。从TCC的执行流程也可以看出,服务提供方需要提供额外的补偿逻辑,那么原来一个服务接口,引入TCC后可能要改造成3种逻辑:
|
|
|
|
|
|
|
|
|
|
|
|
- **Try**:先是服务调用链路依次执行Try逻辑
|
|
|
|
- **Try**:先是服务调用链路依次执行Try逻辑
|
|
|
|
- **Confirm**:如果都正常的话,TCC分布式事务框架推进执行Confirm逻辑,完成整个事务
|
|
|
|
- **Confirm**:如果都正常的话,TCC分布式事务框架推进执行Confirm逻辑,完成整个事务
|
|
|
@ -5537,7 +5642,7 @@ TCC将一次事务操作分为三个阶段:Try、Confirm、Cancel,我们通
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Try
|
|
|
|
**① Try阶段**
|
|
|
|
|
|
|
|
|
|
|
|
Try阶段一般用于锁定某个资源,设置一个预备状态或冻结部分数据。对于示例中的每一个服务,Try阶段所做的工作如下:
|
|
|
|
Try阶段一般用于锁定某个资源,设置一个预备状态或冻结部分数据。对于示例中的每一个服务,Try阶段所做的工作如下:
|
|
|
|
|
|
|
|
|
|
|
@ -5550,7 +5655,7 @@ Try阶段一般用于锁定某个资源,设置一个预备状态或冻结部
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Confirm
|
|
|
|
**② Confirm阶段**
|
|
|
|
|
|
|
|
|
|
|
|
根据Try阶段的执行情况,Confirm分为两种情况:
|
|
|
|
根据Try阶段的执行情况,Confirm分为两种情况:
|
|
|
|
|
|
|
|
|
|
|
@ -5565,40 +5670,33 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
- 仓储服务:修改销售出库单的状态为已创建-CREATED
|
|
|
|
- 仓储服务:修改销售出库单的状态为已创建-CREATED
|
|
|
|
![TCC-Confirm](images/Solution/TCC-Confirm.png)
|
|
|
|
![TCC-Confirm](images/Solution/TCC-Confirm.png)
|
|
|
|
|
|
|
|
|
|
|
|
> Confirm阶段的各个服务本身可能出现问题,这时候一般就需要TCC框架了(比如ByteTCC,tcc-transaction,himly),TCC事务框架一般会记录一些分布式事务的活动日志,保存事务运行的各个阶段和状态,从而保证整个分布式事务的最终一致性。
|
|
|
|
> 注意:Confirm阶段的各个服务本身可能出现问题,这时候一般就需要TCC框架了(比如ByteTCC,tcc-transaction,himly),TCC事务框架一般会记录一些分布式事务的活动日志,保存事务运行的各个阶段和状态,从而保证整个分布式事务的最终一致性。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Cancel
|
|
|
|
**③ Cancel阶段**
|
|
|
|
|
|
|
|
|
|
|
|
如果Try阶段执行异常,就会执行Cancel阶段。比如:对于订单服务,可以实现的一种Cancel逻辑就是:将订单的状态设置为“CANCELED”;对于库存服务,Cancel逻辑就是:将冻结库存扣减掉,加回到可销售库存里去。
|
|
|
|
如果Try阶段执行异常,就会执行Cancel阶段。比如:对于订单服务,可以实现的一种Cancel逻辑就是:将订单的状态设置为“CANCELED”;对于库存服务,Cancel逻辑就是:将冻结库存扣减掉,加回到可销售库存里去。
|
|
|
|
![TCC-Cancel](images/Solution/TCC-Cancel.png)
|
|
|
|
![TCC-Cancel](images/Solution/TCC-Cancel.png)
|
|
|
|
|
|
|
|
|
|
|
|
> 许多公司为了简化TCC使用,通常会将一个服务的某个核心接口拆成两个,如库存服务的扣减库存接口,拆成两个子接口:①扣减接口 ②回滚扣减库存接口,由TCC框架来保证当某个接口执行失败后去执行对应的rollback接口。
|
|
|
|
> 注意:许多公司为了简化TCC使用,通常会将一个服务的某个核心接口拆成两个,如库存服务的扣减库存接口,拆成两个子接口:扣减接口和回滚扣减库存接口,由TCC框架来保证当某个接口执行失败后去执行对应的rollback接口。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 可靠消息最终一致性方案
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
该方案其实就是在分布式系统当中,把一个业务操作转换成一个消息,然后利用消息来实现事务的最终一致性。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
> 比如从A账户向B账户转账的操作,当服务A从A账户扣除完金额后,通过消息中间件向服务B发一个消息,服务B收到这条消息后,进行B账户的金额增加操作。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可靠消息最终一致性方案一般有两种实现方式,原理其实是一样的:
|
|
|
|
## 可靠消息方案(MQ)
|
|
|
|
|
|
|
|
|
|
|
|
- **基于本地消息表**
|
|
|
|
**基于可靠消息方案**也称之为**最终一致性方案**,一般适用于**异步场景**的服务调用,是目前业务主流的分布式事务落地方案。
|
|
|
|
- **基于支持分布式事务的消息中间件,如RocketMQ等**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可靠消息最终一致性方案,一般适用于异步的服务调用,比如支付成功后,调用积分服务进行积分累加、调用库存服务进行发货等等。总结一下,可靠消息最终一致性方案其实最基本的思想就两点:
|
|
|
|
**实现原理:**
|
|
|
|
|
|
|
|
|
|
|
|
- **通过引入消息中间件,保证生产者对消息的100%可靠投递**
|
|
|
|
- 用mq消息异步传递子事务状态,最终达到全局事务的完成
|
|
|
|
- **通过引入Zookeeper,保证消费者能够对未成功消费的消息进行重新消费(消费者要保证自身接口的幂等性)**
|
|
|
|
- 特点是由发起方决定是否回滚,也就是说只要发起者成功,后续的子事务基本都能成功。比如刷卡后,增加消费积分
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可靠消息最终一致性方案是目前业务主流的分布式事务落地方案,其优缺点主要如下:
|
|
|
|
**优/缺点:**
|
|
|
|
|
|
|
|
|
|
|
|
- **优点**:消息数据独立存储,降低业务系统与消息系统间的耦合
|
|
|
|
- **优点**:消息数据独立存储,降低业务系统与消息系统间的耦合
|
|
|
|
|
|
|
|
|
|
|
@ -5606,11 +5704,7 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
一般来讲,99%的分布式接口调用不需要做分布式事务,通过监控(邮件、短信告警)、记录日志,就可以事后快速定位问题,然后就是排查、出解决方案、修复数据。因为用分布式事务一定是有成本的,而且这个成本会比较高,特别是对于一些中小型公司。同时,引入分布式事务后,代码复杂度、开发周期会大幅上升,系统性能和吞吐量会大幅下跌,这就导致系统更加更加脆弱,更容易出bug。当然,如果有资源能够持续投入,分布式事务做好了的话,好处就是可以100%保证数据一致性不会出错。
|
|
|
|
### 方案一:本地消息表
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 本地消息表
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![分布式事务-本地消息表](images/Solution/分布式事务-本地消息表.png)
|
|
|
|
![分布式事务-本地消息表](images/Solution/分布式事务-本地消息表.png)
|
|
|
|
|
|
|
|
|
|
|
@ -5621,7 +5715,7 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
- **消费者(下游服务)**:消费者是接口的服务方,消费消息
|
|
|
|
- **消费者(下游服务)**:消费者是接口的服务方,消费消息
|
|
|
|
![本地消息表](images/Solution/本地消息表.png)
|
|
|
|
![本地消息表](images/Solution/本地消息表.png)
|
|
|
|
|
|
|
|
|
|
|
|
#### 可靠消息服务
|
|
|
|
**① 可靠消息服务**
|
|
|
|
|
|
|
|
|
|
|
|
可靠消息服务就是一个单独的服务,有自己的数据库,其主要作用就是存储消息(包含接口调用信息,全局唯一的消息编号),消息通常包含以下状态:
|
|
|
|
可靠消息服务就是一个单独的服务,有自己的数据库,其主要作用就是存储消息(包含接口调用信息,全局唯一的消息编号),消息通常包含以下状态:
|
|
|
|
|
|
|
|
|
|
|
@ -5632,7 +5726,7 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 生产者
|
|
|
|
**② 生产者**
|
|
|
|
|
|
|
|
|
|
|
|
服务调用方(消息生产者)需要调用下游接口时,不直接通过RPC之类的方式调用,而是先生成一条消息,其主要步骤如下:
|
|
|
|
服务调用方(消息生产者)需要调用下游接口时,不直接通过RPC之类的方式调用,而是先生成一条消息,其主要步骤如下:
|
|
|
|
|
|
|
|
|
|
|
@ -5640,7 +5734,7 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
- 生产者执行本地事务,本地事务执行成功并提交后,向可靠消息服务发送一条确认消息;如果本地执行失败,则向消息服务发送一条取消消息
|
|
|
|
- 生产者执行本地事务,本地事务执行成功并提交后,向可靠消息服务发送一条确认消息;如果本地执行失败,则向消息服务发送一条取消消息
|
|
|
|
- 可靠消息服务如果收到消息后,修改本地数据库中的那条消息记录的状态改为【已发送】或【已取消】。如果是确认消息,则将消息投递到MQ消息队列;(修改消息状态和投递MQ必须在一个事务里,保证要么都成功要么都失败)
|
|
|
|
- 可靠消息服务如果收到消息后,修改本地数据库中的那条消息记录的状态改为【已发送】或【已取消】。如果是确认消息,则将消息投递到MQ消息队列;(修改消息状态和投递MQ必须在一个事务里,保证要么都成功要么都失败)
|
|
|
|
|
|
|
|
|
|
|
|
> 为了防止出现:生产者的本地事务执行成功,但是发送确认/取消消息超时的情况。可靠消息服务里一般会提供一个后台定时任务,不停的检查消息表中那些【待确认】的消息,然后回调生产者(上游服务)的一个接口,由生产者确认到底是取消这条消息,还是确认并发送这条消息。
|
|
|
|
> 注意:为了防止出现:生产者的本地事务执行成功,但是发送确认/取消消息超时的情况。可靠消息服务里一般会提供一个后台定时任务,不停的检查消息表中那些【待确认】的消息,然后回调生产者(上游服务)的一个接口,由生产者确认到底是取消这条消息,还是确认并发送这条消息。
|
|
|
|
|
|
|
|
|
|
|
|
![本地消息表-生产者](images/Solution/本地消息表-生产者.png)
|
|
|
|
![本地消息表-生产者](images/Solution/本地消息表-生产者.png)
|
|
|
|
|
|
|
|
|
|
|
@ -5648,13 +5742,13 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 消费者
|
|
|
|
**③ 消费者**
|
|
|
|
|
|
|
|
|
|
|
|
服务提供方(消息消费者),从MQ消费消息,然后执行本地事务。执行成功后,反过来通知可靠消息服务,说自己处理成功了,然后可靠消息服务就会把本地消息表中的消息状态置为最终状态【已完成】 。这里要注意两种情况:
|
|
|
|
服务提供方(消息消费者),从MQ消费消息,然后执行本地事务。执行成功后,反过来通知可靠消息服务,说自己处理成功了,然后可靠消息服务就会把本地消息表中的消息状态置为最终状态【已完成】 。这里要注意两种情况:
|
|
|
|
|
|
|
|
|
|
|
|
- 消费者消费消息失败,或者消费成功但执行本地事务失败。
|
|
|
|
- **消费者消费消息失败,或者消费成功但执行本地事务失败**
|
|
|
|
针对这种情况,可靠消息服务可以提供一个后台定时任务,不停的检查消息表中那些【已发送】但始终没有变成【已完成】的消息,然后再次投递到MQ,让下游服务来再次处理。也可以引入zookeeper,由消费者通知zookeeper,生产者监听到zookeeper上节点变化后,进行消息的重新投递
|
|
|
|
针对这种情况,可靠消息服务可以提供一个后台定时任务,不停的检查消息表中那些【已发送】但始终没有变成【已完成】的消息,然后再次投递到MQ,让下游服务来再次处理。也可以引入zookeeper,由消费者通知zookeeper,生产者监听到zookeeper上节点变化后,进行消息的重新投递
|
|
|
|
- 如果消息重复投递,消息者的接口逻辑需要实现幂等性,保证多次处理一个消息不会插入重复数据或造成业务数据混乱。
|
|
|
|
- **如果消息重复投递,消费者接口逻辑需要实现幂等性,保证多次处理一个消息不会插入重复数据或造成业务数据混乱**
|
|
|
|
针对这种情况,消费者可以准备一张消息表,用于判重。消费者消费消息后,需要去本地消息表查看这条消息有没处理成功,如果处理成功直接返回成功
|
|
|
|
针对这种情况,消费者可以准备一张消息表,用于判重。消费者消费消息后,需要去本地消息表查看这条消息有没处理成功,如果处理成功直接返回成功
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -5666,12 +5760,12 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 分布式消息中间件
|
|
|
|
### 方案二:消息中间件
|
|
|
|
|
|
|
|
|
|
|
|
许多开源的消息中间件都支持分布式事务,比如RocketMQ、Kafka。其思想几乎是和本地消息表/服务实一样的,只不过是将可靠消息服务和MQ功能封装在一起,屏蔽了底层细节,从而更方便用户的使用。这种方案有时也叫做可靠消息最终一致性方案。以RocketMQ为例,消息的发送分成2个阶段:**Prepare阶段**和**确认阶段**。
|
|
|
|
许多开源的消息中间件都支持分布式事务,比如RocketMQ、Kafka。其思想几乎是和本地消息表/服务实一样的,只不过是将可靠消息服务和MQ功能封装在一起,屏蔽了底层细节,从而更方便用户的使用。这种方案有时也叫做可靠消息最终一致性方案。以RocketMQ为例,消息的发送分成2个阶段:**Prepare阶段**和**确认阶段**。
|
|
|
|
![分布式消息中间件](images/Solution/分布式消息中间件.png)
|
|
|
|
![分布式消息中间件](images/Solution/分布式消息中间件.png)
|
|
|
|
|
|
|
|
|
|
|
|
#### prepare阶段
|
|
|
|
**① prepare阶段**
|
|
|
|
|
|
|
|
|
|
|
|
- 生产者发送一个不完整的事务消息——HalfMsg到消息中间件,消息中间件会为这个HalfMsg生成一个全局唯一标识,生产者可以持有标识,以便下一阶段找到这个HalfMsg
|
|
|
|
- 生产者发送一个不完整的事务消息——HalfMsg到消息中间件,消息中间件会为这个HalfMsg生成一个全局唯一标识,生产者可以持有标识,以便下一阶段找到这个HalfMsg
|
|
|
|
- 生产者执行本地事务
|
|
|
|
- 生产者执行本地事务
|
|
|
@ -5680,7 +5774,7 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 确认阶段
|
|
|
|
**② 确认阶段**
|
|
|
|
|
|
|
|
|
|
|
|
- 如果生产者执行本地事务成功,就向消息中间件发送一个Commit消息(包含之前HalfMsg的唯一标识),中间件修改HalfMsg的状态为【已提交】,然后通知消费者执行事务
|
|
|
|
- 如果生产者执行本地事务成功,就向消息中间件发送一个Commit消息(包含之前HalfMsg的唯一标识),中间件修改HalfMsg的状态为【已提交】,然后通知消费者执行事务
|
|
|
|
- 如果生产者执行本地事务失败,就向消息中间件发送一个Rollback消息(包含之前HalfMsg的唯一标识),中间件修改HalfMsg的状态为【已取消】
|
|
|
|
- 如果生产者执行本地事务失败,就向消息中间件发送一个Rollback消息(包含之前HalfMsg的唯一标识),中间件修改HalfMsg的状态为【已取消】
|
|
|
@ -5689,7 +5783,7 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### ACK机制
|
|
|
|
**③ ACK机制**
|
|
|
|
|
|
|
|
|
|
|
|
消费者消费完消息后,可能因为自身异常,导致业务执行失败,此时就必须要能够重复消费消息。RocketMQ提供了ACK机制,即RocketMQ只有收到服务消费者的ack message后才认为消费成功。所以,服务消费者可以在自身业务员逻辑执行成功后,向RocketMQ发送ack message,保证消费逻辑执行成功。
|
|
|
|
消费者消费完消息后,可能因为自身异常,导致业务执行失败,此时就必须要能够重复消费消息。RocketMQ提供了ACK机制,即RocketMQ只有收到服务消费者的ack message后才认为消费成功。所以,服务消费者可以在自身业务员逻辑执行成功后,向RocketMQ发送ack message,保证消费逻辑执行成功。
|
|
|
|
|
|
|
|
|
|
|
@ -5697,7 +5791,7 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
|
|
|
|
|
|
|
|
### 应用案例
|
|
|
|
### 应用案例
|
|
|
|
|
|
|
|
|
|
|
|
我们最后以一个电子商务支付系统的核心交易链路为示例,来更好的理解下可靠消息最终一致性方案。
|
|
|
|
以一个电子商务支付系统的核心交易链路为示例,来更好的理解下可靠消息最终一致性方案。
|
|
|
|
|
|
|
|
|
|
|
|
**交易链路**
|
|
|
|
**交易链路**
|
|
|
|
|
|
|
|
|
|
|
@ -5719,7 +5813,7 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
- 如果订单接口服务收到链路成功的响应,则向MQ投递一个commit消息,确认之前的half-msg,那仓库服务就可消费消息
|
|
|
|
- 如果订单接口服务收到链路成功的响应,则向MQ投递一个commit消息,确认之前的half-msg,那仓库服务就可消费消息
|
|
|
|
- 仓储服务消费消息成功并执行完自身的逻辑后,会向RocketMQ投递一个ack message,以确保消费成功
|
|
|
|
- 仓储服务消费消息成功并执行完自身的逻辑后,会向RocketMQ投递一个ack message,以确保消费成功
|
|
|
|
|
|
|
|
|
|
|
|
> 注意,如果因为网络原因,导致RocketMQ始终没有收到订单接口服务对half-msg的commit或rollback消息,RocketMQ就会回调订单接口服务的某个接口,以查询该half-msg究竟是进行commit还是rollback。
|
|
|
|
> 注意:如果因为网络原因,导致RocketMQ始终没有收到订单接口服务对half-msg的commit或rollback消息,RocketMQ就会回调订单接口服务的某个接口,以查询该half-msg究竟是进行commit还是rollback。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -5735,7 +5829,7 @@ Confirm阶段一般需要各个服务自己实现Confirm逻辑:
|
|
|
|
|
|
|
|
|
|
|
|
## Sagas事务模型
|
|
|
|
## Sagas事务模型
|
|
|
|
|
|
|
|
|
|
|
|
Saga事务模型又叫做长时间运行的事务。其核心思想是**「将长事务拆分为多个本地短事务」**,由Saga事务协调器协调,如果正常结束那就正常完成,如果**「某个步骤失败,则根据相反顺序一次调用补偿操作」**。
|
|
|
|
Saga事务模型又叫做长时间运行的事务。其核心思想是**「将长事务拆分为多个本地短事务」**,由Saga事务协调器协调,如果正常结束那就正常完成,如果**某个步骤失败,则根据相反顺序一次调用补偿操作**。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -5935,7 +6029,7 @@ XA 模式下,用户只需关注“业务 SQL”,Seata 会自动生成一阶
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### MQ消息事务-RocketMQ
|
|
|
|
#### 可靠消息方案
|
|
|
|
|
|
|
|
|
|
|
|
先说说MQ的分布式事务,RocketMq在4.3版本已经正式宣布支持分布式事务,在选择Rokcetmq做分布式事务请务必选择4.3以上的版本。
|
|
|
|
先说说MQ的分布式事务,RocketMq在4.3版本已经正式宣布支持分布式事务,在选择Rokcetmq做分布式事务请务必选择4.3以上的版本。
|
|
|
|
|
|
|
|
|
|
|
|