数据库MVCC总结

1,330 阅读4分钟

为什么需要MVCC

当多个数据库事务同时运行,其修改数据产生的中间状态对于其他事务的可见性是数据库隔离级别来限制的,而这个可见性的约束有四种

  • 读未提交
  • 读已提交
  • 可重复读
  • 串行化 其中读未提交最弱,多个事务共享一份数据就好了,而串行化最强,读取加读锁,写数据加写锁,而读已提交和可重复读都需要有“不读取未提交的数据”和“只读取创建事务之前的数据”这样的需求,而实现这样的需求,自然而然的想到要引入“快照”的概念,也就是对数据库的每一行,并发修改的时候生成快照,在MySQL,快照的实现借助的是MVCC,即多版本并发控制,下面将主要围绕快照如何组织,以及如何使用快照两个部分对MVCC进行描述。

快照如何组织?

MySQL的快照依赖的是版本链,也就是UndoLog,UndoLog在每次修改数据的时候,保留数据最初始的版本,同时记录最新版,既然是链,就有指针,这个指针在每行数据的隐藏字段当中,同时,每次修改的快照自然需要记录是谁修改的,这个谁就是事务,事务有自己的唯一,自增的事务Id,唯一是标识事务的必要条件,而自增,则给了事务Id顺序性的含义,顺序的ID对于之后使用快照的阶段非常重要,当然事务Id和undo指针一样都在每行数据的隐藏字段当中。

image.png 其中trx_id就是事务Id,roll_p就是指针

如果有一条记录为A,经过了如下的操作

事务2事务3
begin;
将A修改为Bbegin;
将B修改为C(时刻1)
commit;
commit;

那么在时刻1的版本链如下所示:

image.png

现在版本链已经有了,但是如何使用,只靠一个版本链是缺乏足够的信息来实现读已提交和可重复读的,我们还缺乏在事务创建的一些额外的信息,就好比,读已提交需要知道什么是已提交,进一步,哪些事务是已提交的,等等,这些信息构成了使用MVCC的运行时上下文,下面我们总结下MVCC使用的上下文信息。

快照如何使用?

创建快照的时候也需要记录一些创建时刻的基本信息,比如创建事务的时刻,有哪些事务正在运行,这些数据方便界定哪些事务是已经提交了的,我可以读了,也需要当前事务的事务Id,也需要在创建快照的那一刹那,标识对于当前的事务而言,哪些事务对于当前的事务是不可见的未来。对于这些数据,如下图所示:

image.png

其中m_ids表示创建事务的那一刻,当前运行中的事务,creator_trx_id表示创建者事务,min_trx_idm_ids的最小值,小于min_trx_id的事务Id对当前事务而言是妥妥的已提交,而max_trx_id是当前事务Id的下一个Id。

快照如何工作,主要依赖以上描述的信息搭配undoLog链:

  • 如果访问undolog版本的trx_id属性值creator_trx_id相同,则是自己的记录,可见
  • 如果trx_id小于min_trx_id则代表该记录已经提交,可见
  • 如果trx_id大于等于max_trx_id则是创建该视图之后的事务,不可见
  • 如果trx_id处于min_trx_idmax_trx_id之间,则需要判断此时此刻trx_id是否在m_ids之间,如果依然在,则不可见。

读已提交和可重复读的区别

上面描述的基本信息包含在一次读视图中(ReadView)

  • 对于读提交,是每次读操作都会创建新的视图,对于这种情况一个事务中读视图中的数据是不断变化的,因此每次能读取到最新的提交
  • 对于可重复读,是每次事务读的时候创建一个视图,并且一直沿用。

版本链何时清除

  • 后台运行Purge线程在何时的时候进行删除

注意事项

由于MVCC版本链的机制,可见为了支持MVCC,更新的数据可以和老数据同时留存,删除的数据也不会立即删除,而是打上删除标记,因此一定要注意长事务对于MySQL本身的影响

在开发过程中,可以设置set autocommit=1

查询长事务可以通过如下的语句来查看:

select
    * 
from information_schema.innodb_trx
where 
TIME_TO_SEC(timediff(now(),trx_started))>60

参考资料

掘金小册 极客时间