MySQL 锁全景速查手册

MySQL 到底都有哪些锁?本文按三个分类维度给出完整清单,并重点提供三种反查方式——给你一条 SQL、一个隔离级别、一种索引情况,就能查出它会加什么锁。

与《MySQL 锁机制完全解析》(按锁类型逐个详解 + 可视化演示)互补:那篇是「词典」,这篇是「速查手册」。
全局锁 · 表级锁 · 行级锁 共享 S / 排他 X Record / Gap / Next-Key 意向锁 · MDL · AUTO-INC 按 SQL / 隔离级别 / 索引反查
1
全景:三个维度看清所有锁
MySQL 的锁不是一张平铺的清单,而是三个正交维度交叉的结果。

问「MySQL 都有哪些锁」之所以容易答乱,是因为锁可以从三个完全不同的维度分类。同一个锁会同时属于三个维度各一项(比如一次 UPDATE 加的锁 = 行级 + 排他 + 悲观)。

图 1 · MySQL 锁的三个分类维度:粒度(锁多大范围)、模式(读还是写)、思想(悲观还是乐观)
MySQL 锁的三个分类维度 维度一:按粒度(锁定范围) 全局锁 FTWRL · 整个实例只读 表级锁 表锁 LOCK TABLES 元数据锁 MDL 意向锁 IS / IX 行级锁(InnoDB) 记录锁 Record Lock 间隙锁 Gap Lock 临键锁 Next-Key Lock 维度二:按模式(读写) 共享锁 S(读锁) SELECT ... LOCK IN SHARE MODE 多个事务可同时持有 排他锁 X(写锁) UPDATE / DELETE / INSERT SELECT ... FOR UPDATE 兼容规则 S ↔ S 兼容 S ↔ X、X ↔ X 互斥 维度三:按思想(策略) 悲观锁 先加锁再操作 数据库原生支持 SELECT FOR UPDATE 适合冲突多的场景 乐观锁 不加锁,提交时校验 数据库不直接支持 靠版本号 / CAS 实现 适合冲突少的场景 页级锁(BDB 引擎)已随引擎淘汰,现代 MySQL 只需记住全局 / 表 / 行三级
一句话记住全貌:MySQL 的锁 = 粒度(全局 / 表 / 行)× 模式(S / X)× 思想(悲观 / 乐观)。日常开发 90% 打交道的是 InnoDB 行级锁,而它细分为 Record Lock / Gap Lock / Next-Key Lock 三种——其中后两者只在 可重复读(RR) 隔离级别下才登场。
2
全局锁:锁住整个数据库实例
粒度最粗,影响最大,日常几乎只出现在备份场景。
锁怎么用作用与特点
FTWRL
全局读锁
FLUSH TABLES WITH READ LOCK; 整个实例进入只读状态:所有写操作(DML、DDL、甚至事务提交)全部阻塞。典型用途是全库逻辑备份。客户端断开连接后自动释放。
readonly SET GLOBAL readonly=1; 效果类似但不建议用于备份:拥有 SUPER 权限的用户仍能写入;且如果客户端异常断开,数据库会一直保持只读,风险比 FTWRL 大。
一致性快照
(推荐替代)
mysqldump --single-transaction 在 RR 下启动一个事务拿到一致性视图,备份期间不阻塞写入。前提是引擎支持事务(InnoDB 可以,MyISAM 不行——所以 MyISAM 才不得不依赖 FTWRL)。
危险提示:在主库上执行 FLUSH TABLES WITH READ LOCK 会导致整个业务停写。如果备份一定要用,请确保在从库上做,或使用 --single-transaction。这也是为什么「为什么备库备份时主库没事、业务却卡住了」这类故障常常能追溯到一条 FTWRL。
3
表级锁:四类,容易混淆
表锁、MDL、意向锁、AUTO-INC —— 名字都带「表」,但来源和生命周期完全不同。
锁谁加的什么时候加特点
表锁
Table Lock
用户显式执行
或 MyISAM 自动
LOCK TABLES t READ/WRITE InnoDB 一般不用(它有行锁)。MyISAM 的默认并发手段。显式加的表锁用 UNLOCK TABLES 释放,且会隐式提交事务。
MDL
元数据锁
MySQL 自动加
(MySQL 5.5 引入)
访问表就加:
DML → MDL 读锁
DDL → MDL 写锁
不需要显式使用,但最容易踩坑:一个长事务持有 MDL 读锁时,任何 ALTER TABLE 都会被阻塞,后续所有查询又会被这个 DDL 阻塞,形成雪崩。
意向锁
IS / IX
InnoDB 自动加 加行锁之前
先在表上加意向锁
表级锁,但目的只是「打个招呼」:告诉别的事务「这张表里某些行被锁了」。IS/IX 之间互相兼容,只与表级 S/X 锁冲突。
AUTO-INC
自增锁
InnoDB 自动加 插入带 AUTO_INCREMENT 列的表 语句级锁(不是事务级):插入语句结束立即释放,不用等事务提交。MySQL 8.0 默认 innodb_autoinc_lock_mode=2,绝大多数情况下已不再加表级锁。
图 2 · MDL 锁的雪崩效应:一个慢查询 + 一个 DDL = 整张表不可用
MDL 读写锁排队:写锁优先级高,会阻塞后面所有读锁 事务 A(长事务) 会话 B(ALTER TABLE) 会话 C(SELECT) 会话 D(SELECT) 持 MDL 读锁 等 MDL 写锁 等 MDL 读锁 等 MDL 读锁 后果:A 不提交 → B 永远等 → C、D 排在 B 后面全被堵死 → 整张表「假死」,连接数被打满
怎么安全加字段?先查有没有长事务:SELECT * FROM information_schema.innodb_trx;。确认没有后再执行 DDL;或者使用 ALTER TABLE ... WAIT n / NOWAIT(MySQL 8.0)设置等待超时,避免无限堆积。
4
行级锁:InnoDB 的三种 + 一个特例
这是 InnoDB 的核心,也是面试与实战的高频区。
最重要的前置认知:InnoDB 的行锁是加在「索引」上的,不是加在「数据行」上的。 这意味着——如果一条 SQL 没走索引,InnoDB 就会给扫描到的所有索引记录都加锁,效果等同于锁全表。这是「明明只改一行,却把整张表锁死」的根本原因。
锁锁的范围解决什么问题何时出现
记录锁
Record Lock
单条索引记录 防止其它事务修改/删除这一行 唯一索引等值精确命中时;以及 RC 隔离级别下的绝大多数场景
间隙锁
Gap Lock
两条索引记录之间的空隙
(不含记录本身)
防止在空隙里插入新记录 → 解决幻读 只在 RR 下出现;唯一索引等值未命中时也会退化成它
临键锁
Next-Key Lock
记录锁 + 该记录前面的间隙
(左开右闭区间)
既防改又防插,InnoDB 在 RR 下的默认加锁单位 RR 下的范围查询、普通索引等值查询
插入意向锁
Insert Intention
间隙(特殊类型) INSERT 前声明「我要往这个空隙插」 执行 INSERT 时。同一间隙不同位置的插入互不冲突,提升并发
图 3 · 三种行锁在索引上的锁定范围(假设索引上有 5、10、15、20 四个值)
索引值轴:-∞ —— 5 —— 10 —— 15 —— 20 —— +∞ 5 10 15 20 +∞ Record Lock 锁 id=10 这一行 只锁 10 这个点 Gap Lock 锁 (10, 15) 这个空隙 禁止插入 11~14 Next-Key Lock 锁 (10, 15],左开右闭 空隙 + 15 这条记录一起锁 Next-Key Lock = Gap Lock + Record Lock,是 InnoDB 在 RR 下的默认加锁单位
退化规则(面试必考):Next-Key Lock 在以下情况会「退化」成更小的锁——
① 唯一索引上的等值查询,命中记录 → 退化为 Record Lock(因为唯一性已保证不会插入重复值,不需要间隙锁);
② 唯一索引上的等值查询,未命中 → 退化为 Gap Lock;
③ 范围查询向右扫描时,扫到的最后一个不满足条件的记录,其锁退化为 Gap Lock。
5
S 锁与 X 锁:读写互斥的兼容矩阵
模式维度,只有两种,但兼容性是理解阻塞的基础。

