面试官:幻读是啥,会有什么问题?如何解决?
共 3533字,需浏览 8分钟
·
2021-09-21 16:58
你知道的越多,不知道的就越多,业余的像一棵小草!
你来,我们一起精进!你不来,我和你的竞争对手一起精进!
编辑:业余草
cnblogs.com/jian0110/p/15080603.html
推荐:https://www.xttblog.com/?p=5277
大家中秋快乐!
前言
❝我们知道 MySQL 在可重复读隔离级别下别的事物提交的内容,是看不到的。而可提交隔离级别下是可以看到别的事务提交的。而如果我们的业务场景是在事物内同样的两个查询我们需要看到的数据都是一致的,不能被别的事物影响,就使用可重复读隔离级别。这种情况下 RR 级别下的普通查询(快照读)依靠 MVCC 解决“幻读”问题,如果是“当前读”的情况需要依靠什么解决“幻读”问题呢?这就是本博文需要探讨的。
在探讨前可以看下之前的博文(MySQL 是如何实现事务隔离?),主要介绍隔离级别的具体技术细节,读过以后看此篇文章可能更有帮助。
注:本博文讨论的“幻读”都是指在“可重复读”隔离级别下进行。
❞
一、什么是幻读?
假设我们有表 t 结构如下,里面的初始数据行为:(0,0,0),(1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5)。
CREATE TABLE `t`
(
`id` INT(11) NOT NULL,
`key` INT(11) DEFAULT NULL,
`value` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `value` (`value`)
) ENGINE = InnoDB;
INSERT INTO t
VALUES (0, 0, 0),
(1, 1, 1),
(2, 2, 2),
(3, 3, 3),
(4, 4, 4),
(5, 5, 5)
假设select * from where value=1 for update
,只在这一行加锁(注意这只是假设),其它行不加锁,那么就会出现如下场景:
Session A 的三次查询 Q1-Q3 都是select * from where value = 1 for update
,查询的 value=1 的所有 row。
T1:Q1只返回一行(1,1,1); T2:session B 更新 id=0 的 value 为 1,此时表 t 中 value=1 的数据有两行 T3:Q3 返回两行(0,0,1),(1,1,1) T4:session C插入一行(6,6,1),此时表 t 中 value=1 的数据有三行 T5:Q3 返回三行(0,0,1),(1,1,1),(6,6,1) T6:session A 事物 commit。
其中 Q3 读到 value=1 这一样的现象,就称之为幻读,「幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行」。
先对“幻读”做出如下解释:
「在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现(三个查询都是 for update
表示当前读);」「上面 session B 的修改 update 结果,被 session A 之后的 select 语句用“当前读”看到,不能称为幻读,幻读仅专指“新插入的行”」。
幻读有什么问题?
「(1)需要单独解决」
众所周知,select ...for update
语句就是将相应的数据行锁住,比如 session A 在 T1 时刻的 Q1 查询语句:select * from where value=1 for update
就是将 value=1 的数据行锁住,但显然如果是上述的场景发生,此时的 for update 语义被破坏了(并没有锁住 value=1 的数据行)。
「即使把所有的记录都加上锁,还是阻止不了新插入的记录,所以“幻读”问题要单独拿出来解决。没法依靠 MVCC 或者行锁机制来解决。这就引出“间隙锁”,是另外一种加锁机制」。
「(2)间隙锁引发的并发度」
间隙锁引入以后,可能会导致同样语句锁住更大的范围,这可能就会影响了并发度。具体请看下面介绍
「三、如何解决幻读?」
「产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁(Gap Lock)」。
间隙:比如表中加入 6 个记录0,5,10,15,20,25
。则产生 7 个间隙:
「在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙也加上了间隙锁。这样就确保了无法再插入新的记录。」
「间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间(间隙锁开区间,next-key lock 前开后闭区间)」:
间隙锁与间隙锁之间是不存在冲突的,冲突的是往间隙里插入一条记录。
表 t 中是没有 value=7 这个数据的,所以 Q1 加的间隙锁(1,5),而 Q2 也是加的这个间隙锁,两者不冲突都是为了保护这个间隙不允许插入值。
在表 t 初始化后,假设表的数据如下:
如果用select * from for update
执行,则会把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是(-∞,0]、(0,2]、(2,4]、(4,6]、(6,8]、(8, 10]、(10, +supremum]。
间隙锁的引入,可能会导致同样的语句锁住更大的范围,是会影响了并发度
假设发生如下场景:
则明显发生了死锁,分析如下:
Q1:执行
select …for update
语句,由于id=9
这一行并不存在,因此会加上间隙锁 (8,10);Q2:执行
select …for update
语句,同样会加上间隙锁 (8,10),间隙锁之间不会冲突,因 此这个语句可以执行成功;session B 试图插入一行 (9,9,9),被 session A 的间隙锁挡住了,只好进入等待;
session A 试图插入一行 (9,9,9),被 session B 的间隙锁挡住了。
有上述可知间隙锁的引入,可能会导致同样语句锁住更大的范围,这其实是影响了并发度。
为了解决幻读问题可以采用读可提交隔离级别,间隙锁是在可重复读隔离级别下才会生效的。所以如果把隔离级别设置为读提交的话, 就没有间隙锁了。但同时,你要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row,也就是说采用“ RC 隔离级别 + 日志格式 binlog_format=row ”组合
。
总结
「RR 隔离级别下间隙锁才有效,RC 隔离级别下没有间隙锁;」
「RR 隔离级别下为了解决“幻读”问题:“快照读”依靠 MVCC 控制,“当前读”通过间隙锁解决;」
「间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间;」
「间隙锁的引入,可能会导致同样语句锁住更大的范围,影响并发度。」