目录
正在加载目录…
专栏文章
专栏文章
MySQL 系列
1. MySQL:核心概念、架构与使用场景 2. MySQL 数据类型与 Java 类型映射 3. MySQL 基础架构与执行流程 4. MySQL InnoDB 日志系统 5. MySQL 事务与 MVCC 6. MySQL 索引原理与优化 7. MySQL 锁机制 8. MySQL 性能问题排查 9. MySQL 主备复制与高可用 10. MySQL 实战技巧与常见陷阱 11. MySQL 数据库设计规范 12. MySQL SQL 函数与查询技巧 13. MySQL InnoDB Buffer Pool 原理 14. MySQL 排序与聚合原理

MySQL 锁机制

发布于 2026-05-26 10:33 · 最后编辑于 2026-07-31 15:52 · 字数 1,832 👁 91 次阅读

系统梳理 MySQL 锁的分类(全局锁/表锁/行锁)及 InnoDB 加锁规则,重点解析 Next-Key Lock 的锁范围计算,帮助排查死锁和锁等待问题。

目录

章节说明
锁的分类全局锁、表锁、行锁的层级与用途
全局锁FTWRL 的使用场景与替代方案
表锁MDL 锁的隐患与在线 DDL 注意事项
行锁两阶段锁协议,合理安排语句顺序
InnoDB 加锁规则Next-Key Lock 的两条原则四个优化
死锁检测与处理策略

锁的分类

mysql lock types

全局锁

作用

对整个数据库实例加锁,让数据库处于只读状态

-- 加全局读锁
FLUSH TABLES WITH READ LOCK;  -- FTWRL

-- 解锁
UNLOCK TABLES;

主要用途

全量备份时,保证备份数据的一致性(备份期间数据库不能写入)。

更好的替代方案

-- mysqldump 使用 --single-transaction 选项
-- 利用 MVCC 的快照读,备份过程中数据库可以正常写入
mysqldump --single-transaction -h host -u user -p dbname > backup.sql

只适用于支持事务的引擎(InnoDB)。若表中有 MyISAM 引擎,仍需 FTWRL。

表锁

表锁(LOCK TABLES)

LOCK TABLES t1 READ, t2 WRITE;
-- 执行操作
UNLOCK TABLES;

粒度粗,并发性差,InnoDB 引擎下很少直接使用。

元数据锁(MDL,Metadata Lock)

自动加锁,无需显式调用

操作类型MDL 锁类型
SELECT、INSERT、UPDATE、DELETEMDL 读锁(共享锁)
ALTER TABLE(修改表结构)MDL 写锁(排他锁)

读锁之间不互斥,读写锁、写写锁互斥。

MDL 锁的隐患:给表加字段为什么这么难

并发场景:

Session A:SELECT(持有 MDL 读锁)
Session B:SELECT(持有 MDL 读锁)— 与 A 不互斥,成功
Session C:ALTER TABLE ADD COLUMN(等待 MDL 写锁)— 被 A 和 B 阻塞,积压
Session D:SELECT(等待 MDL 读锁)— 被 C 阻塞(写锁请求优先)
...

→ 后续所有读请求都被 C 阻塞,连接池耗尽,数据库雪崩

安全 DDL 步骤

  1. 查询是否有长事务占用 MDL 锁:
SELECT * FROM information_schema.innodb_trx;
  1. 确认无长事务后再执行 DDL;或在 ALTER 时加 WAIT N 超时参数(MariaDB/部分版本支持):
ALTER TABLE t WAIT 5 ADD COLUMN f INT;  -- 等待 5 秒,超时则放弃
  1. 对大表 DDL,优先使用 pt-online-schema-changegh-ost 等在线 DDL 工具。

行锁

两阶段锁协议(2PL)

  • 行锁在需要时才加
  • 行锁在事务结束时(COMMIT 或 ROLLBACK)才释放,不是用完立即释放

优化原则:把最可能导致冲突的行锁操作放到事务最后