共享锁 S(Shared / 读锁)

加锁方式:SELECT ... LOCK IN SHARE MODE(MySQL 8.0 也可用 FOR SHARE)。

多个事务可以同时持有同一行的 S 锁。持有 S 锁时,别的事务能读,但不能加 X 锁(写被阻塞)。

典型用途:需要读出数据后基于它做更新,且要求这期间数据不被别人改(如库存校验)。

排他锁 X(Exclusive / 写锁)

加锁方式:SELECT ... FOR UPDATE、以及 UPDATE / DELETE / INSERT 自动加。

一行上只能有一个 X 锁。持有 X 锁时,别的事务既不能加 S 也不能加 X,读写全被阻塞(普通 SELECT 走 MVCC 快照读除外)。

图 4 · 表级锁兼容矩阵:Y = 兼容,N = 冲突
意向锁只与「整表级」锁冲突,IS 与 IX 之间互相兼容 已持有的锁 → 请求 的锁 X IX S IS X N N N N IX N Y N Y 意向锁设计目的:加表锁时无需逐行检查是否有行锁,看一眼表上的 IS/IX 就知道
6
悲观锁与乐观锁:思想维度
注意:乐观锁不是 MySQL 提供的锁,而是一种应用层实现策略。

悲观锁 数据库原生

