我们知道,对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包 含两个必要的隐藏列(row_id 并不是必要的,我们创建的表中有主键或者非 NULL 的 UNIQUE 键时都不会包含 row_id 列):
trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事 务 id 赋值给 trx_id 隐藏列。
roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修 改前的信息。
为了说明这个问题,我们创建一个演示表
CREATE TABLE teacher (
number INT,
name VARCHAR(100),
domain varchar(100),
PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8;
然后向这个表里插入一条数据:
INSERT INTO teacher VALUES(1, ‘Jack’, ‘源码系列’);
现在表里的数据就是这样的:
假设插入该记录的事务 id 为 60,那么此刻该条记录的示意图如下所示:
假设之后两个事务 id 分别为 80、120 的事务对这条记录进行 UPDATE 操作,操 作流程如下:
每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一 个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没 有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的 情况就像下图一样:
| Trx 80 | Trx 120 |
|---|---|
| BEGIN | |
| BEGIN | |
| UPDATE teacher SET name = ‘Mark’ WHERE number = 1; | |
| UPDATE teacher SET name = ‘James’ WHERE number = 1; | |
| COMMIT | |
| UPDATE teacher SET name = ‘King’ WHERE number = 1; | |
| UPDATE teacher SET name = '大飞 WHERE number = 1; | |
| COMMIT |
对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的 一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成 一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的 值。另外,每个版本中还包含生成该版本时对应的事务 id。于是可以利用这个记 录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版 本并发控制(Mulit-Version Concurrency Control MVCC)。
前面我们已经知道了,REPEATABLE READ 隔离级别下 MVCC 可以解决不可重 复读问题,那么幻读呢?MVCC 是怎么解决的?幻读是一个事务按照某个相同条 件多次读取记录时,后读取时读到了之前没有读到的记录,而这个记录来自另一 个事务添加的新记录。
我们可以想想,在 REPEATABLE READ 隔离级别下的事务 T1 先根据某个搜索 条件读取到多条记录,然后事务 T2 插入一条符合相应搜索条件的记录并提交, 然后事务 T1 再根据相同搜索条件执行查询。结果会是什么?按照 ReadView 中的 比较规则:
3、如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值, 表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以 被当前事务访问。
4、如果被访问版本的* trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间**(min_trx_id < trx_id < max_trx_id)**,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的, 该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经 被提交,该版本可以被访问。
不管事务 T2 比事务 T1 是否先开启,事务 T1 都是看不到 T2 的提交的。请自 行按照上面介绍的版本链、ReadView 以及判断可见性的规则来分析一下。
但是,在 REPEATABLE READ 隔离级别下 InnoDB 中的 MVCC 可以很大程度地 避免幻读现象,而不是完全禁止幻读。怎么回事呢?我们来看下面的情况:
我们首先在事务 T1 中:
select * from teacher where number = 30;
很明显,这个时候是找不到 number = 30 的记录的。
我们在事务 T2 中,执行:
通过执行 insert into teacher values(30,‘Luffy’,‘ELK’);,我们往表中插入了一条 number = 30 的记录。
此时回到事务 T1,执行:
update teacher set domain=‘RabbitMQ’ where number=30;
select * from teacher where number = 30;
嗯,怎么回事?事务 T1 很明显出现了幻读现象。
在 REPEATABLE READ 隔离级别下,T1 第一次执行普通的 SELECT 语句时生成 了一个 ReadView,之后 T2 向 teacher 表中新插入一条记录并提交。
ReadView 并不能阻止 T1 执行 UPDATE 或者 DELETE 语句来改动这个新插入 的记录(由于 T2 已经提交,因此改动该记录并不会造成阻塞),但是这样一来, 这条新记录的 trx_id 隐藏列的值就变成了 T1 的事务 id。之后 T1 再使用普通的 SELECT 语句去查询这条记录时就可以看到这条记录了,也就可以把这条记录返 回给客户端。因为这个特殊现象的存在,我们也可以认为 MVCC 并不能完全禁 止幻读。