-- 场景:转账,需要更新账户余额(高竞争行),还需要插入账单记录(低竞争)

-- ❌ 不好:账户余额更新在最前,行锁持有时间长
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = A;  -- 高竞争
UPDATE account SET balance = balance + 100 WHERE id = B;  -- 高竞争
INSERT INTO bill_record ...;  -- 低竞争
COMMIT;

-- ✅ 更好:低竞争操作先做,高竞争操作最后
BEGIN;
INSERT INTO bill_record ...;  -- 低竞争,先做
UPDATE account SET balance = balance - 100 WHERE id = A;  -- 高竞争
UPDATE account SET balance = balance + 100 WHERE id = B;  -- 高竞争
COMMIT;  -- 立即释放行锁,其他事务等待时间更短

InnoDB 加锁规则

InnoDB 可重复读隔离级别下,默认加 Next-Key Lock(左开右闭区间)。

两条基本原则

  1. InnoDB 加锁的基本单位是 Next-Key Lock
  2. 查找过程中访问到的对象才加锁

四个优化

  1. 索引上的等值查询,命中唯一索引时,Next-Key Lock 退化为行锁
  2. 索引上的等值查询,向右遍历到最后一个不满足等值条件的值时,Next-Key Lock 退化为间隙锁
  3. 唯一索引上的范围查询,会访问到不满足条件的第一个值为止
  4. 索引上的范围查询会锁住整个范围

示例

-- 表:id INT PRIMARY KEY, c INT, d INT, INDEX c(c)
-- 数据:(0,0,0), (5,5,5), (10,10,10), (15,15,15), (20,20,20), (25,25,25)

-- 场景1:等值查询命中(主键唯一索引等值)
SELECT * FROM t WHERE id=5 FOR UPDATE;
-- 加锁:行锁(5),不加间隙锁(唯一索引等值命中,退化为行锁)

-- 场景2:等值查询未命中
SELECT * FROM t WHERE id=7 FOR UPDATE;
-- 访问到 id=10(不满足等值,退化为间隙锁),加锁:间隙锁(5,10)

-- 场景3:普通索引等值查询
SELECT * FROM t WHERE c=5 FOR UPDATE;
-- Next-Key Lock: (0,5],再向右扫描到 c=10(不满足等值),退化为间隙锁(5,10)
-- 总共加锁:(0,5] + (5,10) + 主键行锁(5)

-- 场景4:主键范围查询
SELECT * FROM t WHERE id>=10 AND id<11 FOR UPDATE;
-- id=10:等值命中唯一索引,行锁(10)
-- 继续扫描到 id=15(不满足 id<11),Next-Key Lock(10,15]
-- 总共:行锁(10) + Next-Key Lock(10,15]

插入意向锁

INSERT 在真正插入行之前,先在目标位置的间隙上加一个"插入意向"标记,表示"我打算在这里插入"。属于特殊的间隙锁。

核心特性:同一间隙内的多个插入意向锁互不阻塞

间隙 (5, 10),两个事务同时插入:
  事务 A:INSERT id=6  → 加插入意向锁(5,10)
  事务 B:INSERT id=8  → 加插入意向锁(5,10)
  → A 和 B 不互相阻塞,可并发执行

如果插入意向锁也互斥,同一间隙的并发插入就会全部串行化,性能极差。

与间隙锁的冲突关系

插入意向锁会被已有的间隙锁阻塞

事务 A:SELECT * FROM t WHERE id=7 FOR UPDATE;
  → 加间隙锁(5,10)

事务 B:INSERT INTO t VALUES(6,...);
  → 尝试加插入意向锁(5,10)
  → 被事务 A 的间隙锁阻塞,等待

这正是间隙锁防止幻读的机制——阻止其他事务在锁定范围内插入新行。

已持有 \ 申请间隙锁插入意向锁
间隙锁✅ 相容❌ 阻塞
插入意向锁✅ 相容✅ 相容

间隙锁之间互相不阻塞(都是防御性的),但间隙锁会阻塞插入意向锁。