假设冲突一定会发生,所以每次操作前先加锁,锁住后再改。

-- 典型写法
BEGIN;
SELECT stock FROM goods
  WHERE id=1 FOR UPDATE;  -- 加 X 锁
UPDATE goods SET stock=stock-1 WHERE id=1;
COMMIT;

适合写多读少、冲突概率高的场景(如扣库存、转账)。代价是加锁开销与阻塞。

乐观锁 应用层实现

假设冲突很少发生,不加锁,提交时用版本号校验是否被别人改过。

-- 表上加 version 列
UPDATE goods
  SET stock=stock-1, version=version+1
WHERE id=1 AND version=5;
-- 判断 affected rows:
--   0 = 被人改过,重试或报错
--   1 = 成功

适合读多写少、冲突概率低的场景。无锁开销,冲突时需重试。

选型口诀:冲突频繁用悲观锁(省掉反复重试的成本);冲突罕见用乐观锁(省掉加锁阻塞的成本)。互联网高并发的读多写少场景,乐观锁往往是更优解。
7
反查一:一条 SQL 会加什么锁
最实用的速查表——看到语句就知道锁不锁、锁什么。
语句加锁情况说明
SELECT * FROM t WHERE ... 不加锁 快照读(一致性读),走 MVCC,读的是历史版本,所以不会被任何写阻塞。唯一的例外是 SERIALIZABLE 隔离级别。
SELECT ... LOCK IN SHARE MODE 共享锁 S 当前读。读最新的已提交数据,并加 S 锁(8.0 起也可用 FOR SHARE)。别的事务可读不可写。
SELECT ... FOR UPDATE 排他锁 X 当前读。加 X 锁,别的事务读写都阻塞。这是悲观锁的标准写法。
UPDATE ...
DELETE ...
排他锁 X 自动加 X 锁(隐式当前读)。先定位记录再上锁,所以定位过程走不走索引,直接决定锁多少行。
INSERT ... 排他锁 X + 插入意向锁 插入成功后对新记录加 X 记录锁;插入前在目标间隙加插入意向锁。若遇唯一键冲突,会加 S 锁去检查重复。
ALTER TABLE ... MDL 写锁 表级。与任何 MDL 读锁(来自 DML)互斥,所以长事务会阻塞 DDL。
核心分水岭:快照读 vs 当前读。
快照读(普通 SELECT):基于 MVCC 读历史快照,不加锁、不阻塞。
当前读(FOR UPDATE、LOCK IN SHARE MODE、UPDATE、DELETE、INSERT):读最新版本并加锁。
理解这一点,就能解释「为什么我 SELECT 看到的是旧值,但 UPDATE 却用的是新值」。
8
反查二:不同隔离级别下锁的差异
隔离级别改变的是「间隙锁是否存在」,这是最本质的区别。
隔离级别间隙锁 / 临键锁并发度说明
读未提交
READ UNCOMMITTED
无最高不加锁也能读到未提交数据(脏读)。生产几乎不用。
读已提交
READ COMMITTED
基本没有高只有记录锁,没有间隙锁(外键检查与重复键检查除外)。并发好,但有幻读。很多互联网公司刻意改用 RC 来规避间隙锁带来的死锁。
可重复读
REPEATABLE READ
MySQL 默认
有中默认加 Next-Key Lock,既防不可重复读又防幻读。代价是锁范围更大、更易死锁。
串行化
SERIALIZABLE
有最低连普通 SELECT 都会加共享 Next-Key Lock,读写全串行。基本不用。
图 5 · 同一条范围查询,在 RC 与 RR 下的加锁差异
UPDATE t SET ... WHERE id BETWEEN 10 AND 15 5 10 15 20 RC 只有记录锁 只锁 10、15 两行 → 别的事务仍可插入 11、12、13 → 幻读 RR 临键锁 锁 (5,10] 与 (10,15] 及后续间隙 → 无法插入 11 → 无幻读
为什么很多公司把默认隔离级别从 RR 改成 RC?因为 RR 下的间隙锁会显著扩大锁定范围,在高并发写入时极易造成死锁和锁等待。改用 RC 后锁粒度最小、并发最高,代价是需要接受幻读(多数业务可容忍,或用唯一索引等约束兜底)。同时 RC 下 binlog 格式必须用 ROW。
9
反查三:索引情况决定锁多少行
面试最高频的加锁分析题,本质是「索引怎么走」。

