持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
MySQL 事务隔离的实现依赖于多版本并发控制(MVCC) 和 锁来实现的。
多版本并发控制(MVCC)
通过在每行记录后面保存两个隐藏的列来实现。一个是行的创建时间、一个是行的过期时间(或删除时间)。存储的并不是实际的时间,而是系统版本号。每新增一个事务,版本号就会自动递增。
在REPETABLE READ 隔离级别下,MVCC具体是如何操作的。
- SELECT
Innodb 会根据以下两个条件来检查每行记录:
- InnoDB 只查询版本号小于或等于当前事务版本号的数据行,这样可以确保事务读取的行,要么是在事务开始之前就已经存在的,要么就是事务自身插入或修改过的。
- 行的删除版本要么没定义,要么大于当前事务版本号。这可以确保事务读取的当前行在该事务开始之前还没被删除。
满足以上两个条件的记录才会被事务读取返回。
- INSERT
InnoDB 为新插入的每一行保存当前系统版本号作为行版本号;
- DELETE
InnoDB 为删除的每一行保存当前系统版本号作为行删除标识;
- UPDATE
InnoDB 为插入一行新记录,保存当前系统版本号为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
版本链
对于InnoDB引擎来说,在聚簇索引记录中还包含着两个隐藏的列,分别是trx_id 和 roll_pointer。
- trx_id :当有事务对该记录进行修改时,每次都会将该事务id赋值给trx_id隐藏列。
- roll_pointer:每次对记录修改时,都会把旧版本写入到undo 日志中。这个隐藏列相当于指向undo日志的指针,通过该指针可以找到该记录修改前的信息。
假设表结构为
CREATE TABLE `student` (
`number` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
假设表里面已经有name=王五的数据,那么通过下面的事务(事务id=100)去更新该记录,对应的版本链如下图所示。
begin
select * from student where id=1; # 查询得到王五
update student set name='李四' where id=1;
update student set name='张三' where id=1;
commit;
ReadView
ReadView,也可以叫做一致性视图。对于读已提交 和 可重复读的事务级别来说,都必须保证读到已经提交的事务修改过的记录。核心问题是,需要判断版本链中的哪个版本是当前事务可见的,为此可以通过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提交了,那么m_ids数组是[1,2],min_tex_id是1,max_trx_id就是4;
- creator_tx_id:生成该ReadView的事务的事务id;
只有INSERT/DELETE/UPDATE 更新语句才会有事务id,SELECT语句是没有的,也即是默认值0
有了ReadView后,那么在访问某条记录时,可以按照下面的步骤来判断记录的某个版本是否可见:
- 如果被访问版本的trx_id(事务更新id) 与 ReadView视图中的creator_tx_id相同,表示当前事务在访问它自己修改的记录,所以该版本对于当前事务来说是可见的;
- 如果被访问版本的trx_id值小于ReadView视图中的min_trx_id 值,表示生成该版本的事务在当前事务生成ReadView视图前就已经提交,所以对于当前事务来说是可见的;
- 如果被访问版本的trx_id 值大于或等于 ReadView视图中的max_trx_id 值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本对于当前事务来说不可见;
- 当trx_id 在ReadView视图的 min_trx_id 和 max_trx _id 之间,则需要判断trx_id 是否在当前活跃事务组m_ids中;
- 如果在m_ids数组中,表示创建ReadView时生成该版本的事务还是活跃的,所以该版本不可访问;
- 如果不在,说明在生成ReadView之前,生成该版本的事务就已经被提交了,所以该版本可见。
用伪代码可表述如下:
/**
* 是否可见
* trx_id :访问的记录版本的事务更新id
**/
func isVisible(var trx_id) {
if trx_id == creator_tx_id:
return true;
if trx_id < min_trx_id:
return true;
if trx_id >= max_trx_id:
return false;
if trx_id >= min_trx_id and trx_id < max_trx_id:
if trx_id in m_ids:
return false;
else:
return true;
}
/**
* 遍历版本链找到当前事务可访问的记录
**/
func getVal() {
for undo in undo_list:
if isVisible(undo.trx_id):
return undo.val;
}
READ COMMITED 和 REPEATABLE READ 可以实现不同的隔离级别关键在于两者生成ReadView的时机不同。
READ COMMITED 读已提交
每次读取数据之前都会生成一个新的ReadView视图,意味着每次使用的ReadView视图都是不一样的,也即需要判断的属性值是不一样的;
① 假设有个使用READ COMMITED隔离级别的事务开始执行。
begin;
## SELECT1,此时活跃事务为[100,200]
select * from student where id=1;
那么SELECT1 的执行过程如下:
- 在执行select1 查询语句时,生成一个新的ReadView视图,假设活跃事务m_ids为[100,200],那么min_trx_id为100,max_trx_id为201,creator_trx_id就为0;
- 那么遍历版本链,拿到最新版本的trx_id;由上图可知最新版本的trx_id为100,在m_ids数组中,那么说明对当前事务不可见;
- 拿到下一个"李四" 的记录,trx_id 也为 100,同样不符合条件,继续获取下一个;
- 拿到"王五"记录的trx_id=80,小于 min_trx_id,说明当前版本在事务开始之前就已经提交,也即是可见的;
② 假设此时事务100提交了,事务100的执行语句如下:
begin;
update student set name='李四' where id=1;
update student set name='张三' where id=1;
commit;
事务200的执行如下:
begin;
update student set name='张飞' where id=1;
update student set name='关羽' where id=1;
那么在前面的查询事务中,再次查询该记录
begin;
## SELECT1,此时活跃事务为[100,200]
select * from student where id=1;
## SELECT2,此时事务100已提交
select * from student where id=1;
那么SELECT2 查询的分析如下:
- 在执行SELECT2 语句时又会单独生成一个ReadView视图。此时ReadView视图中m_ids列表为[200],mix_trx_id为200,max_trx_id为201,creator_trx_id 为0;
事务100已经提交了,所以新视图中没有该事务,这也是跟可重复读实现结果不同的原因
- 遍历版本链,发现最新记录的trx_id=200,在m_ids数组中,那么对于当前事务来说是不可见的;
- 拿到下一个"张飞"的记录,trx_id同样为200,与上面一致,不可见;
- 拿到下一个"张三"记录,trx_id为100,小于当前ReadView的min_trx_id,那么当前版本对于该查询来说是可见的,返回该记录;
可以发现,在读已提交的隔离级别下,在同一个事务中,两次查询的结果是可能不一样的,第二次的查询结果就是另一个事务100刚提交的数据;
REPEATABLE READ 可重复读
只在第一次读取数据的时候才会创建新的ReadView视图,意味着在事务执行期间,使用的ReadView视图都是同一个不变的。
从前面对读已提交隔离级别的分析来看,如果是可重复读的隔离级别,那么这两次的查询结果都是一样的,也即都是王五这条记录。因为在这两次查询中,事务生成的ReadView视图只有一个。这也就是可重复读的定义。
小结
从前面的分析中可以看出,所谓的MVCC其实就是在使用READ COMMITED、REPEATABLE READ 这两种隔离级别的事务执行普通的SELECT操作时,访问版本链的过程。这样可以使不同的事务的读-写、写-读操作并发执行,从而提升系统性能。
文档
隐式自动提交的命令:dev.mysql.com/doc/refman/…