一、什么是MVCC
二、相关概念
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读 ,而这个读指的就是快照读 , 而非当前读。当前读实际上是一种加锁的操作。
1.当前读
当前读读取的记录一定是最新的数据,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
加锁的读被称为当前读,还有数据的增删改都是要先读取数据的,这一读取过程也是当前读。
select...lock in share mode (共享读锁)
select...for update
update , delete , insert复制
当前读, 读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。
例如,假设要 update 一条记录,但是另一个事务已经 delete 这条数据并且 commit 了,如果不加锁就会产生冲突。所以 update 的时候肯定要是当前读,得到最新的信息并且锁定相应的记录。
2.快照读
快照读又叫一致性读,读取的是数据行的快照版本。在MySQL中,普通的select语句(不加for update或lock in share mode的select语句)默认就是使用的快照读,不加锁。
SELECT * FROM table WHERE ...
复制
之所以这样,是因为快照读可以避免加锁操作,降低开销。当事务的隔离级别是串行时,快照读就没有用了,会退化为当前读。
三、MVCC 多版本实现
这里我们先来理解下有关MVCC相关的一些概念,这些概念都理解后,我们会通过实际例子来演示MVCC的具体工作流程是怎么样的。
1、事务版本号
事务每次开启时,都会从数据库获得一个自增长的事务ID,可以从事务ID判断事务的执行先后顺序。也就是每当begin的时候,首选要做的就是从数据库获得一个自增长的事务ID,它也就是当前事务的事务ID。
2、隐藏字段
对于InnoDB存储引擎,每一行记录都有三个隐藏列row_id,trx_id、roll_pointer,
row_id:单调递增的行ID,不是必需的,占用6个字节。 这个跟MVCC关系不大
trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
roll_pointer:回滚指针,每次修改数据时,都会把旧数据放入undo log日志中,新的数据指向该旧数据,做成一个版本链,该指针字段就称为回滚指针,通过该指针可以找到修改前的数据。复制
假设 F1~F6 是表中字段的名字,1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID、事务号和回滚指针,如果数据表中存在主键或者非NULL的UNIQUE键时不会创建row_id,否则InnoDB会自动生成单调递增的隐藏主键row_id。
3、undo log
5、readView
读已提交和可重复读都必须保证读到的数据都是其他事务提交了的,所以,其他事务修改了数据但是还未提交,我们不能够访问该数据,但可以通过MVVC机制读取该记录的历史版本,核心问题就是需要判断版本链中的哪条历史版本是当前事务可见的,这也是ReadView要解决的问题。
Read View包含4个比较重要的内容:
trx_ids:当前系统中活跃的事务id集合。提交了的事务不在其中。
creator_trx_id:创建这个Read View的事务id
up_limit_id:活跃的事务中最小的事务id。
low_limit_id:预分配事务ID,当前最大事务ID+1(因为事务ID是自增的)
6、版本链数据访问规则:

在读已提交事务级别,在事务中的每一次执行快照读时就会生成一个ReadView。

第一次快照读:
现在我们分析第一次快照读,查询获取的结果应该是多少?
我们只需要拿着undolog版本链和右侧侧readView规则,从上开始对比就能获取他们应该获取的结果。
当前有2,3,4,5个事务,事务2已经提交,第一次查询生成ReadView的时候,活跃事务(开启还没提交)有(3,4,5),活跃的事务中最小的事务id是3,最大事务id是当前活跃事务id+1是6,创建这个Read View的事务id是5.(我们用黄色标记便于对比)

1、首先拿当前最新记录事务id为4,代入到右侧的规则中。

1️⃣、4==5,排除。
2️⃣、4<3,排除。
3️⃣、4>6,排除,
4️⃣、3<4<6,且4 not in (3,4,5),排除。
2、然后我们从版本链中取事务id为3,代入到右侧的规则中。

1️⃣、3不等于5,排除。
2️⃣、3<3,排除。
3️⃣、3>6,排除,
4️⃣、3<4<6,且4 not in (3,4,5),排除。
3、然后我们从版本链中取事务id为2,代入到右侧的规则中。
1️⃣、2不等于5,排除。
2️⃣、2<3,成立。返回事务2的数据(红框中的数据)

第二次快照读:
由于事务2,3已经提交,,第二次快照读生成ReadView的时候,活跃事务只有(4,5),活跃的事务中最小的事务id是4,最大事务id是当前活跃事务id+1是6,创建这个Read View的事务id是5.(我们用黄色标记便于对比)

1、首先拿当前最新记录事务id为4,代入到右侧的规则中。

1️⃣、 4==5,排除。
2️⃣、4<4,排除。
3️⃣、4>6,排除,
4️⃣、4<4<6,且4 not in (4,5),排除。
2、然后我们从版本链中取事务id为3,代入到右侧的规则中。

1️⃣、 3==5,排除。
2️⃣、3<4,成立。返回本条数据。

在RR(可重复度)事务级别,我们看到在事务5中有两次快照读,不同于RC(读已提交)事务级别,他只是在第一次执行快照读的时候生成了一次ReadView,后续复用该ReadView.这也就是RR能解决不可重复读的底层原理。

---THE END---
