今天聊聊 Mysql 的那些“锁”事!

前言

在 MySQL 的世界里,"锁" 是保证数据并发安全的核心机制。想象一下,当数百个用户同时操作数据库时,有人在查询订单,有人在修改库存,有人在新增用户 ------ 如果没有锁的约束,数据很可能变成一团乱麻:刚查询到的库存还没下单就被别人抢光,修改到一半的订单被另一个操作覆盖,甚至可能出现表结构被意外篡改的情况。

MySQL 的锁机制就像一套精密的交通规则,不同粒度的锁对应不同的 "道路管控级别":全局锁如同全城交通管制,表级锁类似单条道路封闭,行级锁则像针对特定停车位的占用。理解这些锁的特性、使用场景和潜在问题,能帮助我们在 "高并发" 的路况中,既保证数据安全,又避免不必要的性能损耗。

全局锁

全局锁是 MySQL 中粒度最粗的锁,一旦启用,整个数据库实例会进入 "只读模式"。它的核心作用是保证数据的全局一致性,最典型的使用场景是数据库逻辑备份。

如何使用全局锁

通过 FLUSH TABLES WITH READ LOCK; 命令开启全局锁:

sql 复制代码
-- 施加全局锁,数据库进入只读状态
FLUSH TABLES WITH READ LOCK;
-- 执行备份操作(例如使用 mysqldump)
-- mysqldump -u root -p db_name > backup.sql
-- 备份完成后释放锁
UNLOCK TABLES;

开启全局锁是针对于整个 Mysql 示例的,而不仅仅是某个库。

全局锁的影响

加锁后,所有写入操作(INSERT/UPDATE/DELETE)和表结构修改(CREATE/ALTER/DROP)都会被阻塞。例如:

  • 当全局锁生效时,执行 INSERT INTO user (name) VALUES ('test') 会被卡住,直到锁释放。
  • 即使是从库,加锁期间也无法同步主库的 binlog,可能导致主从延迟增大。

适用场景与替代方案

全局锁仅建议在全库逻辑备份时使用(且需确保存储引擎为 MyISAM,因为 InnoDB 可通过事务快照实现一致性备份)。现代实践中(InnoDB),更多使用 --single-transaction 选项替代全局锁,例如:

sql 复制代码
mysqldump --single-transaction -u root -p db_name > backup.sql 

该方式利用 InnoDB 的 MVCC 机制,在不阻塞读写的情况下完成一致性备份。

表级锁

表级锁作用于整张表,虽然粒度比全局锁细,但仍会影响整表的并发操作。这里主要介绍的主要是 InnoDB 下的表级锁,它主要包括表锁、元数据锁(MDL)和意向锁三类。

表锁

表锁是最直接的表级锁,分为读锁(共享锁)和写锁(排他锁),通过 LOCK TABLES 命令手动控制。

表共享读锁(Read Lock)

加解锁命令

sql 复制代码
LOCK TABLES user READ;
UNLOCK TABLES