实验环境

-- 建表
CREATE TABLE t (
  id INT PRIMARY KEY,
  c  INT,
  d  INT,
  INDEX 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);

查看加锁情况(MySQL 8.0+)

-- 查当前事务持有的所有锁
SELECT
  ENGINE_TRANSACTION_ID AS trx_id,
  OBJECT_NAME           AS tbl,
  INDEX_NAME            AS idx,
  LOCK_TYPE,
  LOCK_MODE,   -- X / X,GAP / X,REC_NOT_GAP / X,INSERT_INTENTION
  LOCK_STATUS,
  LOCK_DATA    -- 锁住的键值
FROM performance_schema.data_locks;

-- 查锁等待关系(谁在等谁)
SELECT
  REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx,
  BLOCKING_ENGINE_TRANSACTION_ID   AS blocking_trx
FROM performance_schema.data_lock_waits;

LOCK_MODE 对照:

LOCK_MODE含义
X,REC_NOT_GAP行锁(仅锁记录,不含间隙)
X,GAP间隙锁(不锁记录本身)
XNext-Key Lock(行 + 左侧间隙)
IX表级意向排他锁(加行锁前自动加)
X,INSERT_INTENTION插入意向锁

场景1 实测:等值命中主键

BEGIN;
SELECT * FROM t WHERE id=5 FOR UPDATE;
+--------+------+---------+-----------+---------------+-------------+-----------+
| trx_id | tbl  | idx     | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA |
+--------+------+---------+-----------+---------------+-------------+-----------+
| 292349 | t    | NULL    | TABLE     | IX            | GRANTED     | NULL      |
| 292349 | t    | PRIMARY | RECORD    | X,REC_NOT_GAP | GRANTED     | 5         |
+--------+------+---------+-----------+---------------+-------------+-----------+

解读:

  • IX(表级意向排他锁):InnoDB 内部协议,加行锁前必须先在表上加 IX。其他事务要加全表锁时只需检查表上有无 IX,无需扫描所有行锁。
  • X,REC_NOT_GAP(纯行锁):唯一索引等值命中的退化结果。原本应加 Next-Key Lock (0,5],因为唯一索引保证不存在重复值,间隙不需要保护,直接退化为行锁。
加锁范围:仅 id=5 这一行
(0,5) 和 (5,10) 间隙均未锁 → 其他事务可以 INSERT id=3 或 id=7,不会阻塞

⚠️ 加锁规则较复杂,遇到线上锁等待时优先用 EXPLAIN + performance_schema 分析实际加锁情况。

死锁

死锁示例

-- 事务 A
BEGIN;
UPDATE t SET d=2 WHERE id=1;  -- 加行锁(1)
UPDATE t SET d=2 WHERE id=2;  -- 等待事务 B 释放行锁(2)

-- 事务 B(并发)
BEGIN;
UPDATE t SET d=2 WHERE id=2;  -- 加行锁(2)
UPDATE t SET d=2 WHERE id=1;  -- 等待事务 A 释放行锁(1)
-- 死锁!

死锁检测与处理

-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS;
-- 查看 "LATEST DETECTED DEADLOCK" 部分
参数说明
innodb_deadlock_detect是否开启死锁检测(默认 ON),检测到后回滚代价小的事务
innodb_lock_wait_timeout锁等待超时时间(默认 50 秒),超时后报错返回

减少死锁的策略

  1. 按固定顺序访问表和行(如多个事务都按 id 升序更新)
  2. 缩小事务,减少锁持有时间
  3. 高并发写入场景,考虑降为 RC 隔离级别(去掉间隙锁,死锁概率大降)

参考资料

  • 《MySQL 实战 45 讲》— 第 06 讲:全局锁和表锁
  • 《MySQL 实战 45 讲》— 第 07 讲:行锁功过
  • 《MySQL 实战 45 讲》— 第 21 讲:为什么我只改一行的语句,锁这么多?
  • InnoDB Locking
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论