内容简介:假设增强为:扫描过程中行锁的冲突关系(跟行锁有冲突关系的是
CREATE TABLE `t` ( `id` INT(11) NOT NULL, `c` INT(11) DEFAULT NULL, `d` INT(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) ) ENGINE=InnoDB; INSERT INTO t VALUES (0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);
定义与问题
定义
- 幻读:在同一个事务内,前后两次查询 同一范围 的时候,后一次查询看到了前一次查询没有看到的行
- 幻读专指 新插入的行
- 在 RR 隔离级别下, 普通查询是快照读 ,是看不到其他事务插入的数据的
- 幻读仅在 当前读 时才会出现
解决思路
只有行锁
假设 SELECT * FROM t WHERE d=5 FOR UPDATE;
只会在 id=5
这一行上加 X Lock
,执行时序如下:
时刻 | session A | session B | session C |
---|---|---|---|
T1 | BEGIN; SELECT * FROM t WHERE d=5 FOR UPDATE; result:(5,5,5) |
||
T2 | UPDATE t SET d=5 WHERE id=0; UPDATE t SET c=5 WHERE id=0; |
||
T3 | SELECT * FROM t WHERE d=5 FOR UPDATE; result:(0,5,5),(5,5,5) |
||
T4 | INSERT INTO t VALUES (1,1,5); UPDATE t SET c=5 WHERE id=1; |
||
T5 | SELECT * FROM t WHERE d=5 FOR UPDATE; result:(0,5,5),(1,1,5),(5,5,5) |
||
T6 | COMMIT; |
-
T1
返回id=5
这1行 -
T3
返回id=0
和id=5
这2行-
id=0
不是幻读,因为不是新插入的行
-
-
T5
返回id=0
、id=1
和id=5
的这三行-
id=1
是 幻读 ,因为这是 新插入的行 - 显然只有行锁( RC )是无法解决幻读问题的
-
幻读的问题
破坏语义
- session A在
T1
时刻声明:锁住所有d=5
的行,不允许其他事务进行读写操作 - session B在
T2
时刻修改了id=0,d=5
这一行 - session C在
T4
时刻修改了id=1,d=5
这一行
破坏数据一致性
数据
时刻 | session A | session B | session C |
---|---|---|---|
T1 | BEGIN; SELECT * FROM t WHERE d=5 FOR UPDATE; UPDATE t SET d=100 WHERE d=5; |
||
T2 | UPDATE t SET d=5 WHERE id=0; UPDATE t SET c=5 WHERE id=0; |
||
T3 | SELECT * FROM t WHERE d=5 FOR UPDATE; | ||
T4 | INSERT INTO t VALUES (1,1,5); UPDATE t SET c=5 WHERE id=1; |
||
T5 | SELECT * FROM t WHERE d=5 FOR UPDATE; | ||
T6 | COMMIT; |
-
UPDATE
与SELECT...FOR UPDATE
的加锁语义一致(X Lock
) -
T1
时刻,id=5
这一行变成了(5,5,100)
,在T6
时刻才正式提交 -
T2
时刻,id=0
这一行变成了(0,5,5)
-
T4
时刻,新插入了一行(1,5,5)
binlog
UPDATE t SET d=100 WHERE d=5;
UPDATE t SET d=5 WHERE id=0; -- (0,0,5) UPDATE t SET c=5 WHERE id=0; -- (0,5,5) INSERT INTO t VALUES (1,1,5); -- (1,1,5) UPDATE t SET c=5 WHERE id=1; -- (1,5,5) UPDATE t SET d=100 WHERE d=5; -- 所有d=5的行,d改成100
- 该binlog如果在备库上执行,最终结果为
(0,5,100)
,(1,5,100)
,(5,5,100)
,id=0
和id=1
这两行数据会与主库不一致 - 原因:
SELECT * FROM t WHERE d=5 FOR UPDATE;
只给id=5
这一行X Lock
加强行锁
增强为:扫描过程中 所有 碰到的行,都加上 X Lock
,执行序列如下
时刻 | session A | session B | session C |
---|---|---|---|
T1 | BEGIN; SELECT * FROM t WHERE d=5 FOR UPDATE; UPDATE t SET d=100 WHERE d=5; |
||
T2 | UPDATE t SET d=5 WHERE id=0;(blocked) UPDATE t SET c=5 WHERE id=0; |
||
T3 | SELECT * FROM t WHERE d=5 FOR UPDATE; | ||
T4 | INSERT INTO t VALUES (1,1,5); UPDATE t SET c=5 WHERE id=1; |
||
T5 | SELECT * FROM t WHERE d=5 FOR UPDATE; | ||
T6 | COMMIT; |
- session A把 所有的行 都加了
X Lock
,因此session B在执行第一个update语句时被锁住了- 需要等到
T6
时刻,session A提交之后,session B才能继续执行
- 需要等到
- 对于
id=0
这一行,在数据库中的最终结果还是(0,5,5)
binlog
INSERT INTO t VALUES (1,1,5); -- (1,1,5) UPDATE t SET c=5 WHERE id=1; -- (1,5,5) UPDATE t SET d=100 WHERE d=5; -- 所有d=5的行,d改成100 UPDATE t SET d=5 WHERE id=0; -- (0,0,5) UPDATE t SET c=5 WHERE id=0; -- (0,5,5)
-
id=0
这一行的最终结果也是(0,5,5)
,因此id=0
这一行的数据是一致的 - 对于
id=1
这一行数据而言,在数据库端的结果为(1,5,5)
,而根据binlog的执行结果是(1,5,100)
,数据不一致- 并且依然存在 幻读
- 原因:只能给加锁时存在的行加
X Lock
- 在
T3
时刻,在给所有的行加X Lock
时,此时id=1
这一行还不存在,因此也就加不上X Lock
了 - 即使在 所有的记录 都加上了
X Lock
,依旧 阻止不了插入新纪录
- 在
解决方案
Gap Lock
- 产生幻读的原因:行锁只能锁住行,新插入记录这个动作,要更新的是记录之间的 间隙
- 为了解决幻读,InnoDB引入了新的锁: 间隙锁 ( Gap Lock )
表初始化,插入了6个记录,产生了7个间隙
-
SELECT * FROM t WHERE d=5 FOR UPDATE;
- 给已有的6个记录加上
X Lock
,同时还会加上7个Gap Lock
,这样就确保 无法再插入新纪录
- 给已有的6个记录加上
- 上锁实体
- 数据行
- 数据行之间的间隙
冲突关系
行锁
行锁的冲突关系(跟行锁有冲突关系的是 另一个行锁 )
S Lock | X Lock | |
---|---|---|
S Lock | 兼容 | 冲突 |
X Lock | 冲突 | 冲突 |
间隙锁
跟 间隙锁 存在冲突关系的是 往这个间隙插入一个记录的操作 , 间隙锁之间不会相互冲突
session A | session B |
---|---|
BEGIN; SELECT * FROM t WHERE c=7 LOCK IN SHARE MODE; |
|
BEGIN; SELECT * FROM t WHERE c=7 FOR UPDATE; |
- session B并不会被阻塞 ,因为表t里面并没有
c=7
的记录- 因此session A加的是 间隙锁
(5,10)
,而session B也是在这个间隙加间隙锁 - 两个session有共同的目标: 保护这个间隙,不允许插入值,但两者之间不冲突
- 因此session A加的是 间隙锁
Next-Key Lock
- 间隙锁和行锁合称
Next-Key Lock
,每个Next-Key Lock
都是 左开右闭 区间 -
SELECT * FROM t WHERE d=5 FOR UPDATE;
形成了7个Next-Key Lock
,分别是-
(-∞,0],(0,5],(5,10],(10,15],(15,20],(20,25],(25,+supremum]
-
+supremum
:InnoDB给每一个索引加的一个 不存在的最大值supremum
-
- 约定:
Gap Lock
为 左开右开 区间,Next-Key Lock
为 左开右闭 区间
可能死锁
-- 并发执行 -- 死锁并不是大问题,回滚重试即可 BEGIN; SELECT * FROM t WHERE id=N FOR UPDATE; -- 如果行不存在 INSERT INTO t VALUES (N,N,N); -- 如果行存在 UPDATE t SET d=N SET id=N; COMMIT;
session A | session B |
---|---|
BEGIN; SELECT * FROM t WHERE id=9 FOR UPDATE; |
|
BEGIN; SELECT * FROM t WHERE id=9 FOR UPDATE; |
|
INSERT INTO t VALUES (9,9,9);(blocked) | |
INSERT INTO t VALUES (9,9,9);(Deadlock fund) |
- session A执行
SELECT * FROM t WHERE id=9 FOR UPDATE;
,id=9
这一行不存在,会加上 间隙锁(5,10)
- session B执行
SELECT * FROM t WHERE id=9 FOR UPDATE;
,间隙锁之间不冲突,同样会加上 间隙锁(5,10)
- session B试图插入一行
(9,9,9)
,被session A的间隙锁阻塞 - session A试图插入一行
(9,9,9)
,被session B的间隙锁阻塞,两个session相互等待,形成 死锁- InnoDB的 死锁检测 很快就会发现死锁,并让session A的insert语句 报错返回
- 解决方案:假如 只有一个唯一索引 ,可以用
INSERT ... ON DUPLICATE KEY UPDATE
来替代
小结
- 引入
Gap Lock
,会导致同样的语句 锁住更大的范围 , 影响并发度 -
Gap Lock
是在 RR 隔离级别下才生效的(在 RC 隔离级别是没有Gap Lock
的) - 解决 数据与日志不一致 的另一个方案:RC + binlog_format=row
- 如果 RC (没有
Gap Lock
,锁范围更小)隔离级别够用,业务并不需要可重复读的保证,可以选择RC
- 如果 RC (没有
参考资料
《MySQL实战45讲》
转载请注明出处:http://zhongmingmao.me/2019/02/14/mysql-phantom/
访问原文「MySQL -- 幻读」获取最佳阅读体验并参与讨论
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- MySQL -- 幻读
- MySQL实战 -- 可重复读 与 幻读
- 你未必清楚什么是MySQL的幻读
- MySQL到底是怎么解决幻读的
- MySQL实验: 实践索引对全列匹配、最左前缀匹配、范围查询等条件的影响以及了解脏读、幻读等
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
数据库索引设计与优化
【美】Tapio Lahdenmaki、【美】Michael Leach / 曹怡倩、赵建伟 / 电子工业出版社 / 2015-6 / 79.00元
《数据库索引设计与优化》提供了一种简单、高效、通用的关系型数据库索引设计方法。作者通过系统的讲解及大量的案例清晰地阐释了关系型数据库的访问路径选择原理,以及表和索引的扫描方式,详尽地讲解了如何快速地估算SQL 运行的CPU 时间及执行时间,帮助读者从原理上理解SQL、表及索引结构、访问方式等对关系型数据库造成的影响,并能够运用量化的方法进行判断和优化,指导关系型数据库的索引设计。 《数据库索......一起来看看 《数据库索引设计与优化》 这本书的介绍吧!