MySQL实战解析底层---幻读是什么,幻读有什么问题?

news2024/12/25 9:28:53

目录

前言

幻读是什么?

幻读有什么问题?

如何解决幻读?


  • 前言

  • 为了便于说明问题,这一篇文章,就先使用一个小一点儿的表
  • 建表和初始化语句如下:

  • 这个表除了主键id外,还有一个索引c,初始化语句在表中插入了6行数据
  • 下面的语句序列,是怎么加锁的,加的锁又是什么时候释放的呢?

  • 比较好理解的是,这个语句会命中d=5的这一行,对应的主键id=5,因此在select 语句执行完成后,id=5这一行会加一个写锁,而且由于两阶段锁协议,这个写锁会在执行commit语句的时候释放
  • 由于字段d上没有索引,因此这条查询语句会做全表扫描
  • 那么,其他被扫描到的,但是不满足条件的5行记录上,会不会被加锁呢?
  • InnoDB的默认事务隔离级别是可重复读,所以本文接下来没有特殊说明的部分,都是设定在可重复读隔离级别下
  • 幻读是什么?

  • 现在就来分析一下,如果只在id=5这一行加锁,而其他行的不加锁的话,会怎么样
  • 下面先来看一下这个场景(注意:这是假设的一个场景):

  • 可以看到,session A里执行了三次查询,分别是Q1、Q2和Q3
  • 它们的SQL语句相同,都是select *fromt where d=5 for update
  • 这个语句的意思应该很清楚了,查所有d=5的行,而且使用的是当前读,并且加上写锁
  • 现在来看一下这三条SQL语句,分别会返回什么结果
    • 1-Q1只返回id=5这一行
    • 2-在T2时刻,session B把id=0这一行的d值改成了5,因此T3时刻Q2查出来的是id=0和id=5这两行
    • 3-在T4时刻,session C又插入一行(1,1,5),因此T5时刻Q3查出来的是id=0、id=1和id=5的这三行
  • 其中,Q3读到id=1这一行的现象,被称为“幻读”
  • 也就是说,幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行
  • 这里需要对“幻读”做一个说明:
    • 1-在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的;因此,幻读在“当前读”下才会出现
    • 2-上面session B的修改结果,被session A之后的select语句用“当前读”看到,不能称为幻读;幻读仅专指“新插入的行”
  • 如果只从事务可见性规则来分析的话,上面这三条SQL语句的返回结果都没有问题
  • 因为这三个查询都是加了for update,都是当前读
  • 而当前读的规则,就是要能读到所有已经提交的记录的最新值
  • 并且,session B和session C的两条语句,执行后就会提交,所以Q2和Q3就是应该看到这两个事务的操作效果,而且也看到了,这跟事务的可见性规则并不矛盾
  • 但是,这是不是真的没问题呢?
  • 不,这里还真就有问题
  • 幻读有什么问题?

  • 首先是语义上的
  • session A在T1时刻就声明了,“我要把所有d=5的行锁住,不准别的事务进行读写操作”
  • 而实际上,这个语义被破坏了
  • 如果现在这样看感觉还不明显的话,再往session B和session C里面分别加一条SQL语句,再看看会出现什么现象

  • session B的第二条语句update t set c=5 where id=0,语义是“我把id=0、d=5这一行的c值,改成了5”
  • 由于在T1时刻,session A 还只是给id=5这一行加了行锁, 并没有给id=0这行加上锁
  • 因此,session B在T2时刻,是可以执行这两条update语句的
  • 这样,就破坏了 session A 里Q1语句要锁住所有d=5的行的加锁声明
  • session C也是一样的道理,对id=1这一行的修改,也是破坏了Q1的加锁声明
  • 其次,是数据一致性的问题
  • 锁的设计是为了保证数据的一致性
  • 而这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性
  • 为了说明这个问题,给session A在T1时刻再加一个更新语句,即:update t set d=100 where d=5

  • update的加锁语义和select …for update 是一致的,所以这时候加上这条update语句也很合理
  • session A声明说“要给d=5的语句加上锁”,就是为了要更新数据,新加的这条update语句就是把它认为加上了锁的这一行的d值修改成了100
  • 现在来分析一下上图执行完成后,数据库里会是什么结果:
    • 1-经过T1时刻,id=5这一行变成(5,5,100),当然这个结果最终是在T6时刻正式提交的
    • 2-经过T2时刻,id=0这一行变成(0,5,5)
    • 3-经过T4时刻,表里面多了一行(1,5,5)
    • 4-其他行跟这个执行序列无关,保持不变
  • 这样看,这些数据也没啥问题,但是再来看看这时候binlog里面的内容:
    • 1-T2时刻,session B事务提交,写入了两条语句
    • 2-T4时刻,session C事务提交,写入了两条语句
    • 3-T6时刻,session A事务提交,写入了update t set d=100 where d=5 这条语句
  • 统一放到一起的话,就是这样的:

  • 这样应该看出问题了
  • 这个语句序列,不论是拿到备库去执行,还是以后用binlog来克隆一个库,这三行的结果,都变成了 (0,5,100)、(1,5,100)和(5,5,100)
  • 也就是说,id=0和id=1这两行,发生了数据不一致
  • 这个问题很严重,是不行的
  • 到这里再回顾一下,这个数据不一致到底是怎么引入的?
  • 分析一下可以知道,这是假设“select * from t where d=5 for update这条语句只给d=5这一行,也就是id=5的这一行加锁”导致的
  • 所以可以认为,上面的设定不合理,要改
  • 那怎么改呢?
  • 把扫描过程中碰到的行,也都加上写锁,再来看看执行效果

  • 由于session A把所有的行都加了写锁,所以session B在执行第一个update语句的时候就被锁住了
  • 需要等到T6时刻session A提交以后,session B才能继续执行
  • 这样对于id=0这一行,在数据库里的最终结果还是(0,5,5)
  • 在binlog里面,执行序列是这样的:

  • 可以看到,按照日志顺序执行,id=0这一行的最终结果也是(0,5,5)
  • 所以,id=0这一行的问题解决了
  • 但同时也可以看到,id=1这一行,在数据库里面的结果是(1,5,5),而根据binlog的执行结果是(1,5,100),也就是说幻读的问题还是没有解决
  • 为什么已经这么“凶残”地,把所有的记录都上了锁,还是阻止不了id=1这一行的插入和更新呢?
  • 原因很简单
  • 在T3时刻,给所有行加锁的时候,id=1这一行还不存在,不存在也就加不上锁
  • 也就是说,即使把所有的记录都加上锁,还是阻止不了新插入的记录,这也是为什么“幻读”会被单独拿出来解决的原因
  • 如何解决幻读?

  • 现在知道了,产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”
  • 因此,为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁(GapLock)
  • 顾名思义,间隙锁,锁的就是两个值之间的空隙
  • 比如文章开头的表t,初始化插入了6个记录,这就产生了7个间隙

  • 这样,当你执行 select * from t where d=5 for update的时候,就不止是给数据库中已有的6个记录加上了行锁,还同时加了7个间隙锁
  • 这样就确保了无法再插入新的记录
  • 也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁
  • 现在知道了,数据行是可以加上锁的实体,数据行之间的间隙,也是可以加上锁的实体
  • 但是间隙锁跟我们之前碰到过的锁都不太一样
  • 比如行锁,分成读锁和写锁
  • 下图就是这两种类型行锁的冲突关系

  • 也就是说,跟行锁有冲突关系的是“另外一个行锁”
  • 但是间隙锁不一样,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作
  • 间隙锁之间都不存在冲突关系
  • 这句话不太好理解,举个例子:

  • 这里session B并不会被堵住
  • 因为表t里并没有c=7这个记录,因此session A加的是间隙锁(5,10)
  • 而session B也是在这个间隙加的间隙锁
  • 它们有共同的目标,即:保护这个间隙,不允许插入值
  • 但,它们之间是不冲突的
  • 间隙锁和行锁合称next-key lock,每个next-key lock是前开后闭区间
  • 也就是说,表t初始化以后,如果用select * from t for update要把整个表所有记录锁起来,就形成了7个next-keylock
  • 分别是(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]
  • 这篇文章中,如果没有特别说明,把间隙锁记为开区间,把next-key lock记为前开后闭区间
  • 那这个supremum从哪儿来的呢?
  • 这是因为+∞是开区间
  • 实现上,InnoDB给每个索引加了一个不存在的最大值supremum,这样才符合前面说的“都是前开后闭区间”
  • 间隙锁和next-key lock的引入,帮我们解决了幻读的问题,但同时也带来了一些“困扰”
  • 对应到这个例子的表来说,业务逻辑这样的:
  • 任意锁住一行,如果这一行不存在的话就插入,如果存在这一行就更新它的数据
  • 代码如下:

  • 那这个不是insert …on duplicate key update 就能解决吗?
  • 但其实在有多个唯一键的时候,这个方法是不能满足需求的
  • 现在就只讨论这个逻辑
  • 这个逻辑一旦有并发,就会碰到死锁
  • 你一定也觉得奇怪,这个逻辑每次操作前用for update锁起来,已经是最严格的模式了,怎么还会有死锁呢?
  • 这里用两个session来模拟并发,并假设N=9

  • 可以看到,其实都不需要用到后面的update语句,就已经形成死锁了
  • 按语句执行顺序来分析一下:
  • 1-session A 执行select …for update语句,由于id=9这一行并不存在,因此会加上间隙锁(5,10)
  • 2-session B 执行select …for update语句,同样会加上间隙锁(5,10),间隙锁之间不会冲突,因此这个语句可以执行成功
  • 3-session B 试图插入一行(9,9,9),被session A的间隙锁挡住了,只好进入等待
  • 4-session A 试图插入一行(9,9,9),被session B的间隙锁挡住了
  • 至此,两个session进入互相等待状态,形成死锁
  • 当然,InnoDB的死锁检测马上就发现了这对死锁关系,让session A的insert语句报错返回了
  • 现在知道了,间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的
  • 那为了解决幻读的问题,引入了这么一大串内容,有没有更简单一点的处理方法呢
  • 在文章一开始就说过,如果没有特别说明,这里分析的问题都是在可重复读隔离级别下的,间隙锁是在可重复读隔离级别下才会生效的
  • 所以如果把隔离级别设置为读提交的话,就没有间隙锁了
  • 但同时要解决可能出现的数据和日志不一致问题,需要把binlog格式设置为row
  • 这个配置到底合不合理
  • 关于这个问题本身的答案是,如果读提交隔离级别够用,也就是说,业务不需要可重复读的保证,这样考虑到读提交下操作数据的锁范围更小(没有间隙锁),这个选择是合理的
  • 但其实配置是否合理,跟业务场景有关,需要具体问题具体分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/688886.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

