专栏文章
专栏文章
MySQL 系列
1. MySQL 系列 #01:MySQL 简介 2. MySQL 系列 #02:MySQL 数据类型与 Java 类型映射 3. MySQL 系列 #03:MySQL 基础架构与执行流程 4. MySQL 系列 #04:MySQL InnoDB 日志系统 5. MySQL 系列 #05:MySQL 事务与 MVCC 6. MySQL 系列 #06:MySQL 索引原理与优化 7. MySQL 系列 #07:MySQL 锁机制 8. MySQL 系列 #08:MySQL 性能问题排查 9. MySQL 系列 #09:MySQL 主备复制与高可用 10. MySQL 系列 #10:MySQL 实战技巧与常见陷阱 11. MySQL 系列 #11:MySQL 数据库设计规范 12. MySQL 系列 #12:MySQL SQL 函数与查询技巧 13. MySQL 系列 #13:MySQL InnoDB Buffer Pool 原理 14. MySQL 系列 #14:MySQL 排序与聚合原理

MySQL 系列 #07:MySQL 锁机制

发布于 2026-05-26 10:33 👁 10 次阅读
#mysql#innodb#lock#gap-lock#deadlock

系统梳理 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、DELETE MDL 读锁(共享锁)
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)

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

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

-- ❌ 不好:账户余额更新在最前,行锁持有时间长
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 间隙锁(不锁记录本身)
X Next-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         |
+--------+------+---------+-----------+---------------+-------------+-----------+

解读:

加锁范围:仅 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)

暂无评论,来留下第一条吧。

发表评论