阅读 545

MySQL到底在RR层面解决幻读了吗?

Y说

hi,好久不见。

最近工作上有一些变动,文章很少更新。不过平时还是有收集一些文章idea,后面有空会慢慢写~

这篇文章是因为之前写了一篇关于InnoDB锁的文章,在个人网站上有读者留言问“间隙锁一定程度上解决了幻读问题,为什么不是完全解决了呢”,所以重新把MySQL中关于幻读的问题梳理了一遍。

文章写完后,发现自己也有新的收获,又了解了一些奇怪的知识点,哈哈。

隔离级别与幻读

数据库的事务有四种隔离级别,这四种隔离级别分别应用在不同的场景,隔离级别越低,并发性越高,但数据一致性就越差。四种隔离级别存在的问题如下表:

隔离级别脏读不可重复读幻读
Read uncommitted
Read Committed
Repeatable Reads
Serializable

MySQL InnoDB默认的隔离级别是Repeatable Reads(RR),但它通过MVCC+间隙锁解决了绝大部分幻读(后面会解释为什么是绝大部分而不是全部)的问题。

什么是幻读

简单来说,一个事务同样的查询条件,两次查询到的数据行数不一样,它就产生了幻读。

举例来说,对于下面的数据:

idcd
555
101010
151515

事务A第一次查询sql:

SELECT * FROM demo WHERE id < 10;
复制代码

这个时候只能查出 id = 5 的数据。

而这个时候如果事务B插入了一条id = 6的数据:

INSERT INTO demo VALUES (7, 7, 7);
复制代码

然后A进行第二次查询,用同样的sql,如果查到了两条数据:id = 5 和 id = 7,那就是出现了幻读。对于事务A来说,就像幻觉一样,同样的查询条件,查出来的数据多了一行。

这个在业务场景中也很常见:先查询名为“编了个程”的公众号,发现不存在,于是我准备创建的时候,却发现已经被其它事务创建了,于是唯一索引提示我创建失败,改名称已存在。那我这个事务岂不是出现幻觉了?明明刚刚查过不存在的呀~

快照读和当前读

快照读

在MySQL中,查询语句分为两种,一种是简单的select操作,属于快照读,不加锁。它读的是记录的快照版本(这个版本跟MVCC有关,后面再细聊)。

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 ?;
复制代码

快照读下的幻读

InnoDB是通过MVCC来解决当前读下的幻读问题的。每个事务会有一个递增的事务ID,每行记录都有两个隐藏字段:创建版本和删除版本。在进行操作时,遵循以下规则:

INSERT

保存当前事务id作为行版本号

DELETE

保存当前的事务id到这行数据的“删除版本”。

UPDATE

插入一行新记录,保存当前事务id作为行版本号,同时保存当前事务id到原来的行的“删除版本”。

SELECT

  • 只读取版本小于等于当前事务id的行。这样可以保证事务读取都的行,要么之前就存在,要么是这个事务本身自己插入或者修改的。
  • 只读取“删除版本”为空或者小于等于当前事务id的行。这样可以确保事务读取到的行,在事务之前没有被删除。

当前读下的幻读

当前读下的幻读是通过间隙锁(gap_lock)来实现的。在事务A查询的时候,会锁住一个间隙,其它事务往这个间隙插入、删除等操作都是会被锁阻塞的。

关于间隙锁是如何工作的,可以参考我之前写的这篇文章:《InnoDB的行锁,原来为你做了这么多!》,简单来说,间隙锁和插入意向锁互斥,彻底解决了当前读下的幻读问题。

快照读没有完全解决幻读?

是的,MySQL没有完全解决快照读下的幻读问题。

可以做这个实验:

  1. 当前DB已有id 5, 10, 15三条数据。

  2. 事务A查询id < 10的数据,可以查出一行记录id = 5

  3. 事务B插入id = 6的数据

  4. 事务A再查询id < 10的数据,可以查出一行记录id = 5,查不出id = 6的数据(读场景,解决了幻读)

  5. 事务A可以更新/删除id = 6的数据,不能插入id = 6的数据(写场景,幻读不彻底)

这个很好理解,MySQL虽然通过MVCC的版本号来解决了读场景下的幻读,但对于上面第5步那种写场景的情况,其实是无能为力的,因为MVCC毕竟是无锁实现。

所以如果后续要对数据进行写操作,还是通过for update语句上锁比较稳妥,不然就可能会出现上面第5步那样的问题。

求个支持

我是Yasin,一个坚持技术原创的博主,我的微信公众号是:编了个程

都看到这儿了,如果觉得我的文章写得还行,不妨支持一下。

文章会首发到公众号,阅读体验最佳,欢迎大家关注。

你的每一个转发、关注、点赞、评论都是对我最大的支持!

还有学习资源、和一线互联网公司内推哦

文章分类
后端
文章标签