MySQL多版本并发控制MVCC实现原理

承接上文MySQL事务特性ACID实现原理

MVCC(多版本并发控制)

数据库中的并发大概分几种情况:

  • 读读:不需要并发控制,因为没有改变任何值。
  • 读写:有并发安全问题,比如幻读、脏读、不可重复读。
  • 写写:有并发安全问题,可能存在数据丢失的情况。

解决并发安全最普通的方式,可以使用加锁实现,但是效率低。

  • 当前读

在数据进行读取的时候,读取的都是最新版本的数据,并且还要保证其他并发事务不能修改当前数据,需要对读取的记录进行加锁操作。

  • 快照读

读取历史版本的数据。

什么样的操作会触发当前读或快照读?

select lock in share mode加读锁、select for update加写锁、update、delete、insert这些都会触发当前读。

select有可能触发快照读,读到的不是最新数据,而是旧的数据。

结合案例说明

image.png

对这张表进行操作,

image.png

先关闭自动提交;同时开启2个事务;2边查询到的数据是一致的。

image.png

事务2做了更新操作并提交,事务1读取到的结果是更新之后的,即可以读到最新的数据。

image.png

(纠错:上图heheda修改为lian)事务2再进行一次更新操作,

image.png

此时事务1查询到的结果却不是事务2更新之后的,即没有读到最新的数据。

这里涉及到的就是当前读和快照读的问题,跟缓存没有关系,这也是MVCC存在的重要原理。

MVCC原理介绍

image.png

MySQL默认的隔离级别是可重复读。

MVCC在底层实现的时候,包含了三部分操作:

第一部分叫隐藏字段

MVCC在实际操作的时候,除了我们看到的资源之外,还会包含一些看不到的字段,重点说3个:

  • DB_TRX_ID 创建这条记录或最后一次修改该记录的事务id,在事务操作的时候事务id的值是递增的。
  • DB_ROLL_PTR 回滚指针,指向上一个数据的版本。
  • DB_ROW_ID 隐藏主键,如果没有显式主键的话,就会多一个隐藏主键。

插入一条数据,

image.png

这是第一条数据,没有历史版本,所以DB_ROLL_PTR为NULL。

这个表没有主键,所以会给一个默认的值,有主键的话就会显示对应的主键。

这就是刚开始的数据状态。

第二部分是undolog

回滚日志,记录的是数据的历史版本。

DB_ROLL_PTR指向的历史版本就在undolog中。

事务2将name更新为lisi。

image.png

最后一次修改是事务2,回滚指针指向上一个版本。

再将age修改为21,

image.png

undolog会形成一个链表,链首是最新的旧记录,链尾是旧的旧记录。

那undolog不是会一直变大吗?

一个数据不可能一直无限膨胀,有一个后台的purge线程,会清除没用的数据。

第三部分才是最主要:readview(读视图)

即事务进行快照读操作的时候产生的读视图,在当前的读视图中包含3个关键字段:

  • trx_list表示readview生成时刻当前系统活跃的事务id列表;
  • up_limit_id表示活跃列表中最小的事务id值;
  • low_limit_id系统尚未分配的下一个事务的id值。

当生成readview的时候,会把这些字段值进行填充。

当填充完成之后,再根据可见性算法来判断是否可以读取到对应的数据结果。

image.png

t3时刻事务1进行select操作的时候,能否读取事务2 t2时刻修改之后的结果?

在进行快照读操作的时候,会产生读视图,所谓的快照读就是select操作。

快照读的时候,填充好对应的字段信息,

image.png

当前系统活跃的事务是1和3,事务2已经提交了。

当前活跃列表中最小的事务id是1,系统尚未分配的下一个事务id是4。

DB_TRX_ID 创建这条记录或者最后一次修改该记录的事务id是2,因为事务2修改的,所以DB_TRX_ID为2。

可见性算法的判断过程

image.png

根据可见性算法进行判断,首先比较DB_TRX_ID(2)大于up_limit_id(活跃列表中最小的事务id值1),进入下一个判断,如果小于则当前事务能看到所在的记录。

如果大于等于当前出现过的最大的事务id(4)则表示DB_TRX_ID=2所在的记录在read view生成后出现的,那么对于当前事务肯定不可见,如果小于则进入下一个判断。

DB_TRX_ID=2是否在活跃事务(1,3)中,如果不在,说明在readview生成之前就已经commit了,那么修改的结果是能够看到的。

所以t3时刻进行select操作的时候,能读取t2时刻修改之后的结果。

每次在进行快照读的时候,会生成readview。

若t2、t4时刻共有2次快照读,来把对应的readview数据写完整,

image.png

t2时刻当前系统活跃列表中的id是1、2、3,同一个列表中最小的id是1,尚未分配的下一个事务id是4。

新增或最近修改这条记录的事务id是多少?

因为没有新增操作,但因为事务id的值是递增的,一定是小于1的,假设为0,反正小于1就行了。

image.png

t4时刻对应的read view:事务2提交了,最小事务id值为1,尚未分配的下一个事务id是4。

t3时刻事务2提交了事务,所以t4时刻最后修改的事务id是2。

绿色部分的值跟image.png

这里的readview是一样的。

readview数据值是一样的,可见性算法是一样的,但是结果却是不同的(即事务1在t3时刻可以读取到最新的数据,在t4时刻却读不到最新数据),

所以要考虑下,在整个过程里面,哪里可能会发生变化?

读取时刻是不同的。

根据现象进行大胆猜测:第二次readview并没有重新生成,而是用的之前的readview。

接下来验证猜测是否正确:

image.png最终状态的readview根据可见性算法判断,DB_TRX_ID=2 大于 up_limit_id=1,进入下一个判断,DB_TRX_ID=2 小于 low_limit_id=4,进入下一个判断,验证DB_TRX_ID=2是否在活跃事务中(此时在的),如果在,则代表在readview生成的时刻,这个事物还是活跃状态,还没有commit,当前事务中是看不到修改的数据。

验证完可见性算法之后,跟上述的结论是可以对上的,所以在第二次快照读的时候,确实是用的第一次生成的readview,没有重新生成新的。

小结

在RC隔离级别里,每次进行快照读操作的时候 ,都会重新生成新的readview,所以每次可以查询的最新的结果集的记录;

在RR隔离级别里,只有当前事务在第一次进行快照读的时候才会生成readview,之后进行的快照读操作都会沿用之前的readview;

所以这也是为什么在不同的隔离级别里面看到的效果是不一样的原因。

猜你喜欢

转载自juejin.im/post/7216968724938965049