写点什么

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

  • 2023-04-03
    湖南
  • 本文字数:2288 字

    阅读完需:约 8 分钟

MVCC(多版本并发控制)

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

  • 读读:不需要并发控制,因为没有改变任何值。

  • 读写:有并发安全问题,比如幻读、脏读、不可重复读。

  • 写写:有并发安全问题,可能存在数据丢失的情况。


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


  • 当前读

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


  • 快照读

读取历史版本的数据。


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


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


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

结合案例说明

对这张表进行操作

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

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

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

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


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

MVCC 原理介绍

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


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


第一部分叫隐藏字段

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

  • DB_TRX_ID 创建这条记录或最后一次修改该记录的事务 id,在事务操作的时候事务 id 的值是递增的。

  • DB_ROLL_PTR 回滚指针,指向上一个数据的版本。

  • DB_ROW_ID 隐藏主键,如果没有显式主键的话,就会多一个隐藏主键。


插入一条数据

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


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


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


第二部分是 undolog


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


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


事务 2 将 name 更新为 lisi。

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

再将 age 修改为 21

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


那 undolog 不是会一直变大吗?


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


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


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

  • trx_list 表示 readview 生成时刻当前系统活跃的事务 id 列表;

  • up_limit_id 表示活跃列表中最小的事务 id 值;

  • low_limit_id 系统尚未分配的下一个事务的 id 值。


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


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

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


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


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

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


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


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


可见性算法的判断过程

根据可见性算法进行判断,首先比较 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 数据写完整

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


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


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

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


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


绿色部分的值跟

这里的 readview 是一样的。


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


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


读取时刻是不同的。


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


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

最终状态的 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;


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


作者:平凡人笔记

链接:https://juejin.cn/post/7216968724938965049

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
MySQL多版本并发控制MVCC实现原理_Java_做梦都在改BUG_InfoQ写作社区