Y说
hi,好久不见。
最近工作上有一些变动,文章很少更新。不过平时还是有收集一些文章idea,后面有空会慢慢写~
这篇文章是因为之前写了一篇关于InnoDB锁的文章,在个人网站上有读者留言问“间隙锁一定程度上解决了幻读问题,为什么不是完全解决了呢”,所以重新把MySQL中关于幻读的问题梳理了一遍。
文章写完后,发现自己也有新的收获,又了解了一些奇怪的知识点,哈哈。
隔离级别与幻读
数据库的事务有四种隔离级别,这四种隔离级别分别应用在不同的场景,隔离级别越低,并发性越高,但数据一致性就越差。四种隔离级别存在的问题如下表:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted | 是 | 是 | 是 |
Read Committed | 是 | 是 | |
Repeatable Reads | 是 | ||
Serializable |
MySQL InnoDB默认的隔离级别是Repeatable Reads(RR),但它通过MVCC+间隙锁解决了绝大部分幻读(后面会解释为什么是绝大部分而不是全部)的问题。
什么是幻读
简单来说,一个事务同样的查询条件,两次查询到的数据行数不一样,它就产生了幻读。
举例来说,对于下面的数据:
id | c | d |
---|---|---|
5 | 5 | 5 |
10 | 10 | 10 |
15 | 15 | 15 |
事务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没有完全解决快照读下的幻读问题。
可以做这个实验:
-
当前DB已有id 5, 10, 15三条数据。
-
事务A查询id < 10的数据,可以查出一行记录id = 5
-
事务B插入id = 6的数据
-
事务A再查询id < 10的数据,可以查出一行记录id = 5,查不出id = 6的数据(读场景,解决了幻读)
-
事务A可以更新/删除id = 6的数据,不能插入id = 6的数据(写场景,幻读不彻底)
这个很好理解,MySQL虽然通过MVCC的版本号来解决了读场景下的幻读,但对于上面第5步那种写场景的情况,其实是无能为力的,因为MVCC毕竟是无锁实现。
所以如果后续要对数据进行写操作,还是通过for update语句上锁比较稳妥,不然就可能会出现上面第5步那样的问题。
求个支持
都看到这儿了,如果觉得我的文章写得还行,不妨支持一下。
你的每一个转发、关注、点赞、评论都是对我最大的支持!
还有学习资源、和一线互联网公司内推哦