关于MySQL在RR级别中是否解决了幻读的问题, 一直以来都有两种说法,其中的争议点其实是:幻读的定义
- 幻读指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果。引用自《MySQL技术内幕:InnoDB存储引擎》
类似定义的有幻读 Wikipedia,MySQL官方文档- 另一种看法则认为, 如果执行不同的SQL语句导致了不同的结果, 也属于幻读 如此文InnoDB Repeatable Read隔离级别之大不同
分为两种情况: 两次快照读和两次当前读
模拟数据:
create table student ( id int unsigned, name varchar(100), primary key (id) ); insert into student values (1, '小明');
| T1事务 | T2事务(干扰事务) |
|---|---|
| begin; | begin; |
| select * from student where id > 0; | |
| insert into student (id, name) values (2, '新记录'); | |
| select * from student where id > 0; | |
| commit; |
- T1事务两次执行相同的快照读SQL, 得到的结果是一致的.
原因是: 在MVCC中, 第一次select时, 生成了事务ID, 同时生成了T1事务能够读取到的数据的快照,
而T2事务在T1事务生成快照后执行insert, 则插入的数据不属于快照内容
所以当第二次重复select时, 查询到的事务快照的结果是一致的 - 因为T2事务insert后没有立即commit, 所以RC级别也是相同的结果, 原理也是一样
| T1事务 | T2事务(干扰事务) |
|---|---|
| begin; | begin; |
| select * from student where id > 0 for update; | |
| insert into student values (2, '新记录'); (阻塞中, 等待执行) |
|
| select * from student where id > 0 for update; |
虽然ANSI SQL标准中定义的RR级别是存在幻读可能性的,
但是特殊地, 在MySQL中, 由于T2事务的insert并未实际执行, 所以T1事务的两次当前读必然是相同结果
原因在于MySQL有Next-key锁,
- 当T1事务第一次当前读时, 不仅将当前存在的记录(id=1)施加行锁,
- 同时针对当前尚不存在的记录(如id=2)之间的间隙, 施加间隙锁
(因为记录当前不存在, 所以无法施加行锁)
通过间隙锁, 使得T2事务阻塞, 无法执行在id>0的间隙中插入数据
这样, MySQL在重复执行相同的SQL时(两次快照读和两次当前读), 结果都一致, 所以认为, 在MySQL的RR级别下, 通过MVCC+Next-key锁, 解决了幻读问题
| T1事务 | T2事务(干扰事务) |
|---|---|
| begin; | begin; |
| select * from student where id > 0; | |
| insert into student values(2, '新记录'); | |
| commit; (这样T1事务才能执行当前读) |
|
| select * from student where id > 0 for update; |
此时, 两次select(一次快照读, 一次当前读)结果不一致!
第二次select不是读取快照, 而是读取数据库当前状态数据, 所以会查询到T2事务刚插入的数据
因为在逻辑上这确实也是一种"不可重复读", 所以部分人认为这也是幻读, 且不能被RR级别解决, 只能Serializable级别中解决