原理
隐藏的三个字段
InnoDB的MVCC,是通过在每行纪录后面保存三个隐藏的列来实现的。这三个列,一个保存了行的创建时间,一个保存了行的过期时间(或删除时间),当然存储的并不是实际的时间值,而是系统版本号;一个保存了行的上一个版本地址。 每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行纪录的版本号进行比较。在可重复读隔离级别下,MVCC具体的操作如下:
SELECT
InnoDB会根据以下两个条件检查每行纪录,只有符合上述两个条件的纪录,才能作为查询结果返回。
InnoDB只查找版本早于当前事务版本的数据行,即,行的系统版本号小于或等于事务的系统版本号,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
行的删除版本,要么未定义,要么大于当前事务版本号。这样可以确保事务读取到的行,在事务开始之前未被删除。
INSERT
InnoDB为插入的每一行保存当前系统版本号作为行版本号。
DELETE
InnoDB为删除的每一行保存当前系统版本号作为行删除标识 。
UPDATE
InnoDB插入一行新纪录,保存当前系统版本号作为行版本号;同时,保存当前系统版本号到原来的行作为行删除标识。
优点:
保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好。
缺点:
每行纪录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
ReadView
ReadView(读视图),ReadView中主要就是有个列表来存储我们系统中当前活跃着的事务,也就是还未提交的事务。 通过这个读视图来判断行记录的某个版本是否对当前事务可见,假设当前列表里的事务id为[80,100],具体行记录的可见性如下:
如果你要访问的行记录的版本为50,比当前列表最小的事务id80要小,说明此版本的行记录已经被提交,所以对当前事务来说是可访问的。
如果你要访问的行记录的版本为90,此版本在当前列表最大值和最小值之间,那就再判断一下是否在列表内。如果在,说明此版本的行记录未被提交,所以对当前事务来说是不可访问的;如果不在,说明此版本的行记录已被提交,所以对当前事务来说是可访问的。
如果你要访问的行记录的版本为110,比当前列表最小的事务id100都大,说明这个版本是在ReadView生成之后才发生的,所以对当前事务来说是不可访问的。
如果数据不可见,我们需要去哪里找上个版本的数据呢?
通过行记录的上一个版本地址去undo log信息中寻找上个版本的行记录,按照上面(1)、(2)、(3)的步骤,判断此版本的行记录对当前事务是否可见,以此类推。
具体流程如下:
例子
1.有一行记录,这行记录被3个事务更新过,分别为事务50、事务60、事务100,其中事务50和事务60已经提交,事务100未提交。 如下图所示:
2.一个select事务,要查询id为1的记录,此时生成的ReadView列表只有[100]。 要访问的行记录最近的版本号是100,在ReadView列表中,所以不能访问;这时候就通过上一个版本地址找下一条,找到name值为小明1的那条记录,行版本号是60,小于ReadView列表中的最小值,可以访问,查找到小明1的那条记录。
3.这时候我们把事务id为100的事务提交了,并且新建了一个id为110的事务,也修改这条行记录,并且不提交事务。如下图所示:
4.之前那个select事务又执行了一次查询,要查询id为1的记录。
如果你是已提交读隔离级别,这时候你会重新生成一个ReadView,那你的活动事务列表中的值就变了,变成了[110]。 你查找到就是小明2这条记录。
如果你是可重复读隔离级别,这时候你的ReadView还是第一次select时候生成的ReadView,也就是活动事务列表中的值还是[100]。所以select的结果还是小明1这条记录。第二次select结果和第一次select结果一样,所以叫可重复读!
也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。