MySQL 锁体系全解:从 MDL 到间隙锁,一次讲透

文章目录

MySQL 锁体系全解:从 MDL 到间隙锁,一次讲透

你写了 SELECT ... FOR UPDATE,以为只锁了一行。RR 级别下,你可能锁了半张表。线上改表卡死、长事务堵死、没索引的 FOR UPDATE 变成表锁------都是"没搞懂 MySQL 锁体系"吃的亏。


一、先别急着学行锁------MySQL 有三层锁同时存在

你写一条 UPDATE,InnoDB 同时在三个层面上了锁:

复制代码
UPDATE user SET age = 30 WHERE id = 1;

表级:MDL 读锁       → 保结构不变(有人在改数据,别动表结构)
表级:意向独占锁 IX   → 表上打个标记"里面有行被锁了"
行级:行独占锁 X      → 锁住 id=1 这一行

三层锁各管各的,互不干涉。下面一层一层拆。


二、MDL 锁------自动保安,线上改表的头号杀手

MDL(Metadata Lock)全自动,不用你写:

复制代码
MDL 读锁 ← SELECT、INSERT、UPDATE、DELETE(所有 DML,用表但不改结构)
MDL 写锁 ← ALTER、DROP、TRUNCATE、RENAME(所有 DDL,改表结构)

MDL 读锁兼容读锁,MDL 写锁排斥一切。 命名上注意:INSERT/DELETE/UPDATE 虽然是"写数据",但对表结构来说只是"用表",所以拿 MDL 读锁。

最经典的坑:长事务改表,整库卡死

复制代码
事务A(长事务):SELECT * FROM user;  还没提交 → 持有 MDL 读锁

事务B:ALTER TABLE user ADD COLUMN xxx;
        → 需要 MDL 写锁 → 等 A 释放读锁

事务C:SELECT * FROM user;
        → 也需要 MDL 读锁 → 但排在 B 后面 → 也被阻塞
        → 瞬间:这张表的所有读写全堵死

MDL 读锁本来互相兼容------但写锁排队的瞬间,后面的所有读写也跟着排队。锁队列是先到先得,B 排在 C 前面,C 不能插队。

解决方案:改表前先 SELECT 查一下有没有长事务,或者用 LOCK_WAIT_TIMEOUT 让改表别无限等。


三、表锁 vs MDL vs 意向锁------三门神的分工

表锁 MDL 意向锁
谁加的 你手动 LOCK TABLES MySQL 自动加 InnoDB 自动加
触发条件 你写才锁 任何操作都加 DML 写行之前
锁什么 整张表的读写 表结构变更的时机 不锁,只是一个标记
释放 UNLOCK TABLES 事务提交 事务提交
互斥关系 排斥一切读写 读兼容读,写排斥一切 意向之间完全兼容

表锁 = 手动扔核弹 (LOCK TABLES WRITE 把这表全锁了)

MDL = 自动保安 (有 DDL 才打架,DML 之间相安无事)

意向锁 = 门口登记表(有人要锁整表时看一眼:里面有行锁吗?有就等)

意向锁的意义:有人 LOCK TABLES user WRITE 时,不用一行行去查有没有行锁------看一眼表上有没有 IX 标记就行。


四、行级锁------真正决定并发度的核心

InnoDB 有三种行级锁,但不是你显式选哪个,而是同一句 SQL 在 RR 下自动加的组合:

记录锁(Record Lock)

锁住存在的索引记录。所有精确命中的写操作都会加:

sql 复制代码
UPDATE user SET age = 30 WHERE id = 5;        -- 锁 id=5 这一行
DELETE FROM user WHERE id = 5;                -- 锁 id=5
SELECT * FROM user WHERE id = 5 FOR UPDATE;   -- 锁 id=5

间隙锁(Gap Lock)

锁住索引记录之间的空隙 。只在 RR 级别下才加:

复制代码
索引 age:15 → [间隙] → 22 → [间隙] → 25 → [间隙] → 30
            ↑           ↑           ↑           ↑
         值有记录锁    空隙有间隙锁   ...

事务A:SELECT * FROM user WHERE age BETWEEN 20 AND 30 FOR UPDATE;
事务B:INSERT INTO user VALUES(NULL, '王五', 23);
        → 23 落在 22~25 的间隙 → 被间隙锁阻塞 → 等待

间隙锁是 RR 防幻读的真正手段------不让新行插进查询范围

Next-Key Lock(记录锁 + 间隙锁合体)

RR 级别默认就是这个:锁住记录 + 它前面的间隙

sql 复制代码
-- RR 下,即使你只查一行
SELECT * FROM user WHERE id = 10 FOR UPDATE;
→ 加的实际上是 (上一个id, 10] 这个区间
→ 前面的间隙也锁住了
RC RR
记录锁
间隙锁 ✗ 不用 ✓ 用
Next-Key Lock ✓(默认)
锁范围
并发性能

五、为什么 RC 有行锁,还是会发生不可重复读

因为行锁管的是写写互斥,不管读。MVCC 的读走的是另一条路:

复制代码
MVCC 快照读(无锁)        行锁(有锁)
─────────────────         ──────────
SELECT(纯读)             UPDATE
SELECT(纯读)             DELETE  
                          SELECT ... FOR UPDATE

两条线完全独立!

不可重复读的发生路径:

