字节二面:说一下你对MySQL加锁的理解?

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 昨天在群里看到大家在讨论一个 MySQL 锁的问题,就是执行 select ... for update 语句,如果查询条件没有索引字段的话,是加「行锁」还是加「表锁」?

昨天在群里看到大家在讨论一个 MySQL 锁的问题,就是执行 select ... for update 语句,如果查询条件没有索引字段的话,是加「行锁」还是加「表锁」?

网络异常,图片无法展示
|

如果你做过这个实验的话,你会发现执行 select ... for update 语句的时候,如果查询条件没有索引字段的话,整张表都无法进行增删改了,从这个现象看,好像是把表锁起来了,那难道是因为表锁的原因吗?

先不着急说结论。

MySQL 有提供分析数据表加了什么锁的命令,我们就通过这种方式来看看具体加的是什么锁,才导致整张表都无法进行增删改了。

做好准备

为了方便后续故事的展开,先创建一张 t_user 表。

表里有一个主键索引(id 字段),其他字段都不是索引字段,而是普通字段,表里面有下面这三条记录。

网络异常,图片无法展示
|

一条 select 语句会加什么锁?

不知道大家有没有好奇过,执行一条 select 查询语句会加什么锁呢?

相信大家都知道普通的 select 查询(快照读)语句是不会加行级锁(Innodb 层的锁),因为它是通过 MVCC 技术实现的无锁查询。

要验证这个结论也很简单,在 MySQL 8.0 以上的版本,可以执行 select * from performance_schema.data_locks\G; 这条语句,查看 Innodb 存储引擎为事务加了什么锁。

假设事务 a 执行了这条普通 select 的查询语句:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_user where age < 20;
+----+--------+-----+------------+
| id | name   | age | reward     |
+----+--------+-----+------------+
|  1 | 路飞   |  19 | 3000000000 |
+----+--------+-----+------------+
1 row in set (0.00 sec)
mysql>

select * from performance_schema.data_locks\G; 这条语句,输出结果如下:

网络异常,图片无法展示
|

可以看到,输出结果是空,说明普通 select 的查询语句, Innodb 存储引擎不会为事务加任何锁

那难道什么锁都不加吗?

当然不是的。

当我们对数据库表进行 DML 和 DDL 操作的时候,MySQL 会给这个表加上 MDL 锁,即元数据锁,MDL 锁是 server 层实现的表级锁,适用于所有存储引擎。

  • 对一张表进行增删查改操作(DML 操作)的时候,加的是 MDL 读锁
  • 对一张表进行表结构变更操作(DDL 操作)的时候,加的是 MDL 写锁

之所以需要 MDL 锁,就是因为事务执行的时候,不能发生表结构的改变,否则就会导致同一个事务中,出现混乱的现象,如果当前有事务持有 MDL 读锁,DDL 操作就不能申请 MDL 写锁,从而保证表元数据的数据一致性

MDL 的读锁与写锁满足读读共享,读写互斥,写写互斥的关系,比如:

  • 读读共享:MDL 读锁和 MDL 读锁之间不会产生阻塞,就是说增删改查不会因为 MDL 读锁产生而阻塞,可以并发执行,如果不是这样,数据库就是串行操作了;
  • 读写互斥:MDL 读锁和 MDL 写锁之间相互阻塞,即同一个表上的 DML 和 DDL 之间互相阻塞;
  • 写写互斥:MDL 写锁和 MDL 写锁之间互相阻塞,即两个 session 不能同时对一张表结构做变更操作,需要串行操作;

如果在工作中,发现很多会话执行的 SQL 提示”Waiting for table metadata lock”的等待,这时候就是因为 MDL 的读锁与写锁发生冲突了,如果要应急解决问题,这时候就要考虑 kill 掉持有 MDL 锁的事务了,因为 MDL 锁是在事务提交后才会释放,这意味着事务执行期间,MDL 锁是一直持有的

如何查看事务是否持有 MDL 锁?

在前面,我们的事物 A 执行了普通 select 查询语句,如果要看该事务持有的 MDL 锁,可以通过这条命令 select * from performance_schema.metadata_locks;

网络异常,图片无法展示
|

可以看到,事务 A 此时持有一个表级别的 MDL 锁,锁的类型是 SHARED_READ,也就是 MDL 读锁。

对于,增删改操作,申请的 MDL 锁的类型是 SHARED_WRITE,它也属于 MDL 读锁,因为 SHARED_WRITE 与 SHARED_READ 这两个锁的类型是相互兼容的。

