一、MySQL 幻读被彻底解决了吗
MySQL InnoDB 引擎的默认隔离级别虽然是【可重复读】,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:
-
针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好的避免了幻读问题。
-
针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁 + 间隙锁)方式解决了幻读,因为当执行 select ... For update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会阻塞,无法成功插入,所以就很好了避免幻读问题。
这次,我会举例两个实验场景来说明 MySQL InnoDB 引擎的可重复读隔离级别发生幻读的问题。
二、什么是幻读?
幻读是数据库中的一种并发问题,指在同一事务中执行两次相同的查询,但第二次查询返回了第一次查询所没有的新数据行。【前后读取的记录数不一致】
举个例子:
这是一个电商的大致逻辑,一般用户购买商品后付的钱会先冻结在平台上,然后由平台在固定的时间内结算用户款,例如七天一结算、半月一结算等方式,在结算业务中通常都会涉及到核销处理,也就是将所有为【已签收状态】的订单改为【已核销状态】。
此时假设连接 1 / 事务A 正在执行【半月结算】这个工作,那首先会读取订单表所有状态为【已签收】的订单,并将其更改为【已核销】状态,然后将用户款打给商家。但此时恰巧,某个用户的订单正好到了自动确认收货的时间,因此在事务 A 刚刚改完表中订单的状态时,事务 B 又向表中插入了一条【已签收状态】的订单并提交了,当事务 A 完成打款后,再次查询订单表,结果会发现表中还有一条【已签收状态】的订单数据未结算,这就好像产生了幻觉一样。
发生幻读问题的原因是在于:另外一个事务在第一个事务要处理的目标数据范围之内新增了数据,然后先于第一个事务提交造成的问题。
三、快照图是如何避免幻读的?
可重复读隔离级别是由 MVCC(多版本并发控制)实现的,实现的方式是启动事务后,在执行第一个查询语句后,会创建一个 Read View,后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新记录,也是查询不出来这条数据的,所以就很好的避免了幻读问题。
举个例子:
在 MySQL 默认的可重复级别下,有两个事务的执行顺序如下:
从上面查询结果可以看出,即使事务 B 中途插入了一条记录,事务 A 前后两次查询的结果集都是一样的,并没有出现所谓的幻读现象。
四、当前读是如何避免幻读的?
MySQL 里除了普通查询是快照图,其他都是当前读,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。
这很好理解,假设你要 update 一个记录,另一个事务已经 delete 这条记录并且提交事务了,这样不是会产生冲突吗,所以 update 的时候肯定要知道最新的数据。
另外,select ... for update,这种查询语句是当前读,每次执行的时候都是读取最新的数据。
举个例子:
事务 A 执行了这条当前读语句后,就在对表中的记录加上 id 范围 (2,正无穷] 的 next-key lock 间隙锁和记录锁的组合。
然后,事务 B 在执行插入语句的时候,判断到插入的位置被事务 A 加了 next-key lock,于是事务 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交了事务。这就避免了由于事务 B 插入新纪录而导致事务 A 发生幻读的现象。
五、幻读被彻底解决了吗?
可重复读隔离级别下虽然很大程度上避免了幻读,但是还是没有能完全解决幻读。
5.1、第一个发生幻读现象的场景:
在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新纪录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。
因为这种特殊场景的存在,在首次查询之后,进行了修改操作使用了当前读,然后再进行查询,得到了当前读的内容,所以我们认为 MySQL Innodb 中的 MVCC 并不能完全避免幻读现象。
5.2、第二个发生幻读现象的场景:
-
事务 A 先执行【快照读语句】:select * from t_test where id > 100 得到了3条记录。
-
事务 B 插入一个 id = 200 的记录并提交。
-
事务 A 再执行【当前读语句】: select * from t_test where id > 100 for update 就会得到4条记录,此时也发生了幻读现象。
小结:
MySQK Innodb 引擎的可重复读隔离级别(默认隔离级),根据不同的查询方式,分别提出了避免幻读的方案:
-
针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。
-
针对当前读(select ... For update 等语句),是通过 next-key lock (记录锁+间隙锁)方式解决了幻读。
所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。
要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... For update 这类当前读的语句,因为它会对记录加 next-key lock ,从而避免其他事务插入一条新记录。