这里介绍InnoDB存储引擎中如何通过MVCC 多版本并发控制实现不同隔离级别下的查询
基本原理
版本链
在InnoDB存储引擎下,对于聚簇索引中的记录而言,其会含有两个必要的隐藏列:trx_id、roll_pointer。一方面,当事务对聚簇索引中的记录进行改动时,即会把该事务ID赋值给trx_id列;另一方面,每次对聚簇索引进行改动后,其会将该记录旧版本写入到undo日志当中。故roll_pointer列本质上就是一个指针,用于找到该聚簇索引改动前的版本。这样每次对记录进行改动即会形成一条相应的undo日志记录,同样地,在每条undo日志记录也会包含生成该版本时对应的事务ID信息。同时对于每个undo日志记录也有一个roll_pointer属性。使得undo日志记录之间可以形成一个链表,如下所示。可以看到,对于一条记录的多个历史版本形成了一个版本链
ReadView
前面已经看到通过版本链可以记录下各事务对记录的修改结果。而为了实现在不同隔离级别条件下,能够在版本链中找到合适的版本以对当前事务可见。InnoDB引入了ReadView的概念,其包含以下重要信息
- m_ids:在生成ReadView时,当前数据库中活跃的读写事务的事务ID列表
- min_trx_id:在生成ReadView时,当前活跃的读写事务中最小的事务ID,也就是m_ids中的最小值
- max_trx_id:在生成ReadView时,系统中下一次给其他事务分配所使用的ID。这里对事务ID进行补充说明,一方面,事务ID的分配保证全局递增;另一方面,只有在对记录进行修改时(执行INSERT、DELETE、UPDATE语句)才会为该事务分配事务ID。否则对于一个只读事务而言,其事务ID使用默认值0
- creator_trx_id:生成该ReadView的事务所对应的事务ID
至此,我们就可以利用当前事务所生成的ReadView来判断版本链中某个版本的记录,是否对当前事务可见。具体地规则如下:
- 如果被访问版本的trx_id属性值 等于 ReadView中的creator_trx_id值。意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问
- 如果被访问版本的trx_id属性值 小于 ReadView中的min_trx_id值。表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问
- 如果被访问版本的trx_id属性值 大于等于 ReadView中的max_trx_id值。表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问
- 如果被访问版本的trx_id属性值 在ReadView的min_trx_id和max_trx_id之间。即 min_trx_id ≤ 被访问版本的trx_id属性值 < max_trx_id。则我们需要进一步判断 被访问版本的trx_id属性值 是不是在 m_ids列表 中。如果是,则说明创建ReadView时,生成该版本的事务还是活跃的,则该版本不可以被访问;反之,则说明创建ReadView时,生成该版本的事务已经被提交,故该版本可以被访问
在查询记录时,通过版本链确定版本的可见性。如果该版本可见则表示版本确定完毕,返回该版本的数据作为查询结果;反之,如果某个版本的数据对当前事务不可见的话,则根据版本链找到下一个版本的数据,重复使用上述的规则进行判断,依此类推。如果版本链中最后一个版本也不可见的话,则说明该条数据记录对该当前事务完全不可见,即查询结果不包含该记录
隔离级别
Read Uncommitted 未提交读
对于隔离级别为 Read Uncommitted 未提交读 的事务而言。由于允许读到其他未提交事务修改过的记录。故直接读取记录的最新版本即可。无需使用MVCC机制
Read Committed 已提交读
对于隔离级别为 Read Committed 已提交读 的事务而言,由于允许读到其他已提交事务修改过的记录。故在该事务当中,其每次执行select查询语句时都会重新创建一个新的ReadView用于进行版本的可见性判断
Repeatable Read 可重复读
对于隔离级别为 Repeatable Read 可重复读 的事务而言,其要求该事务中无论进行多少次查询,对于某条记录而言,其数据内容均不能发生改变。故在该事务当中,其只会在第一次执行select查询语句时创建ReadView。后续该事务再次执行select查询语句时,将不会重新创建ReadView。而是会利用该事务第一次创建的ReadView进行版本可见性的判断
Serializable 串行化
对于隔离级别为 Serializable 串行化 的事务而言。InnoDB规定其读取数据采用加锁的方式进行实现,而不使用MVCC机制
Note
- 事实上,对于trx_id、roll_pointer隐藏列而言,其真实的名称为DB_TRX_ID、DB_ROLL_PTR。本文只是为了行文方便换了个叫法
- 特别地,对于二级索引而言。虽然其没有trx_id隐藏列。但在二级索引页面的Page Header中有一个PAGE_MAX_TRX_ID属性,用于表示对该页面进行改动的最大事务ID。如果PAGE_MAX_TRX_ID属性值 小于 当前活跃的读写事务中最小的事务ID,则说明对该页面做修改的事务都已经提交了,即是可见的;反之,则需要进行回表,在聚簇索引中通过MVCC机制判断可见性
参考文献
- MySQL是怎样运行的:从根儿上理解MySQL 小孩子4919著