因此,我们常说的普通查询不加锁,其实指的是不加 Innodb 的行级锁,但实际上是需要持有 MDL 锁的

一条 select ... for update 会加什么锁?

select ... for update 语句属于锁定读语句,它会对表的记录加 X 型的行级锁。

不同隔离级别下,行级锁的种类是不同的。

在读已提交隔离级别下,行级锁的种类只有记录锁,也就是仅仅把一条记录锁上。

在可重复读隔离级别下,行级锁的种类除了有记录锁,还有间隙锁(目的是为了避免幻读),所以行级锁的种类主要有三类:

  • Record Lock,记录锁,也就是仅仅把一条记录锁上;
  • Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;
  • Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

行级锁加锁规则比较复杂,不同的场景,加锁的形式是不同的。

加锁的对象是索引,加锁的基本单位是 next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock 是前开后闭区间,而间隙锁是前开后开区间。

但是,next-key lock 在一些场景下会退化成记录锁或间隙锁。

那到底是什么场景呢?总结一句,在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁。之前也写过一篇详细介绍了不同的场景下, 行级锁的加锁规则,参见:保姆级教程!2 万字 + 30 张图搞懂 MySQL 是怎么加行级锁的?

这次我们只讨论,执行 select ... for update 语句,如果查询条件没有索引字段的话,会加什么锁?

现在假设事务 A 执行了下面这条语句,查询条件中 age 不是索引字段

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_user where age < 20 for update;
+----+-----------+-----+------------+
| id | name      | age | reward     |
+----+-----------+-----+------------+
|  1 |  路飞   |  19 | 3000000000 |
+----+-----------+-----+------------+
1 rows in set (0.00 sec)

这时候有其他事务对这张表进行增删改,都会发生阻塞。

网络异常,图片无法展示
|

先来看看,事务 A 持有什么类型的 MDL 锁?

可以执行 select * from performance_schema.metadata_locks\G; 这条语句,查看事务 A 此时持有了有什么类型的 MDL 锁。

执行结果如下:

网络异常,图片无法展示
|

可以看到,事务 A 此时持有一个表级别的 MDL 锁,锁的类型是 SHARED_WRITE,属于 MDL 读锁。

而在前面我提到过,当事务对表进行增删查改操作的时候,事务会申请 MDL 读锁,而 MDL 读锁之间是相互兼容的

所以,当事务 A 执行了查询条件没有索引字段的 select ... for update 语句后,不可能是因为事务 A 持 MDL 读锁,才导致其他事务无法进行增删改操作

再来看看,事务 A 持有哪些行级锁?

可以执行 select * from performance_schema.data_locks\G; 这条语句,查看事务 A 此时持有了哪些行级锁。

输出结果如下,我删减了不必要的信息:

网络异常,图片无法展示
|

从上图可以看到,共加了两种类型的锁,分别是:

  • 1 个表级锁:X 类型的意向锁(表级别的锁);
  • 4 个行级锁:X 类型的行级锁;

什么是意向锁?

在 InnoDB 存引擎中,当事务执行锁定读、插入、更新、删除操作后,需要先对表加上「意向锁」,然后再对记录加「行级锁」。

之所以要设计「意向锁」,目的是为了快速判断表里是否有行级锁,具体的说明参见:MySQL 全局锁、表级锁、行级锁,你搞清楚了吗?

意向锁不会和行级锁发生冲突,而且意向锁之间也不会发生冲突,意向锁只会和共享表锁(lock tables ... read)和独占表锁(lock tables ... write)发生冲突

所以,当事务 A 执行了查询条件没有索引字段的 select ... for update 语句后,不可能是因为事务 A 持有了意向锁,才导致其他事务无法进行增删改操作

具体是哪 4 个行级锁?

图中 LOCK_TYPE 中的 RECORD 表示行级锁,而不是记录锁的意思:

  • 如果 LOCK_MODE 为 X,说明是 X 型的 next-key 锁;
  • 如果 LOCK_MODE 为 X, REC_NOT_GAP,说明是 X 型的记录锁;
  • 如果 LOCK_MODE 为 X, GAP,说明是 X 型的间隙锁;

然后通过 LOCK_DATA 信息,可以确认 next-key 锁的范围,具体怎么确定呢?

根据我的经验,如果 LOCK_MODE 是 next-key 锁或者间隙锁,那么 LOCK_DATA 就表示锁的范围最右值,而锁范围的最左值为 LOCK_DATA 的上一条记录的值。