web值控制标签的显示与隐藏、document、getElementById、style、css、hidden、display、visibility

文章目录 方式一方式二方式三visibility小结 方式一 使用HTML的hidden属性,隐藏后不占用原来的位置 hidden属性是一个Boolean类型的值,如果想要隐藏元素,就将值设置为true,否则就将值设置为false 选取id为test的元素 let test do…

“事后达尔文”—— 游戏业务效果评估方法实践

作者:vivo 互联网数据分析团队 Luo Yandong、Zhang Lingchao 本文介绍了互联网业务数据效果评估的几种常见问题及方法,并基于分层抽样的逻辑优化出一套可应用于解决用户不均匀的“事后达尔文"分析法,可适用于无法AB测试或人群不均匀的AB…

VCO的设计

理想振荡器只有电感和电容,会一直振荡下去。但是实际的振荡器存在一定的寄生电阻并联在RC两端,会使振幅变小。因此需要RC旁边再并联一个负电阻以此来抵消寄生电阻的影响。一般会选择负阻提供的能量为寄生电阻能量的的2-3倍。如果负阻跟RC并联&#xff0c…

剖析float相加产生精度损失的原因

float相加产生精度损失的原因 一、什么是float类型及其特点1.1、float类型的定义和使用方法1.2、float类型的特点,包括精度限制 二、为什么会出现float相加精度损失2.1、计算机二进制存储浮点数的方式2.2、浮点数运算中的舍入误差2.3、累加多个小数时的误差累积 三、…