所有分析都以 RR 隔离级别 + 当前读(如 UPDATE / FOR UPDATE) 为前提。核心规律:索引越精确,锁得越少;没有索引,锁全表。

索引情况查询类型实际加的锁影响范围
主键 / 唯一索引等值,命中 Record Lock 只锁这一行。最理想。
主键 / 唯一索引等值,未命中 Gap Lock 锁住这个值本该落入的间隙,别的事务不能插进来。
普通索引
(非唯一)
等值 Next-Key Lock 锁命中的记录 + 两侧间隙,且向右扫描到第一个不满足条件的记录才停。
任意索引范围查询
(> < BETWEEN)
Next-Key Lock 锁扫描范围内的所有记录及间隙。
无索引
(全表扫描)
任意 全表 Next-Key Lock 给扫描过的每一条记录都加锁,效果等同锁表。这是最危险的情况。
经典事故:UPDATE user SET name='x' WHERE phone='138...',如果 phone 字段没建索引,InnoDB 只能全表扫描,于是每一行都被加上 Next-Key Lock。表现就是:一条简单的 UPDATE 把整张表锁死,所有其它写操作全部超时。
排查要点:先 EXPLAIN 看有没有走索引。
10
交互:查锁判定器
选择三个条件,实时算出会加什么锁。
隔离级别 可重复读 RR 读已提交 RC
索引情况 主键/唯一索引 普通索引 无索引
查询类型 等值·命中 等值·未命中 范围查询
说明:判定器只覆盖最典型的三种索引情况与三类查询,用于建立直觉。真实环境还要叠加优化器是否真的选了这个索引(EXPLAIN 说了算)、是否为覆盖索引、RC 下的 semi-consistent read 等因素。上线前请以 EXPLAIN + 实际观测为准。
11
怎么观测当前有哪些锁
8.0 与 5.7 的系统表不一样,别用错。
观测对象MySQL 8.0MySQL 5.7
当前持有的锁performance_schema.data_locksinformation_schema.INNODB_LOCKS
锁等待关系performance_schema.data_lock_waitsinformation_schema.INNODB_LOCK_WAITS
活跃事务information_schema.INNODB_TRX(两版本通用)
死锁详情SHOW ENGINE INNODB STATUS\G 的 LATEST DETECTED DEADLOCK 段
-- ① 看当前所有锁(8.0)
SELECT engine, object_name, lock_type, lock_mode, lock_status, lock_data
FROM performance_schema.data_locks;

-- ② 看谁在等谁(8.0)
SELECT * FROM performance_schema.data_lock_waits;

-- ③ 看活跃事务(尤其关注运行时间长的)
SELECT trx_id, trx_state, trx_started, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;

-- ④ 看锁等待与阻塞源头(8.0,一条 SQL 定位问题)
SELECT waiting_pid, waiting_query, blocking_pid, blocking_query
FROM sys.innodb_lock_waits;

lock_mode 字段怎么读

