参考:
当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行。
按理论来说,只有到 可串行化 的最高隔离级别才能解决幻读问题,但是 MySql 在可重复读的隔离级别下就已经通过一些手段解决了幻读问题:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。无锁化,生成 ReadView 时版本链上已经提交的事务可见。
- 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。每次都读最新记录,通过锁来控制并发。
这两个解决方案是很大程度上解决了幻读现象,但是还是有个别的情况造成的幻读现象是无法解决的。
情况1
- 在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView
- 之后事务 B 向表中新插入了一条 id = 5 的记录并提交。
- 接着,事务 A 对 id = 5 这条记录进行了更新操作(有点违和,因为前面 select id=5 的没查到东西还去 update),在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id。(update 操作是当前读,每次都读最新提交的记录,只要记录上无独占锁。事务 B 已经提交所以在 id=5 的记录上加的写锁已经释放,所以事务 A 能读到事务 B 最新提交的)
- 之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了(同一个事务的修改对自己都是可见的),于是就发生了幻读。
情况2
- T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
- T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
- T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。(同理,select ... for update 操作是当前读,每次都读最新提交的记录,只要记录上无独占锁。事务 B 在 id=200 的记录上加的写锁已经释放,所以事务 A 能读到事务 B 最新提交的)
要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。
所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。