kali中Metasploit基本使用方法

1.kali启动postgresql并设置开机自启动 systemctl start postgresql.servicesystemctl enable postgresql.service2.kali启动Metasploit 方式一:应用程序 -> 漏洞利用工具集 -> Metasploit framework 方式二: msfconsole 3. Metasploit常用命令 connect 命令 连接远程主…

Qt信号槽之connect介绍(上)

关于Qt信号槽中connect与disconnect介绍 首先我们要知道,如果想要使用Qt中的信号槽机制, 那么必须继承QObject类,因为QObject类中包含了信号槽的一系列操作,今天我们来讲解的是信号与槽怎么建立连接以及断开连接。 一、connect …

在windows server上用Mosquitto软件创建MQTT服务器

今天下午捣鼓了半天,在云服务器上面创建了个MQTT服务器,然后用MQTTX软件进行了测试。过程记录如下: 1、下载mosquitto软件,链接如下图: 2、下载完成后安装,一直点下一步下一步就好了。 3、在安装路径下&am…

快速捡回使用workbench控制mysql创建数据库的基本步凑

首先如果,不想要在原来已经建好的数据库下建立数据表,可以新建数据库。 具体操作步凑如下: 选择后如下所示: 有现成的创建代码的话,就直接复制执行现成的创建代码即可,如果没有现成的创建代码的话&#xff…

