《MySQL实战45讲》——学习笔记20 “幻读、全表扫描的加锁方式、间隙锁、next-key lock“

news2024/9/24 19:23:25

本篇介绍MySQL在可重复度RR隔离级别下,引入的一种锁机制:间隙锁 (Gap Lock);间隙锁与事务相关的表锁、行锁不同,它锁的是“往这个间隙中插入一个记录”这个操作,除此之外间隙锁之间都不存在冲突关系(因而有可能发生死锁);

间隙锁和行锁合称 next-key lock,每个next-key lock是前开后闭区间;如果使用 select * from t for update 这种全表扫描的语句(不走二级索引),要把整个表的所有记录以及所有的间隙给锁起来,代价很大;因此建议更新的时候能尽量走主键或者二级索引

本篇仅介绍间隙锁和next-key lock的概念以及引入的原因,但是并没有说明加锁规则;加锁规则参考下一篇文章专门介绍;

可重复读隔离级别下"当前读"怎么加锁?

为方便讨论加锁规则,这里用一个简单的表结构说明,建表和初始化语句如下:

# 3个字段的简单表:主键id、索引列c、非索引列d
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;

# 表中初始化6条数据
insert into t values
(0,0,0),
(5,5,5),
(10,10,10),
(15,15,15),
(20,20,20),
(25,25,25);

(1)对指定主键的列加读锁;SQL语句如下:

select * from t where id=1 lock in share mode;

由于 id 上有索引,所以可以直接定位到 id=1 这一行,因此读锁也是只加在了这一行上;但如果是下面的 SQL 语句呢?

(2)对指定的非索引列加读锁;SQL语句如下:

begin;
select * from t where d=5 for update;
commit;

比较好理解的是,这个语句会命中d=5的这一行记录(5,5,5),对应的主键id=5,因此在select语句执行完成后,id=5这一行会加一个写锁,而且由于两阶段锁协议,这个写锁会在执行commit语句的时候释放;

由于字段d上没有索引,因此这条查询语句会做全表扫描;那么,其他被扫描到的,但是不满足条件d=5行记录上,会不会被加锁呢?(本文讨论的场景都是使用InnoDB的默认事务隔离级别,即可重复读);

假设1:假设只在 id=5 这一行加锁,而其他被扫描的行的不加锁

基于上面的假设(这里是假设,不是真实情况!),尝试分析下下面的场景;

现在分析下Q1/Q2/Q3这3次"当前查"分别会返回什么结果;

(1)Q1只返回id=5这一行;
(2)在T2时刻,sessionB把id=0这一行的d值改成了5,因此T3时刻Q2查出来的是id=0和id=5这两行;
(3)在T4时刻,sessionC又插入一行(1,1,5),因此T5时刻Q3查出来的是id=0、id=1和id=5的这三行;

其中,Q3读到id=1这一行的现象,被称为“幻读”;

幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的"多出来的"行

在可重复读隔离级别下,普通的查询是快照读(一致性读),是不会看到别的事务插入的数据的;因此,幻读在“当前读”下才会出现

上面sessionB的修改结果,被sessionA之后的select语句用“当前读”看到,不能称为幻读;幻读仅专指“新插入的行”

Q1/Q2/Q3这3个查询都是加了for update,都是当前读;而当前读的规则,就是要能读到所有已经提交的记录的最新值;并且,sessionB和sessionC的两条语句,执行后就会提交,所以Q2和Q3应该能看到sessionA和sessionB这两个事务的操作效果,这跟事务的可见性规则并不矛盾;

因此结论就是——可能会导致幻读

幻读有什么问题?

1. 破坏了加锁的语义

首先是语义上,sessionA在T1时刻就声明了,“我要把所有d=5的行锁住,不准别的事务进行读写操作”;而实际上,这个语义被破坏了;sessionB和sessionC通过修改原数据行或新插入数据行,导致产生了新的满足d=5的行,这些新的数据行都没有被"锁住";

2. 破坏了数据和日志在逻辑上的一致性

锁的设计是为了保证数据的一致性;而这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性

为了说明这个问题,给sessionA在T1时刻再加一个更新语句,即:update t set d=100 where d=5;再来看下整个执行流程;

SQL执行的过程:(整个过程不存在锁等待)

(1)经过T1时刻,id=5这一行变成(5,5,100),当然这个结果最终是在T6时刻正式提交的;
(2)经过T2时刻,id=0这一行变成(0,5,5);
(3)经过T4时刻,表里面多了一行(1,5,5);