SHOW ENGINE INNODB STATUS 里的写法实际含义
lock_mode X locks rec but not gap记录锁(只锁记录,不锁间隙)
lock_mode X临键锁 Next-Key Lock(默认写法)
lock_mode X locks gap before rec间隙锁
lock_mode X locks gap before rec insert intention插入意向锁
lock_mode S共享临键锁
12
死锁:成因、检测与处理
并发写场景下无法完全避免,关键是快速定位与正确重试。
4
死锁四条件
互斥 · 占有且等待 · 不可抢占 · 循环等待
50s
innodb_lock_wait_timeout
锁等待超时默认值(秒)
ON
innodb_deadlock_detect
死锁检测默认开启
最小
回滚策略
回滚影响行数最少的事务
图 6 · 死锁的本质:两个事务以相反顺序请求同一批资源
事务 A 已持有 id=1 的锁 事务 B 已持有 id=2 的锁 A 等待 id=2(被 B 持有) B 等待 id=1(被 A 持有) 循环等待形成 → InnoDB 检测到后回滚其中一个事务(影响行数最少的)

处理与预防

死锁检测的上限(了解即可):等待列表超过 200 个事务,或检测需遍历的锁超过 100 万个时,InnoDB 会直接判定为死锁并回滚当前检查者。所以极端高并发下可能出现「看起来不该死锁却报死锁」的情况。
13
排查命令速查
线上遇到「卡住了」,照着这个顺序查。
-- ① 有没有锁等待?谁等谁?
SELECT * FROM performance_schema.data_lock_waits;      -- 8.0
SELECT * FROM sys.innodb_lock_waits;                    -- 更友好(含 SQL 文本)

-- ② 有没有长事务 / 未提交事务?
SELECT trx_id, trx_state, trx_started,
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS secs,
       trx_query
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 5
ORDER BY secs DESC;

-- ③ 最近一次死锁详情
SHOW ENGINE INNODB STATUS\G   -- 看 LATEST DETECTED DEADLOCK 段

-- ④ 杀掉问题连接(谨慎!先确认业务影响)
KILL <processlist_id>;

-- ⑤ 查看并设置锁等待超时
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
SET SESSION innodb_lock_wait_timeout = 10;

-- ⑥ 查看当前隔离级别
SELECT @@transaction_isolation;
排查口诀:先查 innodb_trx 找长事务 → 再查 data_lock_waits 找阻塞链源头 → 用 EXPLAIN 确认源头 SQL 走没走索引 → 最后才考虑 KILL。先看清楚再动手。
14
速记与常见坑

完整锁清单(10 种)

  1. 全局锁(FTWRL)
  2. 表锁(LOCK TABLES)
  3. 元数据锁 MDL
  4. 意向锁 IS / IX
  5. AUTO-INC 锁
  6. 记录锁 Record Lock
  7. 间隙锁 Gap Lock
  8. 临键锁 Next-Key Lock
  9. 插入意向锁
  10. 页级锁(BDB,已淘汰)

五个常见坑

  • 没索引 = 锁全表:行锁加在索引上,全表扫描就给每行都加锁
  • 长事务阻塞 DDL:MDL 读锁不释放,ALTER 排队,后面全堵
  • 间隙锁引发死锁:RR 下锁范围比想象大,高并发写入易冲突
  • 以为 SELECT 也加锁:普通 SELECT 是快照读,不加锁(SERIALIZABLE 除外)
  • 死锁不重试:应用层必须捕获 1213 并重试

三个关键配置

  • innodb_lock_wait_timeout:默认 50 秒,高并发可调小到 5~10 秒快速失败
  • innodb_deadlock_detect:默认 ON,极高并发可关
  • innodb_autoinc_lock_mode:8.0 默认 2(交错),5.7 是 1

一句话收尾

  • MySQL 的锁 = 粒度(全局 / 表 / 行)× 模式(S / X)× 思想(悲观 / 乐观),共约 10 种。
  • 行锁加在索引上——这是理解一切行锁问题的起点,没索引就等于锁全表。
  • InnoDB 行锁三种:Record Lock 锁点、Gap Lock 锁空隙、Next-Key Lock 锁「空隙 + 点」(RR 下默认)。
  • 间隙锁只存在于 RR(RC 下基本没有),这也是很多公司把隔离级别改成 RC 的原因。
  • 普通 SELECT 不加锁(快照读/MVCC);FOR UPDATE、LOCK IN SHARE MODE、UPDATE、DELETE、INSERT 才是当前读、才加锁。
  • 死锁无法杜绝,只能预防(统一访问顺序、短事务、合理索引)+ 应用层重试。