MySQL实战45讲 20

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);

这个表除了主键 id 外,还有一个索引 c,初始化语句在表中插入了 6 行数据。

Q:下面的语句序列,是怎么加锁的,加的锁又是什么时候释放的呢?

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

A:

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

Q:由于字段 d 上没有索引,因此 这条查询语句会做全表扫描。那么,其他被扫描到的,但是不满足条件的 5 行记录上,会不会被加锁呢?

A:会

InnoDB 的默认事务隔离级别是可重复读,所以接下来都是设定在可重复读隔离级别下。

幻读是什么?

如果只在 id=5 这一行加锁,而其他行的不加锁的话,会怎么样。

假设只在 id=5 这一行加行锁的场景如下图:

MySQL实战45讲 20

session A 里执行了三次查询,分别是 Q1、Q2 和 Q3。它们的 SQL 语句相同,都是 select * from t 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 语句用”当前读”看到, 不能称为幻读幻读仅专指”新插入的行”

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

幻读有什么问题?

语义上

首先是语义上的。session A 在 T1 时刻就声明了,”我要把所有 d=5 的行锁住,不准别的事务进行读写操作”。而实际上,这个语义被破坏了。

为了体现语义被破坏,再往 session B 和 session C 里面分别加一条 SQL 语句

MySQL实战45讲 20

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 的加锁声明。

数据和日志逻辑一致性

其次,是数据一致性的问题。

锁的设计目的是确保数据一致性。这种一致性不仅包括此时数据库内部数据状态的一致性,还包括数据和日志的逻辑一致性。

[En]

Locks are designed to ensure data consistency. This consistency includes not only the consistency of the internal data state of the database at this moment, but also the logical consistency of data and logs.

给 session A 在 T1 时刻再加一个更新语句,即:update t set d=100 where d=5。

MySQL实战45讲 20

update 的 加锁语义和 select …for update 是 一致的,所以这时候加上这条 update 语句也很合理。session A 声明说”要给 d=5 的语句加上锁”,就是为了要更新数据, 新加的这条 update 语句就是把它认为加上了锁的这一行的 d 值修改成了 100

分析执行完成后数据库中的结果。

[En]

Analyze what the results will be in the database after the execution is complete.

  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 这条语句

如果你把它们放在一起,它是这样的:

[En]

If you put it together, it goes like this:

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 这条语句只给 d=5 这一行,也就是 id=5 的这一行加锁”导致的。

把扫描过程中碰到的行,也都加上写锁,再来看看执行效果。

MySQL实战45讲 20

由于 session A 把所有的行都加了写锁,所以 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),也就是说幻读的问题还是没有解决。这是由于在 T3 时刻,我们给所有行加锁的时候 ,id=1 这一行还不存在,不存在也就加不上锁

也就是说,即使把所有的记录都加上锁,还是阻止不了新插入的记录

如何解决幻读?

间隙锁

产生幻读的原因是, 行锁只能锁住行,但是 新插入记录这个动作,要更新的是记录之间的”间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。

间隙锁,锁的就是两个值之间的空隙。比如开头的表 t,初始化插入了 6 个记录, 这就产生了 7 个间隙

下图是表 t 主键索引上的行锁和间隙锁

MySQL实战45讲 20

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

也就是说这时候, 在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。

行锁分为读锁和写锁。下图显示了这两种类型的行锁之间的冲突关系。

[En]

The row lock is divided into read lock and write lock. The following figure shows the conflicting relationship between these two types of row locks.

MySQL实战45讲 20

也就是说,与行锁冲突的是“另一个行锁”。

[En]

In other words, it is “another row lock” that conflicts with the row lock.

但是间隙锁不一样, 跟间隙锁存在冲突关系的,是”往这个间隙中插入一个记录”这个操作。间隙锁 之间不存在冲突关系。

举个例子:

MySQL实战45讲 20

这里 session B 并不会被堵住。因为表 t 里并没有 c=7 这个记录,因此 session A 加的是间隙锁 (5,10)。而 session B 也是在这个间隙加的间隙锁。它们 有共同的目标,即: 保护这个间隙,不允许插入值。但, 它们之间是不冲突的。

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]。

如果没有特别说明,我们把 间隙锁记为开区间,把 next-key lock 记为前开后闭区间

Q: supremum 从哪儿来的呢?

A:这是因为 +∞是开区间。实现上,InnoDB 给每个索引加了一个不存在的最大值 supremum,这样才符合前面说的”都是前开后闭区间”。

一些”困扰”

间隙锁和 next-key lock 的引入,帮我们解决了幻读的问题,但同时也带来了一些”困扰”。

要求:任意锁定一行,不存在则插入,存在则更新数据,如下所示:

[En]

Requirements: arbitrarily lock a row, insert it if it does not exist, and update its data if it exists, as follows:

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;

Q:不是 insert … on duplicate key update 就能解决吗?

A:但在有多个唯一键的时候,这个方法是不能满足需求(待填)

遇到的现象是,这个逻辑一旦具有并发性,就会遇到死锁。

[En]

