## 分布式事务 笔记
---
### 一、CAP理论,分布式系统三要素:
- C 一致性(Consistency)数据都一样;
- A 可用性(Availability)服务响应时间可接受;
- P 分区容错性(Partition tolerance)一个崩了其他能用。
CAP为什么提出同时只能满足2点,比如:
- 1、满足C,所有数据都一样,则需要同步数据;
- 2、满足A,服务都是正常的且响应时间也可以接受。
- 3、满足P,肯定是多服务或者多服务器。
- a、满足C、A 所有数据一样,时间要短,那么服务器越多数据同步时间越长,所以P不满足。
- b、满足C、P 所有数据一样,服务器多,那么时间不可能短,用户读取非最新数据,所以A不满足。
- c、满足A、P 时间短、服务器多C就不可能满足。
---
### 二、ACID理论,主要是关系型数据库:
- 原子性(Atomicity) 全部成功或全部失败回滚;
- 一致性(Consistency) A与B一共有100块钱,不管怎么转账一共还是只有100;
- 隔离性(Isolation) 并发执行的事务之间不能相互影响,也就是解决事务并发的安全问题;
- 持久性 (Durable) 事务提交后就是永久的。
**事务隔离性内涵以及并发带来的问题有:**
1. 脏读(事务读取另一个事务未提交数据)
a) 两个并发事务A,B。A修改数据未提交,B读取了A未提交的数据;
b) 隔离级别设置为 Read Committed 时,就可以避免脏读,但是仍可能会造成不可重复读;
c) Sql Server、Oracle 默认级别就是 Read committed。
2. 不可重复读(一个事务多次查询返回不同值)
a) 两个并发事务A,B。A读取了数据,B修改数据后,A再读取则数据不同了;
b) 隔离级别设置为Repeatable read时,可以避免不可重复读;
c) MySQL 默认隔离级别就是 Repeatable read。
3. 幻读(事务修改数据后,另一个事务又插入数据,影响读取结果)
a) 两个并发事务A,B。A修改数据后,B插入数据,A又查询发现B插入数据没修改。;
b) 隔离级别设置为Serializable(最高的事务隔离级别)时,不仅可以避免脏读、不可重复读,还可以避免幻读。
**MySql四种隔离级别:**
隔离级别与并发性能成反比,隔离级别越高,并发性能越低
- Serializable (串行化):最高级别,可避免脏读、不可重复读、幻读的发生;
- Repeatable read (可重复读):可避免脏读、不可重复读的发生;
- Read committed (读已提交):可避免脏读的发生;
- Read uncommitted (读未提交):最低级别,任何情况都无法保证。
---
### 三、**数据库的并发控制(锁):**
乐观锁与悲观锁一种解决问题的思想。
乐观锁的理念是:
乐观锁不加锁去执行某项操作,如果发生冲突则失败并重试,直到成功为止。假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性;
悲观锁的理念是:
悲观锁也就是资源独占,其他线程挂起,直到锁释放。假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
##### 乐观锁:
- 乐观锁不锁任何东西,而是提交事务时检查记录是否被其他事务修改,没有则提交,有则回滚;
- 乐观锁采用的实现方式一般是记录数据版本(<font color='red'>版本号、时间戳</font>);
- 读取数据时将版本标识的值一并读出,数据每更新一次同时对版本标识进行更新;
- 当提交更新的时,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对;
- 如果当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据;
- 乐观锁不能解决脏读问题。
##### 悲观锁
- 悲观锁进行资源独占数据处于锁定状态,一般依靠数据库提供的锁机制实现。主要用于数据争用激烈的环境。
- Mysql InnoDB引擎的锁机制(属于悲观锁)
**a) 使用方式可分为:共享锁、排它锁、意向共享锁、意向排他锁**
1. 共享锁/读锁(S):多个事务对于同一数据可以共享一把锁,都能访问到数据,只能读不能修改,注意当前事务也不能改;
2. 排他锁/写锁(X):不能与其他事务并存,一个事务获取数据行的排他锁,其他事务就不能再获取该行的其他锁(包括共享锁和排他锁),普通查询不加锁可以。获取排他锁的事务是可以对数据进行读取和修改;
3. 意向共享锁(IS):对数据行加共享锁(S),需要先获取该表的意向共享锁(IS);
4. 意向排他锁(IX):对数据行加排他锁(X),需要获取该表的意向排他锁(IX)。
意向共享锁和意向排它锁是数据库主动加的,不需要我们手动处理。锁是加<font color='red'>索引</font>上的。
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);
对于普通SELECT语句,InnoDB不会加任何锁,事务可以通过以下语句显示给记录集加共享锁或排他锁。
```c
共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE
```
**b) 粒度可分为:行锁、页锁(间隙锁)、表锁**
1. 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
2. 页级锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
3. 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
<font color='red'>行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件来检索数据才会用到行锁,否则InnoDB将会使用表锁。</font>
##### Mysql MVCC,即多版本并发控制。
Mysql MVCC是一种乐观锁的实现方式
作用:主要解决的是实现对数据库的并发访问,为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
实现:由 3个隐式字段,undo日志 ,Read View 等去完成的。
- DB_TRX_ID 最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR 回滚指针,指向这条记录的上一个版本
- DB_ROW_ID 隐含的自增ID(隐藏主键),数据库表没有主键,就用这个。
- 逻辑删除字段
undo log 是不同事务或相同事务对同一记录修改,记录其版本的线性表。 链首最新记录,链尾即最旧记录。
- undo log 分为insert log、update undo log:
- insert undo log 新增时候产生,提交时候删除
- update undo log 修改、快照查询均需要,由purge线程同一清除(不涉及回滚以及快照查询机制)
read view 事务进行快照查时候产生的读视图
- 进行可见性判断,即根据可见性算法将要被修改的数据最新记录DB_TRX_ID取出来,与系统当前其他活跃事务ID比较
- 如果与Read View的某些属性比较不符合可见性,则通过DB_ROLL_PTR回滚取出undo log中的DB_TRX_ID再比较,即遍历链表undo log的DB_TRX_ID,直到找到满足的DB_TRX_ID
- 那么DB_TRX_ID所在的就旧记录就是当前事务能看见的最新版本
```c
当前读:例如 select xxx lock in share mode(共享锁)、select xxx for update(排他锁)、update、delete、insert 就是当前读。读取时不能修改会对读取加锁。
快照读:即不加锁的非阻塞读,不涉及其他锁冲突就是快照读。前提是当前隔离级别不是串行的。select * from xxx 就是快照读。
当前读与快照读与MVCC关系:
MVCC为了“维持一个数据的多个版本,使得读写操作没有冲突” 这么一个实现,那么就需要Mysql快照读实现读取非阻塞。通过当前读实现悲观锁。
```
```c
MVCC + 悲观锁: MVCC解决读写冲突,悲观锁解决写写冲突
MVCC + 乐观锁: MVCC解决读写冲突,乐观锁解决写写冲突
```
---
#### 四、BASE理论:
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写
BASE是对CAP中一致性和可用性权衡的结果,由CAP演化来的,即:即使无法做到强一致性,也可根据自身特点,做到最终一致性。
1. 简单说,基本可用就是出现故障允许损失可部分可用性,不是全部不可用。例如:响应时间可能变慢,某个功能不可以降级了。
2. 软状态,允许数据存在中间状态并且不影响整体可用性,允许数据之间存在同步延迟。
3. 最终一致性,数据副本之间,允许数据非实时强一致性,达到一定延迟后的最终一致性。
<font color='red'>BASE 理论不同于ACID强一致模型,强调通过牺牲强一致性获得可用性。</font>
---
#### 五、分布式事务选型
#### 5.1 XA
##### X/Open DTP模型
X/Open是一个组织,提出一个DTP模型即分布式事务模型。包含:
1. AP:应用程序;
2. TM: 事务管理器,负责协调RM以及事务调度;
3. RM:资源管理器(数据库或中间件).
<font color='red'>XA协议,即分布式事务处理规范</font>
1. XA定义TM与RM之间的通信接口,搭建起TM与多个RM之间的通讯桥梁。从而在多个库保证了ACID的特性。
2. TM函数比如:
```c
xa_open,xa_close 负责建立与关闭RM连接;
xa_start,xa_end 开始或结束本地事务;
xa_prepare,xa_commit,xa_rollback 预提交、提交、回滚本地事务;
xa_recover 回滚已预提交的事务;
//等
```
##### 5.2 2PC 两阶段提交
一阶段:
1. TM询问RM是否准备好,并等待结果;
2. RM收到请求后,执行事务但不提交,记录日志;
3. RM将事务执行情况反馈给TM,同时RM阻塞等待TM的指令。
二阶段:
1. 经过一阶段后,所有RM回复事务执行情况,存在三种可能性。
a) 所有RM都回复能够正常执行事务;
b) 一个或多个事务执行失败;
c) TM等待超时。
2. 若情况为A,则TM向RM发送commit通知,RM收到通知进行提交,RM向TM返回提交结果;
3. 若情况为B或C,则TM向RM发送rollback通知,RM收到通知回滚事务,RM向TM返回回滚结果。
注:这里要说的,这种方案仅适合单块应用里,跨多个库的分布式事务场景。因为严重依赖于数据库层面来搞定复杂的事务,效率很低,不适合高并发的场景。
微服务本身是禁止服务之间互相访问数据库的。
缺陷:
- 同步阻塞问题,执行过程中,所有节点都是事务阻塞的。
- 协调者故障,参与者将一直阻塞,特别是在二阶段,协调者宕机导致问题无法解决。
- 数据不一致,如由于网络故障,只有一部分参与者接受到commit命令。
##### 5.3 3PC 三阶段提交
在二阶段提交基础上,引入超时机制,并将一阶段拆分两步:询问、再根据返回信息是否通知上锁、最后提交。
一阶段(can_commit):
1. TM询问RM发送询问通知,询问是否可执行事务,TM等待RM回复。
2. RM根据自身情况回复预估值,可以正常执行事务回复确定信息,进入预备状态,反之返回否定信息。
二阶段(pre_commit)
1. 经过一阶段后,所有RM回复情况,存在三种可能性。
a) 所有RM回复确认;
b) 一个或多个RM回复否定;
c) 超时。
2. 若所有RM回复确认,则TM向所有RM发送执行通知,RM收到通知执行不提交事务并返回执行情况。
3. 若回复否定或超时,则TM向所有RM发送abort通知,RM收到中断任务。
4. 执行事务全部正常,则进入提交;只要一个失败超时则进入回滚。
三阶段(do_commit):与2PC相同
注:这里要说的是如果RM无法及时收到TM docommit或abort请求,会在超时之后继续提交。
主要是从概率判定,既然都已收到precommit,那么RM回复的都是YES。 进入第三阶段,虽然超时,但是成功提交几率是很大的。
3PC 可以解决协调者故障,减少同步阻塞,但是无法解决数据不一致问题。 即网络延迟,导致超时提交的问题。
##### 5.4 TCC
所谓TCC即:Try 、Confirm 、Cancel
- Try 阶段:对各个服务的资源做检测以及对资源进行锁定或者预留。
- Confirm 阶段:在各个服务中执行实际的操作。
- Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)
TCC 很少用,原因在于事务回滚严重依赖于代码回滚,补偿代码巨大。
一致性要求高、短流程、并发高的金融领域场景优先考虑 TCC 方案。
这里TCC与3PC最大的区别在于:TCC预提交提交过程都是对开发者可见;TCC是业务层面分布式,不会一直持有锁以补偿性事务实现最终一致性。
##### 5.5 Saga 方案
简单来说,Saga 将长活事务分解成子事务集合,子事务是保证数据库一致性真实事务。
Saga 由 Sub-Transaction Ti组成。每个Ti 都对应补偿动作Ci,补偿用于撤销Ti;与TCC相比,Saga没有预留动作,Ti都是直接提交到库。
1. Saga 执行顺序
T1,T2,T3,Tn或者T1,T2,T3,Tj,Cj,C3,C2,C1。 0 < j < n 。
2. Saga定义两种恢复策略:向后恢复、向前恢复。
向后恢复集按执行子事务相反顺序恢复。
向前恢复,适用于必须成功的场景,进行一直重试。 当然若一直失败可采用降级或人工干预方式。
3. Saga 注意事项
Ci是必须成功的,若失败则需要进行人工干预。
要求Ti和C1是幂等的,因为包含向前与向后恢复策略,所以多次执行Ti或Ci考虑数据一致性所以要求幂等。
还有场景是Ti超时采用向后恢复发送Ci:Ti请求丢失、Ti在Ci之前执行、Ci在Ti之前执行:
第1种情况可通过业务逻辑得到保证。 第2、3种情况则要求Ti与Ci是可交换的,并且结果是子事务被撤销。
4. 使用场景
- Saga 仅允许<font color='red'>两层次嵌套</font>,即顶级Saga和简单子事务;
- 每个子事务应该是独立的原子行为,有对应的数据库保证原子操作。
- 没有事务的隔离性,在某些时候A事务可以读取到B事务的结果。
- 直接提交本地事务,无锁,高性能;
- 参与者可异步执行,高吞吐;
- 补偿服务易于实现,因为一个更新操作的反向操作是比较容易理解的。
.net core 实现实例参考:https://github.com/OpenSagas-csharp/servicecomb-pack-csharp
##### 5.6 本地消息表
这是明显的BASE理论的应用,属于可靠消息最终一致性的范畴。
1. 订单与支付服务,订单表A,订单消息表Ai,支付服务B,支付服务消息表Bi;
2. 订单与订单消息表,一个事务内完成A1,Ai,此时Ai状态为发送中。
3. 通过消息队列,发送消息PayEventMsg;
4. 支付服务消费这条消息,消费成功后回复OrderEventMsg,同时支付服务又来订阅这条消息。
5、支付服务收到消息,将Ai状态设置为已发送或已完成。
主要包含的组件:
1. 一个业务内的消息表,包含数据状态;
2. 一个定时任务,定期轮询发送"发送中"的消息;
3. 一个消息队列中间件(接口也可以,使用消息队列可更好的异步,并发,削峰);
注意事项:
1. 支付服务以及订单服务接口幂等性。因为可能出现重复投递。
2. 有容错机制,即轮询任务的重试或降级策略。引入人工介入渠道。
特点:
1. 消息表的冗余与业务耦合,不利于扩展伸缩;
2. 消息表增加磁盘IO;
3. 简单容易实现
##### 5.7 可靠消息最终一致性方案
其实就是用MQ代替本地消息表。当发起方执行本地事务后并发出消息,事务参与方一定能够接收消息并处理事务成功。此方案是利用消息中间件完成。
要实现此方案,我们需要考虑几个问题:
1.本地事务与消息发送的原子性问题
```c
//此方式消息发送成功,数据库操作失败
begin transaction;
//1.发送MQ
//2.数据操作
commit transation;
//此方式数据库操作成功,如果消息超时数据库回滚,但消息最后发送成功
begin transaction;
//1.数据库
//2.发送MQ
commit transation;
```
2.事务参与方接收消息的可靠性以及幂等性
即消息是可靠可以接收到,并且由于MQ重试机制必须从业务上保证幂等。
3.基于本地消息表解决问题1,2方式:
1. 基于本地消息表的方式,通过消息表使得业务与消息表操作在一个事务内,保证了原子性。
2. 通过定时任务,将"发送中"的消息进行重复投递,保证了消息一定发送到消息队列。
3. 虽然通过本地消息表数据记录状态,可直接判断消息是否由消费方消费成功,以及接口幂等性保证重复消费消息业务正常,但是依然可通过MQ ACK确认机制,由消费方返回ACK 消费完成,避免不必要的消息投递。
4.基于RocketMQ解决问题1,2的方式:
Producer-Consumer
1. 业务生产者(Producer)发送消息到->MQ,此时消息为Prepared(预备状态);
2. MQ 接收到消息,回应Producer一个ACK表示已收到消息。
3. Producer开启本地事务执行业务逻辑。事务执行成功,则发送commit消息,执行失败则发送rollback消息到MQ。
4. MQ 根据commit/rollback来处理消息,rollback则删除消息。commit则将消息状态标识为(可发送).
5. 消息订阅者(Consumer)消费commit的消息,然后执行本地事务,成功则返回ACK到MQ中。否则将重复收到消息。
注意:
1. 同样的,由消息是可重复投递的,需要考虑接口幂等性。
2. MQ由于内存不够或重启导致消息丢失,可考虑支持磁盘化MQ结合Redies等其他中间件来实现。
3. MQ压力过大,这个基本采用分布式MQ以及冷热数据MQ方式进行处理。
4. 容错,考虑在极端情况下,MQ挂掉,数据虽然有持久化,但业务受影响不能立即重启,可考虑结合Zookeeper、Consul等临时B计划进行处理。
5. 业务接口层面实现熔断以及降级。
5.基于RabbitMQ MassTransit实现分布式事务
具体实现参考:https://www.cnblogs.com/edisonchou/p/dnc_microservice_masstransit_foundation_part1.html