复制代码
事务A:SELECT age → 快照读 age=25(走左边,无锁,完全不管右边在干嘛)
事务B:UPDATE age=30; COMMIT(走右边,拿行锁写完提交)
事务A:SELECT age → RC 新建快照 → age=30(不可重复读!)

行锁管的是"两个写同一行别打架",从来不管"读看到什么数据"。


六、没索引的 FOR UPDATE ≈ 表锁

InnoDB 的行锁加在索引记录上。没索引怎么办?全表扫描,扫到一行锁一行:

sql 复制代码
-- age 没索引
SELECT * FROM user WHERE age > 25 FOR UPDATE;
→ 全表扫描 → 扫到的每一行全加锁 → age=20、15、50 都锁
→ 等效表锁,所有写操作全堵

所以WHERE 条件走索引不只是为了快------更是为了让锁的范围尽可能小


七、长事务的锁是怎么一步步把数据库拖死的

长事务持锁不释放,最可怕的不是死锁,是锁等待传导

复制代码
事务A(10分钟):SELECT ... FOR UPDATE,锁住订单 id=100
事务B:UPDATE 订单 id=100 → 等 A 释放
事务C:UPDATE 订单明细,外键关联 id=100 → 等 B 释放
事务D:INSERT 订单日志,间隙被 A 锁了 → 等 C
...
一排车堵在单行道,头车不动,后面全卡

死锁还好------InnoDB 会主动检测并回滚一个。锁等待超时更隐蔽:业务看到的是"操作超时请重试",用户以为系统挂了。

此外,RR 的间隙锁在高并发下是性能杀手:

sql 复制代码
-- 秒杀:100个用户同时下单
SELECT stock FROM product WHERE id = 100 FOR UPDATE;

RC:只锁 id=100 这一行,同行的 UPDATE 排队,INSERT 不受影响
RR:行 + 前间隙全锁,INSERT 也被堵 → 排队人数翻倍

这就是为什么高并发场景普遍选 RC------锁的范围就是并发度的上限


八、各种写操作触发什么锁(一张表总结)

操作 MDL 意向锁 行级锁
纯 SELECT 读锁
SELECT ... FOR UPDATE 读锁 IX RC: 记录锁 / RR: Next-Key Lock
SELECT ... LOCK IN SHARE MODE 读锁 IS 记录锁(共享)
UPDATE 读锁 IX 记录锁 / Next-Key Lock
DELETE 读锁 IX 记录锁 / Next-Key Lock
INSERT 读锁 IX 记录锁(锁自己插的那行)
ALTER TABLE 写锁 不涉及行锁 无(MDL 写锁直接排他)

纯 SELECT 不加任何行锁------MVCC 快照读。这也是"读不阻塞写"的根因。


九、线上排查锁问题的三板斧

sql 复制代码
-- 1. 看当前谁在等锁
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

-- 2. 看当前持有哪些锁
SELECT * FROM information_schema.INNODB_LOCKS;  -- 5.7
SELECT * FROM performance_schema.data_locks;     -- 8.0

-- 3. 找长事务(超过 60 秒的)
SELECT * FROM information_schema.INNODB_TRX 
WHERE trx_started < NOW() - INTERVAL 60 SECOND;

总结

  1. 三层锁同时存在:MDL 锁表结构 + 意向锁做标记 + 行锁锁数据,各管各的
  2. RC 和 RR 的核心锁差别:RC 不要间隙锁,RR 有间隙锁,这是并发吞吐差好几倍的主因
  3. 行锁不保护读:MVCC 和锁是两条独立的路,不可重复读不是行锁失效,是读根本没走锁
  4. 没索引的 FOR UPDATE 就是表锁:锁加在索引记录上,没有索引就全表扫全表锁
  5. 长事务的锁会传导放大:不是死锁才致命,锁等待超时更隐蔽

MDL 管结构,行锁管数据。RC 省间隙高并发,RR 间隙锁防幻读。行锁不保读,MVCC 走快照。

如果觉得有用,点赞收藏。


相关推荐
Rain5091 小时前
GitLab-Runner + AI 代码审查服务 + 远程大模型 全套部署运维实战
linux·运维·人工智能·python·ci/cd·gitlab·ai编程
Engineer邓祥浩1 小时前
宏观认知(4):AI与社会——吴恩达《AI for Everyone》Week4学习笔记
人工智能·笔记·学习
jjjava2.01 小时前
全面拆解 Java 锁:分类辨析 + 底层原理精讲
java·开发语言
曹牧1 小时前
Java:import NeverUsed
java·开发语言·log4j
Trouvaille ~1 小时前
【Redis篇】Redis 事务:原子性与脚本执行机制
数据库·redis·后端·算法·junit·lua·原子性
imDwAaY1 小时前
从非线性分类到多层神经网络 CS188 Note21 学习笔记
人工智能·笔记·python·神经网络·学习·机器学习·分类
稳如磐石.1 小时前
北京工控机生产工厂
大数据·人工智能·python
之歆1 小时前
在 IntelliJ IDEA 里复刻 Cursor 式内联审查:架构复盘-从放弃到拾起:如何用 LineStatusTracker 拯救一个烂掉的项目
java·架构·intellij-idea
jeffer_liu1 小时前
Spring AI 生产级实战-结构化输出
java·人工智能·后端·spring·大模型