MySQL 20 幻读是什么,幻读有什么问题?
首先给出要用到的数据:
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);
那么下面的语句,是怎么加锁的,加的锁又是什么时候释放的?
begin;
select * from t where d=5 for update;
commit;
该语句会命中d=5
的行,对应的主键id=5
,因此在select语句执行完成后,id=5
这一行会加一个写锁,且由于两阶段锁协议,该写锁会在执行commit语句时候释放。
由于字段d上没有索引,该语句会做全表扫描,那么其他被扫描到但不符合条件的记录是否会被加锁呢?
本文接下来没有特殊说明的,都是设定在可重复读隔离级别。
幻读是什么?
先看如果只在id=5
的行加锁会怎么样。
假设有这样一个场景:

分析session A里的三次执行:
Q1只返回
id=5
的行;在T2时刻,session B把
id=0
的d值改成了5,因此T3时刻Q2能返回两行;在T4时刻,session C插入一行,因此Q3时刻查出来3行。
其中,Q3读到id=1
这一行的现象,被称为幻读。幻读指的是一个事务前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
这里对幻读做说明:
在可重复读隔离级别下,普通的查询是快照读,不会看到别的事务插入的数据,因此幻读只有在当前读的情况下才会出现;
session B的修改结果,被session A后的select语句用当前读看到,不能称为幻读,幻读专指新插入的行。
这三个查询都是加了for update,因此都是当前读,要读到所有已经提交的记录的最新值。
从事务可见性规则分析的话,这三条SQL语句的返回结果都没有问题。但是由于造成了幻读,是有其他问题的。
幻读有什么问题?
首先是语义上的问题。session A在T1时刻的语句实际上想要声明,要把所有d=5
的行锁住,不允许别的事务进行读写操作。
如果现在看感觉不明显,看看如下情况:

在session B中,先将id=0
的行也设置了d=5
,之后设置c=5
。由于在T1时刻,session A只是给id=5
的行加了行锁,并没有给id=0
的行加锁,因此session B是可以执行这两条update语句的,这就破坏了session A在T1时刻的加锁声明。
session C也是一样,对id=1
的行的修改也破坏了加锁声明。
其次是数据一致性的问题。锁的设计是为了保证数据的一致性,这不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。
比如如下的情况:

分析数据库里的变化:
经过T1时刻,
id=5
这一行变为(5,5,100),这个结果在T6正式提交;经过T2时刻,
id=0
这一行变为(0,5,5);经过T4时刻,表里多了一行(1,5,5);
其他行与这个执行序列无关,保持不变。
而binlog里的变化:
T2时刻,session B事务提交,写入了两条语句;
T4时刻,session C事务提交,写入了两条语句;
T6时刻,session A事务提交,写入了
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
的行加锁导致的。
所以我们认为上面的设定不合理,假设改为“扫描过程中碰到的行都加上写锁”:

由于session A把所有的行都加了写锁,所以session B在执行第一个语句时就被锁住,需要等到T6时刻session A提交后,session B才能继续执行。这样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=1
这一行还是不一致。这是因为在T3时刻,给所有行加锁的时候,id=1
这一行还不存在,也就加不上锁。
也就是说,即使把所有的记录都加上锁,还是阻止不了新插入的记录。
到这里,实际上已经说明了幻读的定义以及幻读有什么问题。接下来,看看InnoDB怎么解决幻读。
如何解决幻读?
从上面可以看出,产生幻读的原因是,行锁只能锁住行,但是新插入记录,要更新的是记录之间的“间隙”。因此为了解决幻读,InnoDB引入了间隙锁。
在本文的场景中,初始化插入了6个记录,会产生7个间隙:

当执行select * from t where d=5 for update
,就不止是给数据库中已有的6个记录加上了行锁,还同时加了7个间隙锁,这样就确保无法再插入新记录。
间隙锁不像行锁那样,行锁之间会有冲突,间隙锁之间不存在冲突关系,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”的操作。
比如:

由于表里没有c=7
的记录,因此session A和session B都是想要加间隙锁(5,10),都是想保护这个间隙,因此它们之间不冲突,session B不会被堵住。
间隙锁和行锁合称临键锁(next-key lock),临键锁是前开后闭的区间。在本文的例子,如果用select * from t for update
把整个表所有记录锁起来,会形成7个临键锁,分别是\((-\infty,0],(0,5],(5,10],(10,15],(15,20],(20,25],(25,+\text{supremun}]\)。
由于\(+\infty\)是开区间,为了做到前开后闭,InnoDB给每个索引加了一个不存在的最大值supremum。
间隙锁和临键锁的引入,能帮助解决幻读的问题,但同时会带来另外的问题。比如下面的场景:

分析该场景:
session A执行
select … for update
,由于id=9
行不存在,会加上间隙锁(5,10);session B执行
select … for update
同样会加上间隙锁(5,10);session B想要插入(9,9,9),被session A的间隙锁挡住,进入等待;
session A想要插入(9,9,9),被session B的间隙锁挡住。
至此形成了死锁。因此间隙锁的引入,可能导致同样的语句锁住更大的范围,会影响并发度。
为了减少死锁,很多公司实际使用读已提交的隔离级别,同时将binlog格式设置成row,以解决可能出现的数据和日志不一致问题。
MySQL 20 幻读是什么,幻读有什么问题?的更多相关文章
- MYSQL事件隔离级别以及复读,幻读,脏读的理解
一.mysql事件隔离级别 1未提交读(READUNCOMMITTED) 另一个事务修改了数据,但尚未提交,而本事务中的SELECT会读到这些未被提交的数据(脏读)( 隔离级别最低,并发性能高 ) 2 ...
- InnoDB在MySQL默认隔离级别下解决幻读
1.结论 在RR的隔离级别下,Innodb使用MVVC和next-key locks解决幻读,MVVC解决的是普通读(快照读)的幻读,next-key locks解决的是当前读情况下的幻读. 2.幻读 ...
- 线上MySQL读写分离,出现写完读不到问题如何解决
大家好,我是历小冰. 今天我们来详细了解一下主从同步延迟时读写分离发生写后读不到的问题,依次讲解问题出现的原因,解决策略以及 Sharding-jdbc.MyCat 和 MaxScale 等开源数据库 ...
- MySQL 20个经典面试题
1.MySQL的复制原理以及流程 基本原理流程,3个线程以及之间的关联: 1. 主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中: 2. 从:io线程——在 ...
- Java使用FileReader(file)、readLine()读取文件,以行为单位,一次读一行,一直读到null时结束,每读一行都显示行号。
//Java使用FileReader(file).readLine()读取文件,以行为单位,一次读一行,一直读到null时结束,每读一行都显示行号. public static void readFi ...
- MySQL主从复制与lvs+keepalived单点写入读负载均衡高可用实验【转】
一.环境Master(主机A):192.168.1.1Slave(主机B) :192.168.1.2 W-VIP(写入) :192.168.1.3 R-VIP(读取) :192.168.1.4 ...
- MySQL 5.5加主键锁读问题【转载】
背景 有同学讨论到MySQL 5.5下给大表加主键时会锁住读的问题,怀疑与fast index creation有关,这里简单说明下. 对照现象 为了说明这个问题的原因,有 ...
- python-----opencv读视频、循环读图片显示进度条
功能:opencv读视频,显示进度条,推动进度条快进.后退,按q退出.代码如下: import os import cv2 def nothing(emp): pass def jindu(name, ...
- 读源码【读mybatis的源码的思路】
✿ 需要掌握的编译器知识 ★ 编译器为eclipse为例子 调试准备工作(步骤:Window -> Show View ->...): □ 打开调试断点Breakpoint: □ 打开变量 ...
- Java基础之读文件——使用通道读二进制数据(ReadPrimes)
控制台程序,本例读取Java基础之写文件部分(PrimesToFile)写入的primes.bin. import java.nio.file.*; import java.nio.*; import ...
随机推荐
- 需要的效果它都有,让AI对话开发效率翻倍!这款Ant Design扩展组件库绝了
嗨,大家好,我是小华同学,关注我们获得"最新.最全.最优质"开源项目和高效工作学习方法 ant-design-x-vue 是基于 Ant Design Vue 的扩展组件库,专注于 ...
- Spring中的依赖注入DI
目录 Spring中的依赖注入DI Spring中的依赖注入DI 依赖注入的简单理解就是给对象设置变量值. Spring配置文件 <?xml version="1.0" en ...
- RPC实战与核心原理之优雅关闭
优雅关闭:如何避免服务停机带来的业务损失? 上线的大致流程 当服务提供方要上线的时候,一般是通过部署系统完成实例重启.在这个过程中,服务提供方的团队并不会事先告诉调用方他们需要操作哪些机器,从而让调用 ...
- C#之使用线程池
简述 创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销,线程池就是该问题的解决方案,我们事先分配一定的资源,将这些资源放入资源池,每次需要新的资源,只需从池中获取一个,而不用创建 ...
- wireshark的所有入门指令(总结与摘要)
wireshark的所有指令 常用捕获过滤器 1.基于IP地址进行捕获 host 10.3.1.1 dst host 10.3.1.1 net 192.168.1.0/24 net 192.168.1 ...
- linux 的 Docker 配置(版本24.04)
linux 的docker配置(版本24.04) 这里默认是server版本的, 个人感觉好用,资源消耗少 1.配置ssh连接 个人习惯用ssh连接使用 (如果失败,先配置下一步的换源) sudo a ...
- Spring 注解之@RequestBody和@PostMapping
@RequestBody的使用 注解@RequestBody用于接收前端传递给后端的.JSON对象的字符串,这些数据位于请求体中,适合处理的数据为非Content-Type: applicatio ...
- 上传自己java项目到maven中央仓库pom
前提 首先的你项目需要在Gitee或者Github上有仓库 我这里以Gitee是的yhchat-sdk-core仓库为例 开始 在sonatype上创建问题 访问sonatype注册并登录 创建一个问 ...
- 「Log」做题记录 2024.1.29-
\(2024.1.29-2024.2.4\) \(\color{royalblue}{P5903}\) 树上 \(k\) 级祖先模板,长链剖分. \(\color{blueviolet}{CF1009 ...
- Vue 学习笔记 [Part 7]
作者:故事我忘了¢个人微信公众号:程序猿的月光宝盒 目录 一. Promise 1.0 什么是Promise 1.1. Promise的基本使用 1.2. Promise的链式调用 1.3. Prom ...