MVCC为多版本并发控制,在Mysql中InnoDB使用了MVCC来实现数据库事务的可重读隔离级别。主要功能是在多线程并发去修改数据库某一张表时,会对事务之间进行隔离,让一次事务中不会出现分两次读取一个值却得到两个结果的情况。
MVCC在可重复读的隔离级别下的实现原理如下:
首先数据库会在表中的每一行上生成3个隐藏字段,分别是创建或者最后一次修改该记录的事务id(DB_TRX_ID)、隐藏主键(DB_ROW_ID)、回滚指针(DB_ROLL_PTR)。
同时每个事务开始前,会记录下当前仍在活跃也就是开始但未提交的所有事务,保存在一个数组中,即ReadView,然后会根据这个数组,基于一定的规则判断应该读取每个数据的哪个快照。
ReadView中最小的事务 ID 和最大的事务 ID+1,分别称为低水位和高水位。这两个 ID 其实就可以从当前执行的事务的视角,将所有的事务分为三个部分,小于低水位的部分一定是当前事务开始前就提交了的部分,大于等于高水位的则一定是还未提交的事务,我们一定不可见。处于中间的部分就要分类讨论了:
如果在视图数组中,说明当前事务开始时,这些事务仍在活跃,所以应该是不可见的;
如果不在视图数组中,说明这些事务的id比低水位大,但是它们在当前事务开始前就已经提交了,所以是可见的;
简单总结一下,如果记录低水位为 low_id,高水位为 high_id,活跃事务数组为 trx_list。可见的 trx_id 就需要满足 trx_id < low_id 或者 trx_id < high_id 且 !trx_list.contains(trx_id) 的条件,也就是要么比低水位更早,要么比高水位的 id 小但是不能出现在活跃事物数组中。
每次开启新事务时,将DB_TRX_ID自增,事务读和写的操作如下:
读:
第一次进行读时会获取最后一次对读取数据进行修改的事务id,如果获取的事务id小于当前活跃的事务id的最小值,那么直接获取该版本的数据,如果大于当前活跃的事务id的最小值同时小于活跃id的最大值(进行快照读时刻的最大值,不一定是当前活跃id的最大值),那么也会直接获取该版本的数据,如果大于活跃id的最大值,那么就会查询回滚指针所指向的数据,并重复以上流程,直到能获取数据。后续再次读,也是基于第一次快照读所获取的数据。如果是一致性锁定读(加锁),还会使用Next-key lock对该数据和相邻范围内的数据区间上锁,别的事务无法进行该区间内数据的修改,所以也避免了幻读。
写:
如果是第一次写那么会将数据的DB_TRX_ID修改为当前事务id,同时会使用回滚日志记录修改前数据(undolog:每一次对表的修改操作都会在undolog中进行记录,记录修改前的数据),生成一条undo数据,同时将DB_ROLL_PTR 指向生成undo数据。
ACID如何实现: