MVCC原理

MVCC原理

创建一个表

CREATE TABLE hero(
	number INT,
    name VARCHAR(100),
    country VARCHAR(100),
    PRIMARY KEY (number)
)Engine=InnoDB CHARSET=utf8;

向这个表插入一条记录

insert into hero VALUES(1,'刘备','蜀');

版本链

在使用innodb存储引擎的表时,它的聚集索引记录中都包含下面两个必要的隐藏列(row_id并不是必要的;在创建的表中有主键时,或者有不允许为NULLUNIQUE键时,都不会包含row_id列)。

  • trx_id:一个事务每次对某条聚集索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列
  • roll_pointer:每次对某条聚集索引记录进行改动时,都会把旧的版本写入到undo日志中。这个隐藏列就相当于一个指针,可以通过它找到该记录修改前的信息。

比如,表hero中现有只有一条记录

SELECT * FROM hero;
numbernamecountry
1刘备

假设插入该记录的事务id为80,那么此刻该条记录的示意图

在这里插入图片描述

实际上insert undo只在事务回滚时发生作用。当事务提交后,该类型的undo日志就没有用了。

假设之后两个事务id分别为100,200的事务对这条记录进行UPDATE操作,操作流程如图所示

在这里插入图片描述
每对记录进行一次改动,都会记录一条undo日志。每条undo日志也都有一个roll_pointer属性。(INSERT操作对应的undo日志没有该属性,因为INSERT操作的记录并没有更早的版本)。通过这个属性可以将这些undo日志串成一个链表。

在这里插入图片描述
在每次更新该记录后,都会将旧值放到一条undo日志中(就算是该记录的一个旧版本)。随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,这个链表称为版本链

版本链的头节点就是当前记录的最新值。另外,每个版本中还包含生成该版本时对应的事务id。

我们会利用这个记录的版本链来控制并发访问相同记录时的行为,这种机制称之为多版本并发控制(MVCC)

ReadView

对于使用READ UNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了;对于使用SERIALIZABLE隔离级别的事务来说,innodb规定使用加锁的方式来访问记录;对于使用READ COMMITTEDREPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交的事务修改过的记录。

innodb提出了ReadView(一致性视图)的概念。这个ReadView包含四个比较重要的内容。

  • m_ids:在生成ReadView时,当前系统中活跃的读写事务的事务id列表
  • min_trx_id:在生成ReadView时,当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值
  • max_trx_id:在生成ReadView时,系统应该分配给下一个事务的事务id
    • max_trx_id并不是m_ids中的最大值。事务id是递增分配的。比如现在有事务id分别为1,2,3的这3个事务,之后事务id为3的事务提交了,那么一个新的读事务在生成ReadView时,m_ids就包括1,2,min_trx_id的值就是1,max_trx_id的值就是4
  • creator_trx_id:生成该ReadView的事务的事务id

只有对表中的记录进行改动时(执行INSERT,DELETE,UPDATE)才会为事务分配唯一的事务id,否则一个事务的事务id值默认为0

有了这个ReadView后,在访问某条记录时,只需要按照下面的步骤来判断记录的某个版本是否可见。

  • 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问
  • 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问
  • 如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开始,所以该版本不能被当前事务访问
  • 如果被访问版本的trx_id属性值在ReadViewmin_trx_idmax_trx_id之间,则需要判断trx_id属性值是否在m_ids列表中。如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,并继续上面的步骤来判断记录的可见性;以此类推,直到版本链的最后一个版本。如果记录的最后一个版本也不可见,就意味着该条记录对当前事务完全不可见,查询结果就不包含该记录。

在mysql中,READ COMMITTED与REPEATABLE READ隔离级别之间一个非常大的区别就是它们生成ReadView的时机不同

READ COMMITTED——每次读取数据前生成一个ReadView

比如,现在系统有两个事务id分别为100,200的事务正在执行

#Transaction 100#Transaction 200
BEGIN;BEGIN;
UPDATE hero SET name=‘关羽’ WHERE number=1;#更新一些别的表记录
UPDATE hero SET name=‘张飞’ WHERE number=1;

此时,表hero中number为1的记录对应的版本链如图所示

在这里插入图片描述

假设现在有一个使用READ COMMITTED隔离级别的新事物开始执行(不是事务id为100,200的那两个事务)

# 使用READ COMMITTED隔离级别的事务
BEGIN;

#SELECT1:TRANSACTION 100,200未提交
SELECT * FROM hero WHERE number=1;	#得到的列name的值为刘备

这个SELECT1的执行过程如下。

  1. 步骤一,在执行SELECT语句时先生成一个ReadViewReadViewm_ids列表的内容就是[100,200],min_trx_id为100,max_trx_id为201,creator_trx_id为0。
    • 这个新开启的事务并没有对任何记录进行任何改动,所以系统并不会为它分配唯一的事务id,它的事务id默认是0,所以creator_trx_id为0。
  2. 步骤2,然后从版本链中挑选可见的记录,从图中可以看出,最新版本的name列的内容是“张飞”,该版本的trx_id是100,在m_ids列表中,因此不符合可见性要求,根据roll_pointer跳到下一个版本。
  3. 步骤3,下一个版本的name列是“关羽”,该版本的trx_id值也是100,也在m_ids列表内,因此也不符合要求;继续条道下一个版本。
  4. 步骤4,下一个版本的name列是“刘备”,该版本的trx_id值是80,小于ReadVIew中的min_trx_id值100,所以这个版本是符合要求的;最后返回给用户的版本就是这条name列为“刘备”的记录

之后,我们把事务id为100的事务进行提交,如下所示

#Transaction 100
UPDATE hero SET name='关羽' WHERE number=1;
UPDATE hero SET name='张飞' WHERE number=1;
COMMIT;

然后再到事务id为200的事务中更新表hero中number为1的记录

#Transaction 200
BEGIN;
#更新了一些别的表的记录
...

UPDATE hero SET name='赵云' WHERE number=1;
UPDATE hero SET name='诸葛亮' WHERE number=1;
#未提交

在这里插入图片描述
然后再到刚才使用READ COMMITTED隔离级别的事务中执行SELECT2,继续查找这个number为1的记录,如下:

#使用READ COMMITTED隔离级别的事务
BEGIN;

#SELECT1:Transaction 100,200均未提交
SELECT * FROM hero WHERE number=1;	#得到的列name的值为‘刘备’

#SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number=1;	#得到的列name的值为'张飞'

这个SELECT2的执行过程如下

  1. 步骤1,在执行SELECT语句时又会单独生成一个ReadView。该ReadViewm_ids列表的内容就是[200](事务id为100的那个事务已经提交了,所以再次生成ReadView时就没有它了),min_trx_id为200,max_trx_id为201,creator_trx_id为0
  2. 步骤2,从版本链中挑选可见的记录,从图中可以看出,最新版本的name列的内容时‘诸葛亮’,该版本的trx_id值为200,在m_ids列表中,因此不符合可见性要求;根据roll_pointer跳到下一个版本
  3. 步骤3,下一个版本的name列的内容是“赵云”,该版本的trx_id值为200,也在m_ids中;继续跳到下一个版本
  4. 步骤4,下一个版本的name列是“张飞”,该版本的trx_id值为100,小于ReadView中的min_trx_id值200,所以这个版本是符合要求的;最后返回给用户的版本就是这条name列为“张飞”的记录

总结一下就是,使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView

REPEATABLE READ——在第一次读取数据时生成一个ReadView

对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成ReadView

比如,现在系统中有两个事务id分别为100,200的事务正在执行:

#Transaction 100#Transaction 200
BEGIN;BEGIN;
UPDATE hero SET name=‘关羽’ WHERE number=1;#更新一些别的表记录
UPDATE hero SET name=‘张飞’ WHERE number=1;

此时,表hero中的number为1的记录的版本链如图所示

在这里插入图片描述

假设现在有一个使用REPEATABLE READ隔离级别的新事务开始执行

# 使用READ COMMITTED隔离级别的事务
BEGIN;

#SELECT1:TRANSACTION 100,200未提交
SELECT * FROM hero WHERE number=1;	#得到的列name的值为刘备

这个SELECT1的执行过程如下。

  1. 步骤一,在执行SELECT语句时先生成一个ReadViewReadViewm_ids列表的内容就是[100,200],min_trx_id为100,max_trx_id为201,creator_trx_id为0。
    • 这个新开启的事务并没有对任何记录进行任何改动,所以系统并不会为它分配唯一的事务id,它的事务id默认是0,所以creator_trx_id为0。
  2. 步骤2,然后从版本链中挑选可见的记录,从图中可以看出,最新版本的name列的内容是“张飞”,该版本的trx_id是100,在m_ids列表中,因此不符合可见性要求,根据roll_pointer跳到下一个版本。
  3. 步骤3,下一个版本的name列是“关羽”,该版本的trx_id值也是100,也在m_ids列表内,因此也不符合要求;继续条道下一个版本。
  4. 步骤4,下一个版本的name列是“刘备”,该版本的trx_id值是80,小于ReadVIew中的min_trx_id值100,所以这个版本是符合要求的;最后返回给用户的版本就是这条name列为“刘备”的记录

之后,我们把事务id为100的事务进行提交,如下所示

#Transaction 100
UPDATE hero SET name='关羽' WHERE number=1;
UPDATE hero SET name='张飞' WHERE number=1;
COMMIT;

然后再到事务id为200的事务中更新表hero中number为1的记录

#Transaction 200
BEGIN;
#更新了一些别的表的记录
...

UPDATE hero SET name='赵云' WHERE number=1;
UPDATE hero SET name='诸葛亮' WHERE number=1;
#未提交

此时,表hero中number为1的记录的版本连如图所示

在这里插入图片描述
然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个number为1的记录,如下:

#使用READ COMMITTED隔离级别的事务
BEGIN;

#SELECT1:Transaction 100,200均未提交
SELECT * FROM hero WHERE number=1;	#得到的列name的值为‘刘备’

#SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number=1;	#得到的列name的值为'刘备'

这个SELECT2的执行过程如下。

  1. 步骤1,因为当前事务的隔离级别为REPEATABLE READ,而之前在执行SELECT1已经生成过ReadView了,所以此时直接复用之前的ReadView。之前的ReadViewm_ids列表的内容就是[100,200],min_trx_id为100,max_trx_id为201,creator_trx_id为0
  2. 步骤2,然后从版本链中挑选可见的记录。从图中可以看出,最新版本的name列内容是“诸葛亮”,该版本的trx_id的值为200,在m_ids列表中,因此不符合可见性要求,根据roll_pointer跳到下一个版本中
  3. 步骤3,下一个版本的name列的内容是“赵云”,该版本的trx_id值为200,也在m_ids列表内,因此也不符合要求;继续跳到下一个版本
  4. 步骤4,下一个版本的name列的内容是“张飞”,该版本的trx_id值为100,而m_ids列表中包含值为100的事务id,所以这个版本也不符合要求。同理,下一个name列的内容是“关羽”的版本也不符合要求;继续跳到下一个版本
  5. 步骤5,下一个版本的name列的内容是“刘备”,该版本的trx_id值为80,小于ReadView中的min_trx_id值100,所以这个版本是符合要求的;最后返回给用户的版本就是name列为“刘备”的记录

也就是说,在REPEATABLE READ隔离级别下,事务的两次查询得到的结果是一样的,记录的name列值都是“刘备”。这就是可重复读的含义

二级索引与MVCC

只有在聚集索引中才有trx_idroll_pointer隐藏列。如果某个查询语句使用二级索引来执行查询,该如何判断可见性。

比如下面这个事务。

BEGIN;

SELECT name FROM hero WHERE name='刘备';

假设查询优化器决定先到二级索引idx_name中定位name值为“刘备”的二级索引记录,那么怎么知道这条二级索引记录对这个查询事务是否可见呢?

判断可见性的过程大致分为下面两步

  • 二级索引页面的Page header部分有一个名为PAGE_MAX_TRX_ID的属性,每当对该页面中的记录执行增删改操作时,如果执行该操作的事务的事务id大于PAGE_MAX_TRX_ID属性值,就会把PAGE_MAX_TRX_ID属性设置为执行该操作的事务的事务id。这也就意味着PAGE_MAX_TRX_ID属性值代表着修改该二级索引页面的最大事务id是什么。当SELECT语句访问某个二级索引记录时,首先会看一下对应的ReadViewmin_trx_id是否大于该页面的PAGE_MAX_TRX_ID属性值,如果是,说明该页面中的所有记录都对该ReadView可见,否则就得执行步骤2,在回表之后再判断可见性
  • 利用二级索引记录中的主键值进行回表操作,得到对应的聚集索引记录后再按照前面讲过的方式找到对该ReadView可见的第一个版本,然后判断该版本中相应的二级索引列的值是否与利用该二级索引查询时的值相同。本例中就是判断找到的第一个可见版本的name值是不是“刘备”。如果是,就把这条记录发送到客户端(如果WHERE子句中还有其他搜索条件的话还需继续判断),否则就跳过该记录

MVCC小结

所谓的MVCC指的就是在使用READ COMMITTED,REPEATABLE READ这两种隔离级别的事务执行普通的SELECT操作时,访问记录的版本链的过程。 这样可以使不同事务的读-写,写-读操作并发执行,从而提升系统性能。READ COMMITTED,REPEATABLE READ这两个隔离级别有一个很大的不同,就是生成ReadView的实时机不同:READ COMMITTED在每一次进行普通SELECT操作前都会生成一个ReadView;而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView

只有我们进行普通的SELECT查询时,MVCC才生效。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值