因此,此时事务 A 在主键索引(INDEX_NAME : PRIMARY)上加了 4 个 next-key 锁,如下:

  • X 型的 next-key 锁,范围:(-∞, 1]
  • X 型的 next-key 锁,范围:(1, 2]
  • X 型的 next-key 锁,范围:(2, 3]
  • X 型的 next-key 锁,范围:(3, +∞]

这相当于把整个表给锁住了,其他事务在对该表进行增、删、改操作的时候 都会被阻塞。只有在事务 A 提交了事务,事务 A 执行过程中产生的锁才会被释放。

为什么因为事务 A 对表所有记录加了 X 型的 next-key 锁后,其他事务就无法进行增、删、改操作了呢?

其他事务在执行「删除或者更新操作」的时候,也会申请 X 型的 next-key 锁,next-key 锁是包含记录锁和间隙锁的,间隙锁之间虽然是相互兼容的,但是记录锁之间存在 X 型和 S 型的关系,即读读共享、读写互斥、写写互斥的关系。

所以当事务 A 持有了 X 型的 next-key 锁后,其他事务就无法申请 X 型的 next-key 锁,从而发生阻塞。

比如,前面的例子,事务 B 在更新 id = 1 的记录的时候,它会申请 X 型的记录锁(唯一索引等值操作, next-key 锁会退化为记录锁),但是因为事务 A 持有了 X 型的 next-key 锁,所以事务 B 在申请 X 型的记录锁的时候,会发生阻塞。

我们也可以通过 select * from performance_schema.data_locks\G; 这条语句得知。

网络异常,图片无法展示
|

事务 C 的删除操作被阻塞的原因,也是这个原因。

事务 D 的插入操作被阻塞的原因,跟事务 B 和事务 C 的原因不同。

插入语句在插入一条记录之前,需要先定位到该记录在 B+树 的位置,如果插入的位置的下一条记录的索引上有间隙锁,如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态,现象就是插入语句会被阻塞

事务 D 插入了一条 id = 10 的新记录,在主键索引树上定位到插入的位置,而该位置的下一条记录是 supremum pseudo-record,该记录是一个特殊的记录,用来标识最后一条记录,而该特殊记录上正好持有了间隙锁(next-key 锁包含间隙锁),所以这条插入语句会发生阻塞。

我们也可以通过 select * from performance_schema.data_locks\G; 这条语句得知。

网络异常,图片无法展示
|

为什么只是查询年龄 20 岁以下的行记录,而把整个表给锁住了呢?

这是因为事务 A 的这条锁定读查询语句,没有使用索引列作为查询条件,所以扫描的方式是全表扫描,行级锁是在遍历索引的时候加上的,并不是针对输出的结果加行级锁

不只是锁定读查询语句不加索引才会导致这种情况,update 和 delete 语句如果查询条件不加索引,那么由于扫描的方式是全表扫描,于是就会对每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表。

因此,在线上在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题

如果数据量很大,还是一样的原因吗?

前面我们结论得出,如果如果锁定读查询语句,没有使用索引列作为查询条件,导致扫描是全表扫描。那么,每一条记录的索引上都会加 X 型的 next-key 锁(行级锁)。正是因为这个原因,才导致其他事务,无法对该表进行增删改操作。

那如果一张表的数据量超过几百万行,还是一样对每一条记录的索引上都会加 X 型的 next-key 锁吗?

群里有小伙伴提出了这个说法,说如果 MySQL 认为数据量太大时,自动将行所升级到表锁。

网络异常,图片无法展示
|

不着急说结论,我们直接做个实验。

我在 t_user 表插入了 300 多万条数据。

网络异常,图片无法展示
|

现在有个事务执行了这条查询语句,查询条件 age 字段不是索引字段。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_user where age < 20 for update;

然后,我们执行 select * from performance_schema.data_locks\G; 这条语句(我执行了好长时间,至少有几十分钟)。

网络异常,图片无法展示
|

可以看到,每一条记录的索引上都会加 X 型的 next-key 锁(行级锁)。

所以,MySQL 认为数据量太大时,自动将行所升级到表锁 ,这句话并不准确

总结

在执行 select … for update 语句的时候,会有产生 2 个表级别的锁:

  • 一个是 Server 层表级别的锁:MDL 锁。事务在进行增删查改的时候,server 层申请 MDL 锁都是 MDL 读锁,而 MDL 读锁之间是相互兼容的,MDL 读锁只会和 MDL 写锁发生冲突,在对表结构进行变更操作的时候,才会申请 MDL 写锁。
  • 一个是 Inoodb 层表级别的锁:意向锁。事务在进行增删改和锁定读的时候,inoodb 层会申请意向锁,意向锁不会和行级锁发生冲突,而且意向锁之间也不会发生冲突,意向锁只会和共享表锁(lock tables ... read)和独占表锁(lock tables ... write)发生冲突。

