MySQL 锁机制完全解析

深入理解 MySQL(InnoDB)中各种锁的原理、实现方式与使用场景 · 交互式可视化

🔍 锁的宏观分类

MySQL 中的"锁"可以从多个维度来分类。很多混淆来自于把概念层实现层的锁混在一起讨论。

核心区分:概念 vs 实现
"悲观锁"和"乐观锁"是并发控制的思想/设计模式,不是 MySQL 提供的特定锁类型。而"行锁"、"表锁"、"间隙锁"等是 MySQL(主要是 InnoDB)中真实存在的锁实现

分类维度一览

分类维度 具体类型 说明
按思想模式 悲观锁、乐观锁 并发控制的设计思想,不是具体锁
按锁粒度 表级锁、行级锁、页级锁 锁的作用范围大小
按兼容性 共享锁(S锁)、排他锁(X锁) 锁之间是否兼容
按实现方式 记录锁、间隙锁、临键锁、意向锁 InnoDB 的具体锁实现
按触发方式 自动加锁、显式加锁 语句是否自动获取锁

存储引擎与锁的关系

存储引擎支持锁类型说明
InnoDB表锁 + 行锁(MVCC)支持事务,默认行级锁,MVCC 实现非锁定读
MyISAM表级锁不支持事务,只支持表级锁
MEMORY表级锁同 MyISAM
NDB行级锁集群存储引擎

🤔 悲观锁 vs 乐观锁

💡 重要:这是"思想",不是具体锁!

悲观锁和乐观锁是并发控制的设计模式,在数据库层面,它们通过不同的机制来实现。

悲观锁 Pessimistic Locking

假设一定会发生并发冲突,所以在操作数据前先加锁,其他人无法同时修改。

实现方式(InnoDB)SQL 示例
SELECT ... FOR SHARE(S锁) SELECT * FROM orders WHERE id=1 FOR SHARE;
SELECT ... FOR UPDATE(X锁) SELECT * FROM orders WHERE id=1 FOR UPDATE;
UPDATE / DELETE(自动加X锁) UPDATE orders SET status=1 WHERE id=1;

注意:SELECT ... FOR UPDATE 在 InnoDB 中具体加的是"临键锁"(Next-Key Lock),而非简单的行锁。这是很多面试考点。

乐观锁 Optimistic Locking

假设大概率不会发生冲突,不加锁直接操作,提交时检查数据是否被修改过。

实现方式说明示例
版本号 Version 每行数据带 version 字段,更新时比对 UPDATE ... WHERE id=1 AND version=5;
时间戳 Timestamp 用更新时间判断是否有并发修改 UPDATE ... WHERE id=1 AND updated_at='...';
MVCC 快照读 InnoDB 的普通 SELECT 就是乐观锁思想的体现 SELECT * FROM orders WHERE id=1;
-- 乐观锁典型实现(版本号) UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 100 AND version = 5; -- 如果版本变了,影响行数为 0

📦 表级锁 Table-Level Lock

表级锁是 MySQL 中粒度最粗的锁,锁定整张表。MyISAM 只支持表锁,InnoDB 也支持(但一般不用)。

1. 表锁(手动)

-- 加表级读锁(S锁) LOCK TABLES users READ; -- 加表级写锁(X锁) LOCK TABLES users WRITE; -- 解锁 UNLOCK TABLES;
锁类型自身可读自身可写其他事务可读其他事务可写
表级读锁(READ)❌(阻塞)
表级写锁(WRITE)❌(阻塞)❌(阻塞)

2. 元数据锁 MDL(Metadata Lock)

MDL 是 MySQL 自动加的表级锁,用于保护表的元数据(结构)。当对表做 CRUD 时自动加 MDL 读锁;做 DDL(ALTER TABLE 等)时加 MDL 写锁。

MDL 导致的问题:一个长事务持有 MDL 读锁时,DDL 操作会被阻塞,而后续所有 CRUD 操作又会阻塞在 DDL 后面,形成"锁队列堆积"。这是线上事故的常见原因!

🔐 行级锁 Row-Level Lock

行级锁是 InnoDB 的核心特性,粒度最细,并发度最高。注意:行锁是通过锁索引记录来实现的,如果没有索引,会退化为表锁!

共享锁(S锁)与排他锁(X锁)

锁类型含义加锁方式
共享锁 S(Shared)读锁,其他事务可同时持有 S 锁,但不能加 X 锁SELECT ... FOR SHARE
排他锁 X(Exclusive)写锁,其他事务不能加任何锁SELECT ... FOR UPDATE、UPDATE、DELETE

兼容性矩阵:S 锁与 S 锁兼容,S 与 X 不兼容,X 与任何锁都不兼容。下面有交互式兼容矩阵可以体验。

🎯 记录锁 Record Lock

记录锁是最基本的行锁,锁定索引记录本身。例如 SELECT * FROM users WHERE id=5 FOR UPDATE 会对 id=5 这条索引记录加 X 型记录锁。

📊 记录锁可视化 — 点击索引记录查看锁状态
上图模拟了 id 为 1~8 的索引记录。点击某条记录可以"加锁",观察哪些记录被锁定。

🕳️ 间隙锁 Gap Lock

间隙锁锁定的是索引记录之间的"间隙",不包括记录本身。用于防止其他事务往这个间隙中插入新记录,从而解决幻读问题。

重要:间隙锁只在 REPEATABLE READ 隔离级别下生效。READ COMMITTED 级别下,间隙锁会失效(除了外键和唯一性检查)。

