### MVCC
MVCC:Multi-Version Concurrency Control,即多版本并发控制,为每一次事务生成一个新版本的数据,一个数据维护多个版本,使读写操作没有冲突。
### 当前读和快照读
- 当前读:
共享锁(lock in share mode)、排它锁(for update,insert,delete)操作都是当前读。这些操作读取的记录是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
- 快照读
不加锁的select操作就是快照读。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。
### MVCC的实现
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的```3个隐式字段```,```undoLog日志```,```Read View```来实现的。
#### 隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的```DB_TRX_ID```,```DB_ROLL_PTR```,```DB_ROW_ID```等字段
- DB_TRX_ID
6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR
7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
- DB_ROW_ID
6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
还有一个删除的flag字段,用来判断该行记录是否已经被删除(修改和删除后原数据都会标记为删除)。
#### undoLog日志
undoLog是指事务的回滚日志,分为两类。
- insert undo log
事务在insert新记录时产生的undo log,当事务提交之后会被删除。
- update undo log
事务在update/delete时产生的undo log,在事务回滚和快照读时需要他,只有当没有比这个log更早的read-view的时候才能删除。
> purge
> 为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
> 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链,它的执行流程如下:
一、 比如一个有个事务插入```persion```表插入了一条新记录,记录如下,```name```为Jerry,```age```为24岁,```隐式主键```是1,事务ID和回滚指针,我们假设为NULL

二、 现在来了一个```事务1```对该记录的```name```做出了修改,改为Tom
- 在```事务1```修改该行(记录)数据时,数据库会先对该行加```排他锁```
- 然后把该行数据拷贝到```undo log```中,作为旧记录,即在```undo log```中有当前行的拷贝副本
- 拷贝完毕后,修改该行```name```为Tom,并且修改隐藏字段的事务ID为当前```事务1```的ID,我们默认从1开始,之后递增,```回滚指针```指向拷贝到```undo log```的副本记录,既表示我的上一个版本就是它
- 事务提交后,释放锁

三、 又来了个```事务2```修改```person```表的同一个记录,将```age```修改为30岁
- 在```事务2```修改该行数据时,数据库也先为该行加```排他锁```
- 然后把该行数据拷贝到```undo log```中,作为旧记录,发现该行记录已经有```undo log```了,那么最新的旧数据作为链表的表头,插在该行记录的```undo log```最前面
- 修改该行```age```为30岁,并且修改隐藏字段的```事务ID```为当前```事务2```的ID,那就是2,回滚指针指向刚刚拷贝到```undo log```的副本记录
- 事务提交,释放锁

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录(当然就像之前说的该undo log的节点可能是会purge线程清除掉,向图中的第一条insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里)
#### Read View(读视图)
Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID,这个ID是递增的,所以最新的事务,ID值越大)
所以Read View主要是用来做可见性判断的,即当某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View遵循一个可见性算法,将获取到的行(第一次获取到的是最新的记录)的事务ID取出,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID,那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本。

如上,它是一段MySQL判断可见性的一段源码,即```changes_visible```方法,该方法展示了拿DB_TRX_ID去跟Read View某些属性进行怎么样的比较
在展示之前,先简化一下Read View,可以把Read View简单的理解成有三个全局属性
- trx_list:一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID
- up_limit_id:记录trx_list列表中事务ID最小的ID
- low_limit_id:Read View生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值 + 1
过程:
1. 首先比较DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断。
2. 接下来判断 DB_TRX_ID >= low_limit_id ,如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断。
3. 判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了,你修改的结果,我当前事务是能看见的。

#### 整体流程
| 事务1 | 事务2 | 事务3 | 事务4 |
| :------: | :------: | :------: | :----------: |
| 事务开始 | 事务开始 | 事务开始 | 事务开始 |
| ... | ... | ... | 修改且已提交 |
| 进行中 | 快照读 | 进行中 | ... |
- 当事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务ID为2,此时还有事务1和事务3在活跃中,事务4在事务2快照读前一刻提交更新了,所以Read View记录了系统当前活跃事务1、3的ID,维护在一个列表上,假设我们称为trx_list。
- Read View不仅仅会通过一个列表trx_list来维护事务2执行快照读那刻系统正活跃的事务ID,还会有两个属性up_limit_id(记录trx_list列表中事务ID最小的ID),low_limit_id(记录trx_list列表中事务ID最大的ID,也有人说快照读那刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值+1) ;所以在这里例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5,trx_list集合的值是1,3,Read View如下图

- 我们的例子中,只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以当前该行当前数据的undo log如下图所示;我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list)进行比较,判断当前事务2能看到该记录的版本是哪个。

- 所以先拿该记录DB_TRX_ID字段记录的事务ID 4去跟Read View的的up_limit_id比较,看4是否小于up_limit_id(1),所以不符合条件,继续判断 4 是否大于等于 low_limit_id(5),也不符合条件,最后判断4是否处于trx_list中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件,所以事务4修改后提交的最新结果对事务2快照读时是可见的,所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本。

- 也正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同,在RC隔离级别下,每个快照读都会生成并获取最新的Read View,而在RR隔离级别下,同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View。
#### 解决幻读
在RR的隔离级别下,通过MVCC和间隙锁解决了部分幻读问题。

| 事务1 | 事务2 |
| :------: | :------: |
| begin;| begin; |
| select * from `order`; (19条记录) | - |
| - | INSERT INTO `order` ( user_id, type, price, `status` ) VALUE ( 1, 1, 1, 1 ); |
| select * from `order`; (19条记录) | - |
| select * from `order` lock in share mode; (阻塞) | - |
| - | COMMIT; |
| select * from `order` lock in share mode; (20条记录) | - |
| select * from `order`; (19条记录) | - |
这里通过MVCC解决了幻读问题,RR级别下同一个事务中第一个快照读会创建read view,后面的快照读都是同一个read view,但是当前读还是会触发幻读问题。
| 事务1 | 事务2 |
| :------: | :------: |
| begin;| begin; |
| select * from `order` where id > 107200 and id < 207210; (12条记录) | - |
| select * from `order` where id > 107200 and id < 207210 lock in share mode; (12条记录) | - |
| - | INSERT INTO `order` (id, user_id, type, price, `status` ) VALUE (1, 1, 1, 1, 1 ); (成功)|
| select * from `order` where id > 107200 and id < 207210; (12条记录) | - |
| select * from `order` where id > 107200 and id < 207210 lock in share mode; (12条记录) | - |
| - | INSERT INTO `order` (user_id, type, price, `status` ) VALUE (1, 1, 1, 1 ); (阻塞)|
| select * from `order` where id > 107200 and id < 207210 lock in share mode; (12条记录) | - |
| COMMIT; | - |
| - | COMMIT; |
这里通过间隙锁解决了幻读问题,因为间隙范围内(左开右闭)加了排它锁,导致无法插入,但是间隙锁外无法解决。
参考(复制):
1、https://blog.csdn.net/SnailMann/article/details/94724197
2、https://juejin.im/post/6844903969815265288#comment

mysql的MVCC(多版本并发控制)以及解决幻读