效果

  • 当前会话:可读取 user 表,执行 INSERT/UPDATE 会报错(ERROR 1099 (HY000): Table 'user' was locked with a READ lock and can't be updated)。
  • 其他会话:可读取 user 表,但写入操作(如 INSERT INTO user (name) VALUES ('a'))会被阻塞,直到锁释放。

表独占写锁(Write Lock)

加锁命令

sql 复制代码
LOCK TABLES user WRITE;

效果

  • 当前会话:可读写 user 表(例如 SELECT * FROM user; 和 UPDATE user SET age=20 WHERE id=1; 均可执行)。
  • 其他会话:无论是 SELECT * FROM user; 还是 INSERT 操作,都会被阻塞。

注意:表锁会锁定当前会话的表操作权限,例如加锁后无法访问未锁定的表(如执行 SELECT * FROM order; 会报错)。

元数据锁(MDL)

元数据锁(Metadata Lock)是 MySQL 5.5 引入的 "隐式锁",无需手动操作,由数据库自动管理。它的核心作用是防止表结构变更(DDL)与数据操作(DML)的冲突,保证读写的正确性。

查看元数据锁:

sql 复制代码
SELECT object_type, object_schema, object_name, lock_type, lock_duration FROM performance_schema.metadata_locks;

MDL 的触发规则

  • 当执行 DML 操作(SELECT/INSERT/UPDATE/DELETE)时,自动加 MDL 共享锁(SHARED_READ/SHARED_WRITE)
  • 当执行 DDL 操作(ALTER/CREATE/DROP)时,自动加 MDL 排他锁(EXCLUSIVE)
  • 规则:共享锁之间兼容(多个 DML 可并行),共享锁与排他锁互斥(DML 未完成时 DDL 会阻塞)。

经典案例:MDL 导致的表阻塞

sql 复制代码
-- 会话1:开启事务并执行查询(加 MDL 共享锁)
START TRANSACTION;
SELECT * FROM user;
-- 注意:未提交事务,MDL 共享锁持续持有
-- 会话2:尝试修改表结构(需要 MDL 排他锁)
ALTER TABLE user ADD COLUMN email VARCHAR(20);
-- 结果:被阻塞,等待会话1释放锁
-- 会话3:执行简单查询(需要 MDL 共享锁)
SELECT * FROM user;
-- 结果:被阻塞!因为会话2的排他锁请求会阻塞后续所有锁请求

解决办法:避免长事务,及时提交或回滚;修改表结构时先检查长事务(可通过 SELECT * FROM information_schema.innodb_trx; 查看)。

实测验证

开启一个事务,分别执行一个 select 和一个 update 语句。

查看元数据锁,结果符合预期,分别加了一个 SHARED_READ 和 SHARED_WRITE 元数据锁。

其他测试结论:

  1. 在事务里执行 DDL 后(事务未提交)是查看不到对应的元数据锁的,是因为 DDL 的隐式提交特性所致,执行完就立马提交了,所以不好看到锁。
  2. 在同一个事务内,如果先执行了 DML 操作上了 元数据共享锁,此时如果没有其他的事务上了元数据锁,是可以执行 DDL 操作的(原来的元数据共享锁会被去掉)。

需要注意的是,当没有显示打开事务执行 sql 时,是会隐式自动提交的,提交后即释放锁,如果是手动管理事务,则是在手动提交后再释放锁。而对于 DDL 操作,就算在事务(未提交)里也会即时自动提交的。

意向锁

对数据库表加表锁时需要判断是否存在冲突的行锁,若存在,是无法上锁的。如果逐行记录检查,效率是非常低的,意向锁就是为了解决该问题而生的。

意向锁是 InnoDB 为了优化表锁与行锁的交互效率 而设计的 "中间层锁"。它的核心作用是:当需要加表级锁时,无需逐行检查行锁状态,只需通过意向锁判断即可。

具体的实现是在上行锁的同时会自动上意向锁,这样在上表锁时只需判断和当前的意向锁是否兼容即可,可以理解为实际上就是一个用于方便判断的标志。

查看意向锁及行锁 sql:

sql 复制代码
SELECT object_schema, object_name, index_name, lock_type, lock_mode, lock_data FROM performance_schema.data_locks;                                                                                                                                                          

意向锁的类型

  • 意向共享锁(IS) :事务打算对表中的某些行加行共享锁(S 锁),加行锁前自动加 IS 锁。
  • 意向排他锁(IX) :事务打算对表中的某些行加行排他锁(X 锁),加行锁前自动加 IX 锁。

交互规则

意向锁之间兼容(IS 与 IX 可共存)。

意向锁与表锁的关系:

  • IS 锁与表读锁(READ)兼容,与表写锁(WRITE)互斥。
  • IX 锁与表读锁(READ)、表写锁(WRITE)均互斥。

示例:意向锁的自动触发

sql 复制代码
-- 执行行共享锁(自动加 IS 锁)
SELECT * FROM user WHERE id=1 LOCK IN SHARE MODE;
-- 执行行排他锁(自动加 IX 锁)
SELECT * FROM user WHERE id=1 FOR UPDATE;

实测验证

开启一个事务,执行 DML 操作。

可以看到,同时加上了排他意向锁以及排他行锁。

行级锁

行级锁是 InnoDB 独有的锁机制,粒度最细,仅锁定单行数据(或行所在的间隙),是高并发场景的 "首选锁"。

InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:

  1. 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持。
  2. 间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。
  3. 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持。

查看意向锁及行锁 sql:

sql 复制代码
SELECT object_schema, object_name, index_name, lock_type, lock_mode, lock_data FROM performance_schema.data_locks;                                                                                                                                                          

行锁

行锁直接作用于表中的行记录,分为共享锁(S 锁)和排他锁(X 锁),需通过特定 SQL 触发。

行共享锁(S 锁)

  • 触发命令:SELECT ... LOCK IN SHARE MODE;
  • 效果:持有 S 锁的事务可读取行,其他事务可加 S 锁(共享读取),但无法加 X 锁(修改会阻塞)。
sql 复制代码
-- 会话1:对 id=1 的行加 S 锁
SELECT * FROM user WHERE id=1 LOCK IN SHARE MODE;
-- 会话2:加 S 锁(兼容)
SELECT * FROM user WHERE id=1 LOCK IN SHARE MODE;
-- 成功执行
-- 会话2:尝试加 X 锁(互斥)
UPDATE user SET name='test' WHERE id=1;
-- 结果:被阻塞,等待会话1释放锁
  • 实测

会话一:

会话二: 执行 UPDATE 时被阻塞住了。

查看行锁情况:两个会话分别加了共享行锁。

行排他锁(X 锁)

  • 触发命令:SELECT ... FOR UPDATE; 或 DML 操作(INSERT/UPDATE/DELETE 自动加 X 锁)。
  • 效果:持有 X 锁的事务可修改行,其他事务的 S 锁和 X 锁请求都会被阻塞。
sql 复制代码
-- 会话1:对 id=1 的行加 X 锁
SELECT * FROM user WHERE id=1 FOR UPDATE;
-- 会话2:尝试加 S 锁
SELECT * FROM user WHERE id=1 LOCK IN SHARE MODE;
-- 结果:被阻塞
-- 会话2:尝试修改
UPDATE user SET name='test' WHERE id=1;
-- 结果:被阻塞

注意:行锁的 "命中条件"

行锁仅对索引命中的行生效。若查询未使用索引,会升级为表锁:

sql 复制代码
-- 假设 user 表的 name 字段无索引
-- 会话1:未使用索引,实际加表锁
SELECT * FROM user WHERE name='test' FOR UPDATE;
-- 会话2:操作其他行也会被阻塞
UPDATE user SET age=20 WHERE id=2;
-- 结果:被阻塞(因表锁导致)

实测

执行一个更新,并且使其未命中索引

查看上锁情况,可以看到整张表的数据都被上了排他锁。

间隙锁

间隙锁(Gap Lock)是 InnoDB 解决 "幻读" 问题的关键锁机制,它锁定的是索引之间的间隙,而非具体行。

幻读与间隙锁的作用

幻读:一个事务中,两次相同查询返回不同数量的行(因其他事务插入新行)。例如:

sql 复制代码
-- 初始数据:id=1,3,5(id 为主键)
-- 会话1:查询 id 在 2-4 之间的行
SELECT * FROM user WHERE id BETWEEN 2 AND 4;
-- 结果:仅 id=3 的行
-- 会话2:插入新行
INSERT INTO user (id) VALUES (2);
-- 会话1:再次查询
SELECT * FROM user WHERE id BETWEEN 2 AND 4;
-- 结果:id=2,3 → 出现"幻读"

间隙锁通过锁定插入区间解决幻读:

sql 复制代码
-- 会话1:加锁查询(触发间隙锁)
SELECT * FROM user WHERE id BETWEEN 2 AND 4 FOR UPDATE;
-- 锁定间隙:(1,3) 和 (3,5)
-- 会话2:尝试插入间隙内的行
INSERT INTO user (id) VALUES (2); -- 落在 (1,3) 间隙 → 被阻塞
INSERT INTO user (id) VALUES (4); -- 落在 (3,5) 间隙 → 被阻塞
INSERT INTO user (id) VALUES (6); -- 不在间隙内 → 可正常执行

间隙锁的特性

  • 仅在 REPEATABLE READ 隔离级别下生效(MySQL 默认级别)。
  • 间隙锁之间兼容(多个事务可同时锁定同一间隙)。
  • 锁定范围基于索引值,例如索引存在 1,3,5 时,间隙包括 (-∞,1)、(1,3)、(3,5)、(5,+∞)。

拓展

Mysql 在​​可重复读(REPEATABLE READ)隔离级别​​ 下有 MVCC(多版本并发控制)和间隙锁(Gap Lock)机制,为什么还会有幻读问题呢?

虽然通过MVCC(多版本并发控制)和间隙锁(Gap Lock)机制在大多数场景下避免了幻读问题,但​​仍存在特定场景下的幻读风险​

  1. ​快照读与当前读混用​

    • 若事务先快照读,其他事务插入数据后,当前事务再执行当前读(如UPDATE),可能读到新数据并修改,后续查询会出现幻读。

      • 场景 :事务A快照读未查到id=5,事务B插入id=5并提交;事务A执行UPDATE id=5后,再次查询会看到该记录。
  2. ​未加锁的范围查询​

    • 普通SELECT(快照读)不锁定间隙,若其他事务在查询范围外插入数据,后续当前读可能因条件匹配而读到新数据。
  3. ​复杂查询条件​

    • 当查询条件涉及多列或非索引字段时,间隙锁可能无法完全覆盖所有潜在插入位置,导致幻读

临键锁(Next-Key Lock)

临键锁是行锁 + 间隙锁的组合,它既锁定索引记录本身,又锁定该记录前面的间隙。在 InnoDB 中,行级锁的默认加锁方式就是临键锁(除了唯一索引的等值查询)。

注意:下面用于实测的表数据如下:

InnoDB 在 REPEATABLE READ 隔离级别下默认采用 ​​next-key 锁​​(临键锁),其锁机制特性如下:

  1. ​唯一索引优化​

    当通过唯一索引进行等值查询(如 WHERE id = 1)且记录存在时,next-key 锁会退化为 ​​行锁(Record Lock)​​,仅锁定目标行,提高并发性能。

  2. ​非索引检索的锁升级​

    InnoDB 的行锁依赖于索引。若查询条件未使用索引(如全表扫描 WHERE name = 'xxx'name 无索引),则所有记录会被加锁,​​退化为表锁​​(红色高亮部分强调此风险)。

  3. ​唯一索引的等值查询​

    • 当对不存在的记录加锁时(例如 WHERE id = 100id=100 不存在),next-key 锁会 ​优化为间隙锁​,仅锁定目标值所在的间隙,而非临键锁。

    实测:

    尝试更新一条不存在的记录。 可以看到,加了一个间隙锁,锁住了 id 在 24 到前一条记录 id 值之前的间隙(不包含 24),此时无法向该间隙插入记录。

  4. ​普通索引的等值查询​

    • 当向右遍历索引时,如果最后一个值不满足查询条件(例如 WHERE name = 'Bob' 但后续记录是 'Charlie'),next-key 锁会 ​退化为间隙锁​,仅锁定符合条件的范围间隙。

    实测:

    给 high_context 字段创建了普通索引,并且在事务中执行以下 select 语句。

    可以看到一共上了 3 个行级锁,其中一个临键锁,锁住目标记录及其前面的间隙,一个行锁,锁住目标记录,一个间隙锁,锁住目标记录(不包含)至下一个记录(不包含)之间的间隙。之所以需要这样,是因为对于普通索引而言,索引字段值是可以重复的,例如在 high_context=7 的记录后,可以再插入 high_context=7 的记录,所以就需要把前后的间隙给锁了,防止幻读。

  5. ​唯一索引的范围查询​

    • 在范围查询(如 WHERE id BETWEEN 10 AND 20)时,InnoDB 会扫描索引,​直到遇到第一个不满足条件的值为止​,并对扫描范围内的记录施加 next-key 锁。

    实测:

    执行范围查询 id >= 24

    可以看到加了一个行锁,用于锁住 24 记录本身,此外加了两个临键锁,分别用于锁定间隙 (24, 28] 和 (28, +∞]。

临键锁的锁定范围

以索引值 1,3,5 为例,临键锁的锁定区间为左开右闭:

  • 对 id=3 加临键锁 → 锁定 (1,3]
  • 对 id>3 加临键锁 → 锁定 (3,5]

示例:临键锁的实际效果

sql 复制代码
-- 会话1:查询 id>=3 的行(触发临键锁)
SELECT * FROM user WHERE id >=3 FOR UPDATE;
-- 锁定范围:(1,3]、(3,5](即覆盖 id>=3 的所有可能插入位置)
-- 会话2:插入数据
INSERT INTO user (id) VALUES (2); -- 落在 (1,3] → 被阻塞
INSERT INTO user (id) VALUES (3); -- 行锁生效 → 被阻塞
INSERT INTO user (id) VALUES (4); -- 落在 (3,5] → 被阻塞
INSERT INTO user (id) VALUES (6); -- 落在 (5,+∞) → 可执行

特殊情况:唯一索引的等值查询

当查询唯一索引(如主键)的等值条件时,临键锁会退化为行锁:

sql 复制代码
-- id 是主键(唯一索引)
-- 会话1:查询 id=3(唯一等值)
SELECT * FROM user WHERE id=3 FOR UPDATE;
-- 仅加行锁(不锁间隙)
-- 会话2:插入 id=2(落在 (1,3) 间隙)
INSERT INTO user (id) VALUES (2); -- 可正常执行(无间隙锁阻塞)

这是因为唯一索引能精确定位行,无需通过间隙锁防止幻读。

总结

MySQL 中的锁机制是保障数据一致性、完整性和并发性能的核心要素。从全局级别的表锁到粒度更细的行锁,再到特殊场景下的页锁,每种锁类型都有其独特的适用场景与性能特点。表锁操作简单但并发性能受限,行锁能最大程度提升并发效率却存在加锁开销,而页锁则介于两者之间。事务隔离级别与锁模式的配合使用,进一步影响着数据的读取与写入行为。在实际应用中,我们需根据业务场景合理选择锁策略,权衡并发性能与数据一致性,同时通过监控和调优手段避免死锁、锁等待等问题,从而充分发挥 MySQL 在高并发场景下的稳定性能。

优质项目推荐

推荐一个可用于练手、毕业设计参考、增加简历亮点的项目。

lemon-puls/txing-oj-backend: Txing 在线编程学习平台,集在线做题、编程竞赛、即时通讯、文章创作、视频教程、技术论坛为一体

相关推荐
超浪的晨21 分钟前
Java UDP 通信详解:从基础到实战,彻底掌握无连接网络编程
java·开发语言·后端·学习·个人开发
AntBlack28 分钟前
从小不学好 ,影刀 + ddddocr 实现图片验证码认证自动化
后端·python·计算机视觉
Pomelo_刘金1 小时前
Clean Architecture 整洁架构:借一只闹钟讲明白「整洁架构」的来龙去脉
后端·架构·rust
双力臂4041 小时前
Spring Boot 单元测试进阶:JUnit5 + Mock测试与切片测试实战及覆盖率报告生成
java·spring boot·后端·单元测试
水瓶_bxt1 小时前
Centos安装HAProxy搭建Mysql高可用集群负载均衡
mysql·centos·负载均衡
♡喜欢做梦1 小时前
【MySQL】深入浅出事务:保证数据一致性的核心武器
数据库·mysql
遇见你的雩风1 小时前
MySQL的认识与基本操作
数据库·mysql
dblens 数据库管理和开发工具1 小时前
MySQL新增字段DDL:锁表全解析、避坑指南与实战案例
数据库·mysql·dblens·dblens mysql·数据库连接管理
weixin_419658311 小时前
MySQL的基础操作
数据库·mysql
midsummer_woo3 小时前
基于spring boot的医院挂号就诊系统(源码+论文)
java·spring boot·后端