-- 假设 users 表中 id 有 1, 5, 10 -- 间隙包括:(-∞, 1), (1, 5), (5, 10), (10, +∞) -- 事务 A:对 (5, 10) 这个间隙加锁 START TRANSACTION; SELECT * FROM users WHERE id > 5 AND id < 10 FOR UPDATE; -- 事务 B:尝试插入 id=7,会被阻塞! INSERT INTO users (id, name) VALUES (7, 'Alice'); -- 阻塞!
📊 间隙锁可视化 — 展示索引间隙与锁定范围
点击不同间隙可以查看间隙锁的锁定范围。注意间隙锁之间不互斥(不同事务可以持有重叠的间隙锁)。

🔗 临键锁 Next-Key Lock

临键锁是记录锁 + 间隙锁的组合,锁定的是"当前记录 + 前面的间隙"。这是 InnoDB 在 REPEATABLE READ 隔离级别下的默认行锁算法

核心公式:Next-Key Lock = Record Lock + Gap Lock
锁定范围:(上一个记录的id, 当前记录的id](左开右闭区间)

-- 假设 id 索引有值:1, 5, 10, 15 -- 执行以下语句(RR 隔离级别): SELECT * FROM users WHERE id = 10 FOR UPDATE; -- 加的 Next-Key Lock 锁定范围:(5, 10] -- 即:锁住 id=10 这条记录,以及 (5, 10) 这个间隙
📊 临键锁(Next-Key Lock)可视化
展示 Next-Key Lock 的锁定范围(记录 + 前面的间隙)。可以切换不同的索引记录查看锁定区间变化。

🤝 意向锁 Intention Lock

意向锁是表级锁,由 InnoDB 自动添加,用于表明"某个事务即将或正在对表中的某些行加锁"。

没有意向锁的话,如果要给表加 X 锁,需要逐行检查是否有行锁 — 效率极低。意向锁让"InnoDB 快速判断表里是否有行锁"成为可能。

锁类型说明兼容性
意向共享锁 IS事务打算给某些行加 S 锁与表级 S 兼容,与表级 X 不兼容
意向排他锁 IX事务打算给某些行加 X 锁与表级 S/X 都不兼容
-- 以下语句会自动加意向锁: SELECT * FROM users WHERE id=1 FOR SHARE; -- 自动加 IS 锁(表级)+ S 锁(行级) SELECT * FROM users WHERE id=1 FOR UPDATE; -- 自动加 IX 锁(表级)+ X 锁(行级)

🔢 AUTO-INC 锁

AUTO-INC 锁是 InnoDB 对 AUTO_INCREMENT 列插入时使用的特殊表级锁。在插入完成后立即释放(不是事务结束才释放)。

MySQL 提供了 innodb_autoinc_lock_mode 参数来控制其行为:

mode说明
0(traditional)所有 INSERT 都使用表级 AUTO-INC 锁,并发最低
1(consecutive,默认)简单 INSERT 使用轻量级锁,批量 INSERT 仍用表锁
2(interleaved)所有 INSERT 都不使用表锁,并发最高,但主从复制可能不安全

📄 页级锁 Page-Level Lock

页级锁锁定的是数据页(InnoDB 默认页大小 16KB)。粒度介于表锁和行锁之间。

注意:InnoDB 不支持页级锁。页级锁主要是 Berkeley DB (BDB) 存储引擎的特性。InnoDB 只有表级锁和行级锁。很多资料会混淆这一点。

💀 死锁 Deadlock

死锁是指两个或更多事务互相持有对方需要的锁,导致所有事务都无法继续执行。

-- 经典死锁场景 -- 事务 A: START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 持有 id=1 的 X 锁 -- 此时事务 B 执行... UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待 id=2 的 X 锁 → 死锁! -- 事务 B: START TRANSACTION; UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 持有 id=2 的 X 锁 UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 等待 id=1 的 X 锁 → 死锁!
📊 死锁演示 — 点击"开始演示"观察死锁发生过程

🌳 锁分类树(交互式)

下面是一棵完整的交互式锁分类树,展示了 MySQL 中所有锁类型之间的层次关系。点击节点可以展开/收起。

根节点是"MySQL 锁",子节点按不同维度展开。概念层(悲观/乐观)和实现层(行锁/表锁/...)用不同颜色区分。

🔗 锁兼容矩阵(交互式)

点击矩阵中的格子,查看两种锁类型之间的兼容关系说明。

点击上方矩阵中的任意格子,查看兼容关系详细说明。

📝 总结与速查

MySQL 锁速查表
锁名称 类型 作用范围 关键特点
悲观锁🧠 概念-先加锁再操作,FOR UPDATE 是其实现
乐观锁🧠 概念-版本号/MVCC,不加锁
表级锁🔧 实现整张表MyISAM 默认,粒度粗
行级锁🔧 实现索引记录InnoDB 默认,需索引支持
记录锁🔧 实现单条索引记录行锁的基础形式
间隙锁🔧 实现索引记录之间的间隙防止幻读,RR 级别下有效
临键锁🔧 实现间隙 + 记录InnoDB RR 级别默认算法
意向锁🔧 实现表级(意向)InnoDB 自动添加,快速判断行锁冲突
MDL🔧 实现整张表(元数据)保护表结构,DDL 会加写锁
AUTO-INC 锁🔧 实现表级(自增)插入完成后立即释放

面试高频考点:
1. InnoDB 在 RR 级别下如何防止幻读?(Next-Key Lock)
2. 什么是当前读、什么是快照读?(当前读加锁,快照读走 MVCC)
3. 为什么 InnoDB 需要意向锁?(快速判断表中是否存在行锁冲突)
4. FOR UPDATE 加的是什么锁?(Next-Key Lock,不是简单的行锁)