The phenomenon encountered is that once this logic has concurrency, it will encounter a deadlock.

Q:这个逻辑每次操作前用 for update 锁起来,已经是最严格的模式了,怎么还会有死锁呢?

A: 间隙锁导致的死锁

用两个 session 来模拟并发,并假设 N=9

MySQL实战45讲 20

按照语句执行的顺序分析它:

[En]

Analyze it in the order in which the statements are executed:

  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

Original: https://www.cnblogs.com/ydssx7/p/16532558.html
Author: ydssx
Title: MySQL实战45讲 20

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/505125/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • java中如何在ISO-8859-1和UTF-8之间相互转换呢?

    我们都知道在一些特殊的场景,我们需采用特殊的编码格式,如:UTF-8,但是系统默认的编码为ISO-8859-1 那么我们就需要将编码转换为我们所需的编码格式, 今天我就遇到这个问题…

    数据库 2023年6月11日
    081
  • Shell第一章《变量》

    什么是shell shell-‘壳’ 命令解释器,一种应用程序 shell语言特点 SHELL语言是指UNIX操作系统的命令语言,同时又是该命令语言的解释程…

    数据库 2023年6月14日
    082
  • 【JDBC】笔记(5)— 悲观锁和乐观锁的概念;演示行级锁机制

    1.悲观锁和乐观锁的概念: 2.演示行级锁机制: 演示行级锁机制 此程序先开启事务,并使用行级锁锁住”loginName = abc”的那条记录 impor…

    数据库 2023年5月24日
    076
  • leetcode 543. Diameter of Binary Tree 二叉树的直径(简单)

    给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。 示例 :给定二叉树 1 / \ 2 3 / \ …

    数据库 2023年6月16日
    094
  • H3C设备关于开启端口方法及端口显示状态查看方法

    1、通过控制线登录到设备命令行: 2、 备注:1、LINK状态为ADM说明是手动关闭的端口如需要开启需要手动开启 2、LINK状态为DOWN 说明接口没有接线或线没有插好 3、LI…

    数据库 2023年6月9日
    0140
  • MySQL的B+树索引和hash索引的区别

    简述一下索引: 索引是数据库表中一列或多列的值进行排序的一种数据结构;索引分为聚集索引和非聚集索引,聚集索引查询类似书的目录,快速定位查找的数据,非聚集索引查询一般需要再次回表查询…

    数据库 2023年5月24日
    092
  • 重新学习数据库(1)

    单元概述 通过本章的学习能够了解MySQL结构查询语言的概念,掌握SELECT查询语句的基本语法,掌握SELECT查询语句中过滤条件的使用,掌握过滤条件中比较运算符和逻辑运算符的使…

    数据库 2023年6月16日
    0201
  • MySQL 用 limit 为什么会影响性能?

    1.前言 首先说明一下MySQL的版本: mysql> select version(); +———–+ | version() | +———–+…

    数据库 2023年5月24日
    075
  • spring的自动注入

    Spring自动注入 spring的ioc 在刚开始学习spring的时候肯定都知道spring的两个特点:ioc,aop,控制反转和切面编程,这篇就只说说ioc ioc是什么:在…

    数据库 2023年6月16日
    089
  • 推荐几款最好用的MySQL开源客户端,建议收藏!

    一、摘要 众所周知,MYSQL 是目前使得最广泛、最流行的数据库技术之一,为了更方便的管理数据库,市场上出现了大量软件公司和个人开发者研发的客户端工具,比如我们所熟知的比较知名的客…

    数据库 2023年6月14日
    098
  • 设计模式之适配器模式

    一、适配器模式:把两个不匹配的接口通过一个中间层变成可匹配的,这个中间层叫适配器,比如两脚插头+三脚插座无法进行匹配,可使用一个转接头来结合使用,这个转接头就是适配器。 二、实现思…

    数据库 2023年6月14日
    074
  • 设计模式遵循的设计原则

    一、什么是设计原则? 答:如果说设计模式是编写代码的一种套路,那么设计原则就是用来约束我们使用这种套路应该要遵循的规则,只有遵循了这些规则的设计模式编写出来的应用程序才具有更好的扩…

    数据库 2023年6月14日
    084
  • mybatisplus不支持sum,但支持这个

    我们知道,要对数据求和,写sql很简单:select sum(exp) from table_name我们在用mybatisplus做求和计算的时候,mybatisplus的Wra…

    数据库 2023年6月9日
    0235
  • Amazon Aurora解读(SIGMOD 2017)

    Amazon在SIGMOD 2017发表了论文《Amazon Aurora: DesignConsiderations for High Throughput Cloud-Nati…

    数据库 2023年6月9日
    085
  • Go 接口:深入内部原理

    接口的基本概念不在这里赘述,详情请看第十六章:接口 nil 非空? package main func main() { var obj interface{} obj = 1 p…

    数据库 2023年6月6日
    0112
  • 有趣的网络知识

    简单的网络入侵方法 命令 描述 attrib +s +a +h +r 磁盘:文件&#…

    数据库 2023年6月11日
    099
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球