纠错
我猜,你们在各种博文中看到对于幻读的解释是这样的:
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
即事务A 执行两次 select 操作得到不同的数据集,即 select 1 得到 10 条记录,select 2 得到 11 条记录。
这其实并不是幻读,这是不可重复读的一种,只会在 R-U R-C 级别下出现,而在 mysql 默认的 RR 隔离级别是不会出现的(下面会举例推翻)。
然而,我终于在茫茫文章中,找到了相对正确的解释:
幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。
更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
推翻错误的解释
事务隔离级别
mysql 有四级事务隔离级别 每个级别都有字符或数字编号
读未提交 READ-UNCOMMITTED | 0:存在脏读,不可重复读,幻读的问题
读已提交 READ-COMMITTED | 1:解决脏读的问题,存在不可重复读,幻读的问题
可重复读 REPEATABLE-READ | 2:解决脏读,不可重复读的问题,存在幻读的问题,默认隔离级别,使用 MMVC机制 实现可重复读
序列化 SERIALIZABLE | 3:解决脏读,不可重复读,幻读,可保证事务安全,但完全串行执行,性能最低
幻读会在 RU / RC / RR 级别下出现,SERIALIZABLE 则杜绝了幻读,但 RU / RC 下还会存在脏读,不可重复读,故我们就以 RR 级别来研究幻读,排除其他干扰。
举例推翻
建表a,id列自增主键,name列唯一索引。
CREATE TABLE a (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(255) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY UIDX_NAME (name)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
准备初始数据:
INSERT INTO a VALUES(NULL,'a'),(NULL,'b');
MySQL隔离级别设置为RR(默认),准备两个事务AB。然后依次执行:
- 开始事务A,执行查询语句:
START TRANSACTION;
SELECT * FROM a WHERE name BETWEEN 'a' AND 'z';
+----+------+
| id | name |
+----+------+
| 1 | a |
| 2 | b |
+----+------+
2 rows in set
- 开启事务B,执行插入语句,并提交
START TRANSACTION;
INSERT INTO a VALUES(NULL,'c'),(NULL,'d');
COMMIT;
- 事务A再次执行查询语句:
SELECT * FROM a WHERE name BETWEEN 'a' AND 'z';
+----+------+
| id | name |
+----+------+
| 1 | a |
| 2 | b |
+----+------+
2 rows in set
小结
可以看到,在RR级别下,所谓的"幻读"并没有出现。而SQL-92标准中定义的RR级别是没法解决幻读的,这就是矛盾点所在。如果是所谓的"幻读",事务A应该读到abcd四条数据。出现这种情况有两种可能:
- MySQL在RR级别解决了幻读。
- 这不是真正的幻读。
查阅MySQL官方文档,并没有某一段文字说明其在RR级别解决了幻读问题。
https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html
所以说,其实这并不是“幻读”的范畴,这仍然属于RR级别所解决的,不可重复读范畴。
解释
我们能确定的是,RR级别解决了不可重复读的问题。
那么为什么说上述例子属于不可重复读范畴呢?我们得从解决不可重复读问题的原理MVCC讲起。
MVCC
MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
快照读VS当前读
在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQL InnoDB为例:
快照读:简单的select操作,属于快照读,不加锁。
select * from table where ?;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。
为什么将 插入/更新/删除 操作,都归为当前读?
一个Update操作的具体流程。当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。
注:针对一条当前读的SQL语句,InnoDB与MySQL Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操作;然后在读取下一条加锁,直至读取完毕。
总结
所以说,在上述例子中,事务A第二次读取(快照读)的是记录的历史版本。而不是最新的版本——事务B插入新纪录后的abcd四条。这属于不可重复读的范畴。
有些博文错误
的把这种情况归为“幻读”。甚至还有的说MySQL的RR级别解决了幻读。在此纠错。
参考:
https://segmentfault.com/a/1190000016566788?utm_source=tag-newest
http://hedengcheng.com/?p=771