MySQL 锁体系从 MDL到间隙锁详解

 更新时间:2026年06月04日 09:34:10   作者:学计算机的计算基  
本文详细解析了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 TABLESMySQL 自动加InnoDB 自动加
触发条件你写才锁任何操作都加DML 写行之前
锁什么整张表的读写表结构变更的时机不锁,只是一个标记
释放UNLOCK TABLES事务提交事务提交
互斥关系排斥一切读写读兼容读,写排斥一切意向之间完全兼容

表锁 = 手动扔核弹(LOCK TABLES WRITE 把这表全锁了)
MDL = 自动保安(有 DDL 才打架,DML 之间相安无事)
意向锁 = 门口登记表(有人要锁整表时看一眼:里面有行锁吗?有就等)

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

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

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

记录锁(Record Lock)

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

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 级别默认就是这个:锁住记录 + 它前面的间隙

-- RR 下,即使你只查一行
SELECT * FROM user WHERE id = 10 FOR UPDATE;
→ 加的实际上是 (上一个id, 10] 这个区间
→ 前面的间隙也锁住了
RCRR
记录锁
间隙锁✗ 不用✓ 用
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 的行锁加在索引记录上。没索引怎么办?全表扫描,扫到一行锁一行:

-- 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 的间隙锁在高并发下是性能杀手:

-- 秒杀:100个用户同时下单
SELECT stock FROM product WHERE id = 100 FOR UPDATE;
RC:只锁 id=100 这一行,同行的 UPDATE 排队,INSERT 不受影响
RR:行 + 前间隙全锁,INSERT 也被堵 → 排队人数翻倍

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

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

操作MDL意向锁行级锁
纯 SELECT读锁
SELECT … FOR UPDATE读锁IXRC: 记录锁 / 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 快照读。这也是"读不阻塞写"的根因。

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

-- 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 走快照。

到此这篇关于MySQL 锁体系从 MDL到间隙锁详解的文章就介绍到这了,更多相关mysql从 mdl到间隙锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Mysql中的单表最大记录是多少

    Mysql中的单表最大记录是多少

    这篇文章主要介绍了Mysql中的单表最大记录是多少问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • 一文详解MYSQL的多版本并发控制MVCC(Multi-Version Concurrency Control)

    一文详解MYSQL的多版本并发控制MVCC(Multi-Version Concurrency Co

    MVCC是一种用于数据库管理系统的并发控制技术,允许多个事务同时访问数据库,而不会导致读写冲突,本文就详细的介绍了MVCC的具体用法,具有一定的参考价值,感兴趣的可以了解一下
    2023-10-10
  • MySQL两种临时表的用法详解

    MySQL两种临时表的用法详解

    这篇文章主要介绍了MySQL两种临时表的用法详解,.内容比较详细,这里分享给大家,供大家参考,学习。
    2017-10-10
  • MySQL on k8s 云原生环境部署

    MySQL on k8s 云原生环境部署

    这篇文章主要为大家介绍了MySQL on k8s 云原生环境部署实现过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • MySQL Shell import_table数据导入的实现

    MySQL Shell import_table数据导入的实现

    这篇文章主要介绍了MySQL Shell import_table数据导入的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • MySQL 之压力测试工具的使用方法

    MySQL 之压力测试工具的使用方法

    这篇文章主要介绍了MySQL 之压力测试工具的使用方法,mysqlslap是mysql自带的基准测试工具,该工具查询数据,语法简单,灵活容易使用,感兴趣的可以了解一下
    2020-05-05
  • 一文彻底搞懂MySQL TimeStamp时区问题

    一文彻底搞懂MySQL TimeStamp时区问题

    MySQL的timestamp类型默认使用的是服务器的时区来存储时间值,这意味着如果服务器的时区发生了变化,那么存储的timestamp值也会发生变化,下面这篇文章主要给大家介绍了关于如何通过一文彻底搞懂MySQL TimeStamp时区问题的相关资料,需要的朋友可以参考下
    2024-01-01
  • Mysql虚拟列的使用场景

    Mysql虚拟列的使用场景

    MySQL虚拟列是一种在查询时动态生成的特殊列,它不占用存储空间,可以提高查询效率和数据处理便利性,本文给大家介绍Mysql虚拟列的相关知识,感兴趣的朋友一起看看吧
    2025-01-01
  • MySQL中索引失效的12种场景及对应解决方案

    MySQL中索引失效的12种场景及对应解决方案

    MySQL索引是提升数据库性能的关键因素,正确使用索引可以将查询效率提高几十倍甚至上百倍,本文将分享MySQL索引失效的12种典型场景,需要的可以参考一下
    2025-05-05
  • MySQL8.0之CTE(公用表表达式)的使用

    MySQL8.0之CTE(公用表表达式)的使用

    本文主要介绍了MySQL8.0之CTE(公用表表达式)的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07

最新评论