总结下来,T6时刻后,id=0/id=1/id=5这3行的结果,变成了(0,5,5) / (1,,5,5) / (5,5,100);

binlog是在commit时生成(两阶段提交),日志逻辑如下

(1)T2时刻,sessionB事务提交,写入了两条语句,id=0这一行变成(0,5,5);
(2)T4时刻,sessionC事务提交,写入了两条语句,表里面多了一行(1,5,5);
(3)T6时刻,sessionA事务提交,写入了update t set d=100 where d=5这条语句;满足d=5的记录的d的值都被更新成100;

总结下来,T6时刻后,id=0/id=1/id=5这3行的结果,变成了(0,5,100) / (1,5,100) / (5,5,100);

也就是说——id=0和id=1这两行,发生了数据不一致binlog恢复出来的结果和实际表里的数据不一致,这个问题很严重,是不行的;

假设2:把扫描过程中碰到的行也都加上写锁

经过上面的分析可知,假设1 "select * from t where d=5 for update" 这条语句只给d=5这一行,也就是id=5的这一行加锁”是不行的,会导致幻读,破坏对d=5的记录加锁的语义,并且产生的binlog逻辑与实际的数据不一致;

现在假设把扫描过程中碰到的行也都加上写锁,来看下问题是否解决;

分析:由于sessionA把所有的行都加了写锁,所以sessionB在执行第一个update语句的时候就被锁住了;需要等到T6时刻sessionA提交以后,sessionB才能继续执行;所以,id=0这一行的数据不一致的问题解决了;

但是,id=1这一行,在数据库里面的结果是(1,5,5),而根据binlog的执行结果是(1,5,100),也就是说幻读的问题还是没有解决;

原因很简单,在T1时刻,我们给所有行加锁的时候,id=1这一行还不存在,不存在也就加不上锁;也就是说,即使把当前所有的记录都加上锁,还是阻止不了未来新插入的记录被"当前读"读到(幻读),这也是为什么“幻读”会被单独拿出来解决的原因;

如何解决幻读?

产生幻读的原因是,行锁只能锁住当前的行,但是未来新插入记录锁不了;为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁(Gap Lock);

由于未来新插入的数据只能插到当前表数据主键id之间的空隙中(主键唯一),因此间隙锁,顾名思义,锁的就是两个值之间的空隙;

比如文章开头的表t,初始化插入了6个记录,这就产生了7个间隙,如下;

这样,当执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的6个记录加上了行锁(会全表扫描6条记录),还同时加了7个间隙锁;这样就确保了当前无法再插入新的记录;

也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁;即数据行是可以加上锁的实体,数据行之间的间隙,也是可以加上锁的实体

但是间隙锁跟我们之前碰到过的锁都不太一样;行锁锁冲突规则是:读读不冲突,其他的读写/写读/写写都会冲突,也就是说跟行锁有冲突关系的是“另外一个行锁”;

但是间隙锁与行锁不一样,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作;间隙锁之间都不存在冲突关系;举个例子:


这里sessionB并不会被堵住;因为表t里并没有c=7这个记录,因此sessionA加的是间隙锁(5,10);而sessionB也是在这个间隙加的间隙锁;它们有共同的目标,即:保护(5,10)这个间隙,不允许插入值;但这两个"当前读"语句之间是不冲突的;

注意:这里的(5,10)指的是c的值而非主键索引;因为c字段有索引,对条件所在的索引位置的前后间隙加锁;如果查询条件没走索引走全表扫描,则对全表所有行之间加间隙锁;关于加锁的详细规则在下篇介绍;

next-key lock

间隙锁和行锁合称next-key lock;把间隙锁记为开区间,因此每个next-key lock是前开后闭区间;

例如,对于上面的表t,如果执行select * from t for update要把整个表所有记录锁起来(全表扫描),就形成了7个next-key lock,分别是(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20,25]、(25,+supremum];(因为+∞是开区间,InnoDB给每个索引加了一个不存在的最大值supremum,这样才符合前面说的“next-key lock都是前开后闭区间”)

间隙锁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;

你可能想到一种SQL写法 insert … on duplicate key update;但MySQL官方文档有声明,在有多个唯一键的时候,insert … on duplicate key update 这个写法并发是不安全的并且这种写法一般在公司的MySQL开发手册中是不推荐/禁止使用的,细节可参考下面的文章:Mysql死锁排查:insert on duplicate死锁一次排查分析过程;另一种写法 replace 语句也有类似的作用,详情可参考insert...on duplicate key update语法详解;