Java设计模式之单例模式-【懒汉式与饿汉式】

1、单例,模式 单例模式属于创建型模式的一种,应用于保证一个类仅有一个实例的场景下,并且提供了一个访问它的全局方法 单例模式的特点:从系统启动到终止,整个过程只会产生一个实例。单例模式常用写法:懒汉…

STM32设置为I2C从机模式

STM32设置为I2C从机模式 目录 STM32设置为I2C从机模式前言1 硬件连接2 软件编程3 运行测试3.1 I2C连续写入3.1 I2C连续读取3.1 I2C单次读写测试 4 总结 前言 STM32的I2C作为主机的情况相信很多同学都用过,网上也有很多教程,但是作为从设备使用的例子应该…

【C++ 程序设计】第 9 章:函数模板与类模板

目录 一、函数模板 (1)函数模板的概念 (2)函数模板的示例 (3)函数或函数模板调用语句的匹配顺序 二、类模板 (1)类模板概念 (2)类模板示例 &…

阵列模式综合第三部分:深度学习(附源码)

一、前言 这个例子展示了如何设计和训练卷积神经网络(CNN)来计算产生所需模式的元素权重。 二、介绍 模式合成是阵列处理中的一个重要课题。阵列权重有助于塑造传感器阵列的波束图案,以匹配所需图案。传统上,由于空间信号处理和频…

SSL工作原理

SSL 是一个安全协议,它提供使用 TCP/IP 的通信应用程序间的隐私与完整性。因特网的 超文本传输协议(HTTP)使用 SSL 来实现安全的通信。 在客户端与服务器间传输的数据是通过使用对称算法(如 DES 或 RC4)进行加密的。公…

使用ZenDAS进行Gompertz趋势分析

某项目做了18次测试,每次测试发现的缺陷个数如下表所示: 测试序号 发现缺陷数 1 60 2 96 3 157 4 191 5 155 6 106 7 64 8 335 9 92 10 196 11 109 12 133 13 166 14 129 15 16 16 30 17 19 18 5 对上述的数据在Z…

IPv6手工隧道配置与验证实验

IPv6手工隧道配置与验证实验 【实验目的】 熟悉IPv6手工隧道的概念。 掌握IPv6和IPv4共存的实现方法。 掌握IPv6手工隧道的配置。 验证配置。 【实验拓扑】 实验拓扑如下图所示。 实验拓扑 设备参数如表所示。 设备参数表 设备 接口 IPv6地址 子网掩码位数 默认网…

中间件-netty(1)

netty 前言篇 文章目录 一、IO基础篇1.概念1.1 阻塞(Block)和非阻塞(Non-Block)1.2 同步(Synchronization)和异步(Asynchronous)1.3 BIO 与 NIO 对比1.3.1 面向流与面向缓冲1.3.2 阻塞与非阻塞1.3.3 选择器的问世 2.NIO 和 BIO 如何影响应用程序的设计2.1 API调用2.2 数据处理2…

蓝桥杯专题-试题版-【操作格子】【查找整数】【分解质因数】【高精度加法】

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 👉关于作者 专注于Android/Unity和各种游…

Spring FrameWork从入门到NB -三级缓存解决循环依赖内幕 (二)

开始用上一篇文章讲到的Spring依赖注入的步骤,用两个例子来推导一下整个过程,举例子有助于了解真相。 先用一个最简单的例子:没有依赖的单例bean的创建。 推导过程是需要上一篇文章的步骤的,要参照步骤一步一步来。 无依赖的单…

Linux内核代码60%都是驱动?驱动代码不会造成内核臃肿吗?

为什么内核中驱动占比最高 一、前言二、Linux中避免内核臃肿的措施2.1 交叉编译及SDK包的裁剪2.2 设备树2.3 模块化2.4 硬件抽象层 三、嵌入式Linux的裁剪四、总结 一、前言 今天逛知乎看到这么一个问题:为什么Linux内核代码60%都是驱动? 如果每支持新的设备就加入…

【设计模式】Java设计模式——模板方法模式(Template Pattern)

文章目录 1. 介绍1.1 定义1.2 作用 2. 模式结构2.1 UML类图2.2 模式组成 3. 代码实例3.1 背景3.2 应用 4. 优点5. 缺点6. 应用场景 1. 介绍 1.1 定义 模板方法模式(Template Pattern),又叫模板模式,它属于行为型模式模板方法模式定义一个模板结构&…