如果 select … for update 语句的查询条件没有索引字段的话,整张表都无法进行增删改了,从这个现象看,好像是把表锁起来了,但是并不是因为上面这两个表级锁的原因

而是因为如果锁定读查询语句,没有使用索引列作为查询条件,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁(行级锁),这样就相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
SQL 关系型数据库 MySQL
MySQL 锁
MySQL里常见的几种锁
73 3
|
13天前
|
存储 关系型数据库 MySQL
MySQL进阶突击系列(06)MySQL有几种锁?| 别背答案,现场演示一下
本文详细解析了MySQL InnoDB存储引擎的锁机制,涵盖读锁、写锁、意向锁、记录锁、间隙锁和临键锁等8种锁类型。重点探讨了不同锁类型的加锁与释放方式,以及事务并发场景下的实战验证。通过具体示例,展示了在不同情况下锁的行为及其对事务的影响。文章还特别强调了锁的作用范围主要是索引,并解释了锁如何影响数据的读写操作。最后总结了并发事务中加锁规则,帮助读者深入理解MySQL的锁机制。
|
4月前
|
存储 关系型数据库 MySQL
优化 MySQL 的锁机制以提高并发性能
【10月更文挑战第16天】优化 MySQL 锁机制需要综合考虑多个因素,根据具体的应用场景和需求进行针对性的调整。通过不断地优化和改进,可以提高数据库的并发性能,提升系统的整体效率。
230 1
|
4月前
|
关系型数据库 MySQL Java
MySQL数据锁:Record Lock,Gap Lock 和 Next-Key Lock
本文基于 MySQL 8.0.30 版本及 InnoDB 引擎,深入解析三种行锁机制:记录锁(Record Lock)、间隙锁(Gap Lock)和临键锁(Next-key Lock)。记录锁锁定索引记录,确保事务唯一修改;间隙锁锁定索引间的间隙,防止新记录插入;临键锁结合两者,锁定范围并记录自身,有效避免幻读现象。通过具体示例展示了不同锁的作用机制及其在并发控制中的应用。
460 2
|
4月前
|
存储 关系型数据库 MySQL
MySQL数据库锁:共享锁和独占锁
本文详细介绍了`InnoDB`存储引擎中的两种行级别锁:共享锁(S锁)与排他锁(X锁)。通过具体示例展示了这两种锁的工作机制及其在`InnoDB`与`MyISAM`引擎中的表现差异。文章还提供了锁的兼容性矩阵,帮助读者更好地理解锁之间的互斥关系。最后总结了两种锁的特点及适用场景。适合希望深入了解`MySQL`并发控制机制的读者阅读。
156 1
|
5月前
|
监控 关系型数据库 MySQL
MySQL锁机制与解决死锁问题
MySQL锁机制与解决死锁问题
401 5
|
4月前
|
存储 关系型数据库 MySQL
MySQL锁,锁的到底是什么?
【10月更文挑战第16天】MySQL 锁锁定的是与数据和资源相关的对象,其目的是为了保证数据的一致性、避免冲突,并在并发环境下合理协调事务或操作的执行。理解锁的对象和意义对于优化数据库性能、处理并发问题至关重要。
141 0
|
4月前
|
关系型数据库 MySQL 数据库
mysql锁详解
通过理解并合理运用MySQL中的锁机制,开发者可以有效管理数据库并发访问,平衡性能与数据一致性需求。更多关于MySQL锁的深入探讨和最佳实践,请参考专业的数据库管理资源[[深入MySQL锁机制详解
92 0
|
5月前
|
关系型数据库 MySQL 数据库
Mysql的锁
本文介绍了MySQL中表级锁和行级锁的区别,其中MyISAM仅支持表级锁,而InnoDB支持表级锁和行级锁,默认为行级锁。表级锁锁定整个表,实现简单,资源消耗少,但并发度低;行级锁仅锁定相关记录,减少冲突,提高并发度,但加锁开销大。此外,还介绍了共享锁和排他锁的概念及意向锁的作用。
|
5月前
|
存储 SQL 关系型数据库
MySQL 的锁机制,那么多的锁,该怎么区分?
MySQL 的锁机制,那么多的锁,该怎么区分?
60 0