回到这个例子,现象是:这个业务逻辑一旦有并发,就会碰到死锁;你一定也觉得奇怪,这个逻辑每次操作前用for update锁起来,已经是最严格的模式了,怎么还会有死锁呢?这里,用两个session来模拟并发,并假设N=9;

通过图中的分析,至此,两个session进入互相等待状态,形成死锁;当然,InnoDB的死锁检测马上就发现了这对死锁关系,两个事务回滚成本一样,选择让sessionA的insert语句报错返回了;

可见——间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的

小结

1. 幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行;幻读仅专指“新插入的行”;

2. 在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的;因此,幻读在“当前读”下才会出现;

3. 幻读会导致对满足条件的行加锁这个语义被破坏,因为加锁后插入了新行;

4. 幻读会导致binlog恢复的数据与真实数据不一致,因为在提交事务之前,有新行插入,导致提交的事务中的更新语句作用到了预期之外的新行;

5. 为了解决幻读问题,InnoDB间隙锁 (Gap Lock),锁的就是两个值之间的空隙,可以是主键,也可以是二级索引;

6. 与行锁不同,间隙锁之间都不存在冲突关系,跟间隙锁存在冲突关系的,是"往这个间隙中插入一个记录"这个操作;

7. 间隙锁和行锁合称next-key lock,每个 next-key lock 是前开后闭区间;

8. 间隙锁和next-key lock的引入,解决了幻读的问题,但同时在并发情况下,可能导致死锁发生,原因是间隙锁获取不冲突导致多个线程都持有同一个间隙锁,但是执行插入时会冲突;

9. 为了解决幻读的问题引入间隙锁,但也带来了影响并发的问题;如果把隔离级别设置为读提交的话,就不存在幻读,也没有间隙锁了,如使用读提交隔离级别加 binlog_format=row 的组合;但是就无法使用"一致性视图"来做到"边备份边更新数据"了;

本篇建议与下篇文章结合着一起看;

下篇文章:待定

本章参考:20 | 幻读是什么,幻读有什么问题?-极客时间

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

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

相关文章

Spring @Autowire注解源码详解

目录 一:触发方式: 二:源码解析 2.1 扫描注入点 2.2 属性赋值 一:触发方式: 1.Spring容器在每个Bean实例化之后,调用AutowireAnnotationBeanPostProcessor的postProcessMergedBeanDefinition方法进行扫…

[附源码]计算机毕业设计美发店会员管理系统Springboot程序

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

七周成为数据分析师 | 数据可视化

