
前言
在 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 元数据锁。
其他测试结论:
- 在事务里执行 DDL 后(事务未提交)是查看不到对应的元数据锁的,是因为 DDL 的隐式提交特性所致,执行完就立马提交了,所以不好看到锁。
- 在同一个事务内,如果先执行了 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的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:
- 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持。
- 间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。
- 临键锁(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)机制在大多数场景下避免了幻读问题,但仍存在特定场景下的幻读风险:
-
快照读与当前读混用
-
若事务先快照读,其他事务插入数据后,当前事务再执行当前读(如
UPDATE
),可能读到新数据并修改,后续查询会出现幻读。- 场景 :事务A快照读未查到
id=5
,事务B插入id=5
并提交;事务A执行UPDATE id=5
后,再次查询会看到该记录。
- 场景 :事务A快照读未查到
-
-
未加锁的范围查询
- 普通
SELECT
(快照读)不锁定间隙,若其他事务在查询范围外插入数据,后续当前读可能因条件匹配而读到新数据。
- 普通
-
复杂查询条件
- 当查询条件涉及多列或非索引字段时,间隙锁可能无法完全覆盖所有潜在插入位置,导致幻读
临键锁(Next-Key Lock)
临键锁是行锁 + 间隙锁的组合,它既锁定索引记录本身,又锁定该记录前面的间隙。在 InnoDB 中,行级锁的默认加锁方式就是临键锁(除了唯一索引的等值查询)。
注意:下面用于实测的表数据如下:

InnoDB 在 REPEATABLE READ
隔离级别下默认采用 next-key 锁(临键锁),其锁机制特性如下:
-
唯一索引优化
当通过唯一索引进行等值查询(如
WHERE id = 1
)且记录存在时,next-key 锁会退化为 行锁(Record Lock),仅锁定目标行,提高并发性能。 -
非索引检索的锁升级
InnoDB 的行锁依赖于索引。若查询条件未使用索引(如全表扫描
WHERE name = 'xxx'
且name
无索引),则所有记录会被加锁,退化为表锁(红色高亮部分强调此风险)。 -
唯一索引的等值查询
- 当对不存在的记录加锁时(例如
WHERE id = 100
但id=100
不存在),next-key 锁会 优化为间隙锁,仅锁定目标值所在的间隙,而非临键锁。
实测:
尝试更新一条不存在的记录。
可以看到,加了一个间隙锁,锁住了 id 在 24 到前一条记录 id 值之前的间隙(不包含 24),此时无法向该间隙插入记录。
- 当对不存在的记录加锁时(例如
-
普通索引的等值查询
- 当向右遍历索引时,如果最后一个值不满足查询条件(例如
WHERE name = 'Bob'
但后续记录是'Charlie'
),next-key 锁会 退化为间隙锁,仅锁定符合条件的范围间隙。
实测:
给 high_context 字段创建了普通索引,并且在事务中执行以下 select 语句。
可以看到一共上了 3 个行级锁,其中一个临键锁,锁住目标记录及其前面的间隙,一个行锁,锁住目标记录,一个间隙锁,锁住目标记录(不包含)至下一个记录(不包含)之间的间隙。之所以需要这样,是因为对于普通索引而言,索引字段值是可以重复的,例如在 high_context=7 的记录后,可以再插入 high_context=7 的记录,所以就需要把前后的间隙给锁了,防止幻读。
- 当向右遍历索引时,如果最后一个值不满足查询条件(例如
-
唯一索引的范围查询
- 在范围查询(如
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 在线编程学习平台,集在线做题、编程竞赛、即时通讯、文章创作、视频教程、技术论坛为一体