一.常见的初级图表 维度:描述分析的角度和属性,分类数据(时间,地理位置,产品类型等) 度量:具体的参考数值,数值数据(元,销量,销售金额等&#x…

windows监控linux服务器资源grafana+prometheus+node_exporter

Windows环境监控Linux服务器资源grafanaprometheusnode_exporter 1.安装包下载 链接:https://pan.baidu.com/s/1xqdIYNtadt2tRSN-XlELUw 提取码:12342.安装grafana (1)将压缩包解压后,在bin文件目录下,点…

【Linux】shell命令以及运行原理和Linux权限详解

本期主题:Linux权限详解博客主页:小峰同学分享小编的在Linux中学习到的知识和遇到的问题小编的能力有限,出现错误希望大家不吝赐 目录 🍁1.shell命令以及运行结果 🍁2. Linux用户管理 🍁3. Linux权限管理 …

Python进阶学习之阅读代码

起因 在这个过程中,学习到了一些东西,同时整理了自己以前的一些收获,然后分享给大家,有不对的地方还望海涵、指正。 阅读代码有助于处理bug 阅读代码是一项更重要的技能,在大学编程语言的考试中也有相关的考察——代…

Paper reading:Fine-Grained Head Pose Estimation Without Keypoints (CVPR2018)

Paper reading:Fine-Grained Head Pose Estimation Without Keypoints (CVPR2018) 一、 背景 为什么要读这篇论文,因为LZ之前要做头部姿态估计,看到一些传统的方法,都是先进行人脸检测,然后再…

Java基于JSP旅游网站系统的设计于实现

我国的旅游事业目前正处于一个科学技术日新月异飞速向前发展的环境中。信息技术和通信技术以令人目不暇接的速度发展,尤其是互联网络的广泛流行,使得各种服务信息已近乎透明,且个性突出的游客们已不再满足于死板的标准化的旅游项目&#xff0…

JavaScript-T2

JavaScript-T2 前言 本次主要讲解的知识点是: JavaScript自定义函数 JavaScript系统函数 JavaScript 事件 JavaScript 的常用事件 JavaScript自定义函数 函数就是为了完成程序中的某些特定功能而进行专门定义的一段程序代码 函数包括自定义函数和系统函数 使用函数…

Akka 学习(二)第一个入门程序

目录一 sbt 介绍1.1 Sbt1.2 下载安装1.3 sbt的特点1.4 Idea 配置Sbt开发工具二 构建定义2.1 指定版本2.2 build.sbt 设置三 代码实现3.1 Java版本3.2 Scala版本3.3 对比一 sbt 介绍 1.1 Sbt sbt 是为 Scala 和 Java 项目构建的。它是93.6%的 Scala 开发人员的首选构建工具&am…

2000-2021年各省GDP包括名义GDP、实际GDP、GDP平减指数(以2000年为基期)

全国31省市GDP平减指数(2000-2021年)及计算步骤 1、时间:2000-2021年 2、范围:31省 3、数据包括:2000-2021年各省市GDP平减指数,以2000年为基期,包括数据来源、计算方法、公式等。 4、计算步骤: 第一步…

物联卡采购注意要点有哪些

在这个万物互联的时代,针对于企业设备联网的物联卡就显得格外重要了,而共享单车,移动支付,智慧城市,自动售卖机等企业采购物联卡会面临着各种问题,低价陷阱,流量虚假,管理混乱&#…

sealos issue #2157 debug 思路流程记录

sealos issues#2157 debug思路流程前言分析issue剖析源码解决方案总结前言 这个项目蛮有意思的,sealos 是以 kubernetes 为内核的云操作系统发行版。 boss上看到 -> 沟通 -> 解决某个issue直接offer -> 舒服 本文记录解决 issue 的思路 分析issue BUG…

Linux系统常用的工具

1.1 Vscode编辑器 从官网下载 ubuntu 版本,官网地址:https://code.visualstudio.com/。下载xxx.deb的包。 或者使用指令下载:wget https://az764295.vo.msecnd.net/stable/6261075646f055b99068d3688932416f2346dd3b/code_1.73.1-1667967334…

基于Intel Lake-UP3平台为半导体与集成电路测试设备提供优异计算性能

为什么半导体和IC测试设备需要升级? 随着众多新的高性能应用的需求不断增加,信迈旨在为半导体集成电路测试设备领域的客户提供更好的方案。半导体和集成电路(IC)测试设备设计用于在一台测试机上同时对不同线路的数百个集成电路…

How Can We Know What Language Models Know?

Abstract 最近的工作通过让语言模型(LM)填补诸如“奥巴马是一个职业”之类的提示的空白,提出了一个有趣的结果,以检查语言模型(LM)中包含的知识。这些提示通常是手动创建的,而且很可能不是最佳…

Linux进程通信之进程信号

一、信号的概念: 信号机制是Linux最基本的通讯机制,它可以用来向一个或者多个进程发送异步事件信息,传送少量信息。信号是一个软件中断,并且是一个“软中断”(只是告诉有这样一个信号,但这个信号具体如何进…

Redis6入门到实战------思维导图+章节目录

Redis学习大纲 思维导图 思维导图 Redis6入门到实战------1、NoSQL数据库简介 地址: Redis6入门到实战------2、Redis6概述和安装 地址: Redis6入门到实战------3、常用五大数据类型 地址: Redis6入门到实战------4、Redis6配置文件详解…

Stack Overflow 临时禁用 ChatGPT 生成内容,网友:人类和AI快打起来!

如果有一天我们查询到的「知识」真假难辨,那这就太可怕了。 要问最近 AI 圈哪个模型最火爆,你不得不把 OpenAI 推出的 ChatGPT 排在前面。自从发布以来,这个对话模型可谓是出尽风头,很多人更是对其产生了一百个新玩法,…

Linux系统移植四:Petalinux使用本地sstate-cache加速构建根文件系统

根文件系统简介 根文件系统 rootfs 是Linux内核启动以后挂载(mount)的第一个文件系统,然后从根文件系统中读取初始化脚本,比如rcS,inittab等 根文件系统和Linux内核是分开的,单独的Linux内核是没法正常工作的,必须要…