MySQL 各种锁机制详解
重点放在 InnoDB,引擎不同锁语义也不同。
目标:弄清楚"有哪些锁、锁什么粒度、什么时候会被加上"。
一、为什么需要锁?
数据库是"多人同时操作同一份数据"的系统:
- 多个事务并发更新同一行;
- 一个事务在扫一段范围时,另一个插入新数据;
- 结构变更(DDL)和读写(DML)并发执行;
如果不加锁:
- 数据会被"乱改";
- 读到一半被改、更新丢失、约束被破坏。
所以需要各种锁:
- 控制并发写;
- 配合 MVCC 控制并发读写;
- 控制 元数据变更(DDL);
MySQL 的锁大致可以从几类角度划分:
- 按对象粒度:全局锁、库锁、表锁、行锁;
- 按功能/用途:读锁(S)、写锁(X)、意向锁、元数据锁、间隙锁;
- 按引擎:Server 层锁、InnoDB 锁、MyISAM 锁等。
下面重点围绕 InnoDB 来讲。
二、从大到小看:锁的粒度分类
2.1 全局锁(Global Lock)
- 作用范围:整个实例(所有数据库、所有表)。
- 常见命令:
sql
FLUSH TABLES WITH READ LOCK;
作用:
- 把整个实例"锁成只读",常用于全库逻辑备份;
- 期间所有 DML、DDL 都会被阻塞(写不了)。
不常用,慎用,线上大量业务时基本不会直接这么干。
2.2 表级锁(Table Lock)
2.2.1 MySQL Server 层的显式表锁
语法:
sql
LOCK TABLES t1 READ, t2 WRITE;
UNLOCK TABLES;
特点:
- 不区分引擎,属于 Server 层机制;
- READ:其他会话可读不能写;
- WRITE:其他会话不能读也不能写;
- 粒度粗,并发度差,实际业务很少手工用。
2.2.2 MyISAM 的表锁
MyISAM 没有行锁,只有表锁:
- 读锁(READ LOCK):多读互不阻塞;
- 写锁(WRITE LOCK):写独占,阻塞其它读写。
这也是 MyISAM 不适合高并发写场景的重要原因。
2.3 元数据锁(Metadata Lock,MDL)
- 作用对象:表结构(元数据)层面;
- 自动加,不能手动控制。
场景:
- 对一张表执行
SELECT/INSERT/UPDATE/DELETE时:- 获得一个 MDL 读锁;
- 阻止别人对该表执行结构变更(DDL);
- 对表执行
ALTER TABLE/DROP TABLE等 DDL 时:- 需要获取 MDL 写锁;
- 会等待所有 MDL 读锁释放。
作用:
- 保证 DDL 和 DML 不会"打架";
- 比如:你在跑查询时,不会有人把表给删了。
注意:
- 长事务会长期持有 MDL 读锁;
- 这会导致后续的 DDL 一直阻塞在"Waiting for table metadata lock";
- 线上经常见到这种场景。
2.4 行级锁(Row Lock)------InnoDB 主角
InnoDB 提供的行级锁包括:
- 记录锁(Record Lock)
- 间隙锁(Gap Lock)
- Next-Key Lock(记录锁 + 间隙锁)
- 插入意向锁(Insert Intention Lock)
以及上层逻辑上的:
- 共享锁(S 锁,读锁)
- 排他锁(X 锁,写锁)
- 意向锁(意向共享 / 意向排他)
行级锁的特点:
- 粒度细,并发度高;
- 成本高于表锁;
- 默认是行锁 + MVCC 组合模式。
下面逐个拆。
三、InnoDB 行锁的类型
3.1 共享锁(S)和排他锁(X)
逻辑层面最常见的两种:
- 共享锁(Shared Lock, S 锁) :
- 多个事务可以同时持有 S 锁;
- 通常用于读,互相不冲突;
- 排他锁(Exclusive Lock, X 锁) :
- 只允许一个事务持有;
- 通常用于写,和其它 S/X 锁都不兼容(除自己)。
兼容矩阵(简化):
| 当前持有 \ 请求 | S 请求 | X 请求 |
|---|---|---|
| S | 兼容 | 冲突 |
| X | 冲突 | 冲突 |
常见 SQL:
sql
-- 共享锁
SELECT * FROM t WHERE id = 1 LOCK IN SHARE MODE; -- 8.0 之后不推荐,用 FOR SHARE
SELECT * FROM t WHERE id = 1 FOR SHARE;
-- 排他锁
SELECT * FROM t WHERE id = 1 FOR UPDATE;
3.2 意向锁(Intention Lock)
作用对象:表级别,但用来配合行锁使用。
- 意向共享锁(IS):事务打算在某些行上加共享锁;
- 意向排他锁(IX):事务打算在某些行上加排他锁。
为什么需要意向锁?
- 主要是为了配合表锁:
- 如果要给整张表加表级 S/X 锁,需要知道表中是否存在行锁冲突;
- 不可能一行行扫描,所以设计了"意向锁"。
行为:
- 当事务要在某一行上加 X 锁时,会先在表上加 IX 锁;
- 当事务要在某一行上加 S 锁时,会先在表上加 IS 锁。
表级锁与意向锁的兼容大致如下(记个直观印象即可):
| IS | IX | S 表锁 | X 表锁 | |
|---|---|---|---|---|
| IS | √ | √ | √ | × |
| IX | √ | √ | × | × |
| S 表锁 | √ | × | √ | × |
| X 表锁 | × | × | × | × |
总结:
- 意向锁本身不会阻塞普通行锁;
- 它的主要任务是:加速"表锁与行锁之间的冲突判断"。
3.3 记录锁(Record Lock)
- 锁定"索引上的某一条记录";
- 本质是:锁定某个索引键值。
例子:
sql
SELECT * FROM user WHERE id = 10 FOR UPDATE;
假设 id 有索引:
- 就会对
id = 10这一条索引记录加记录锁(X 锁); - 其他事务不能修改或删除这条记录。
注意:
InnoDB 的行锁是基于 索引 实现的,没有索引就可能退化成表锁或锁一大片。
3.4 间隙锁(Gap Lock)
- 锁定"索引之间的间隙",不包括记录本身。
比如:索引里现有值:10, 20, 30, 40,
则间隙为:(-∞,10)、(10,20)、(20,30)、(30,40)、(40,+∞)。
间隙锁用来:
- 阻止其他事务在某些范围"插入新记录";
- 本质目的是防止"幻读(Phantom Read)"。
例子:
sql
SELECT * FROM t WHERE age BETWEEN 20 AND 30 FOR UPDATE;
InnoDB(在某些隔离级别)可能会:
- 不仅锁定现有的满足条件的记录;
- 还会锁定 20~30 之间的"间隙",阻止插入新的 age 在这个范围内的数据;
- 从而在后续同一事务再次查询时,不会出现"多出一条新的行"的幻读。
3.5 Next-Key Lock(记录锁 + 间隙锁)
- 是"记录锁 + 间隙锁"的组合;
- 锁定一个"左开右闭"的区间:
(前一个索引值, 当前索引值]。
在 InnoDB 默认的 REPEATABLE READ 下:
- 对索引范围查询(带 for update / for share)会采用 Next-Key Lock;
- 它既锁定记录本身,也锁定附近的间隙;
- 减少幻读发生的可能。
简单理解:
Next-Key Lock = 将记录锁扩展到一个范围,以防止新记录插进来造成幻读。
3.6 插入意向锁(Insert Intention Lock)
一种特殊的间隙锁,用于插入时:
- 当事务要在某个间隙插入记录时,会先声明一个"插入意向锁";
- 多个事务在不同位置插入,彼此不会互相阻塞;
- 只有插入位置冲突时,才会等待。
四、InnoDB 锁是如何被加上的?
4.1 普通 SELECT(不加锁)
sql
SELECT * FROM t WHERE id = 10;
- 默认是一致性读(Consistent Read);
- 利用 MVCC 读取数据版本;
- 一般不加行锁(除非特殊情况如手工 hint 或特定隔离级别)。
4.2 锁定读:SELECT ... FOR UPDATE / FOR SHARE
sql
-- 排他锁
SELECT * FROM t WHERE id = 10 FOR UPDATE;
-- 共享锁
SELECT * FROM t WHERE id = 10 FOR SHARE;
特点:
- 在可重复读 / 读已提交下,都会对符合条件的记录加行锁(记录锁/Next-Key Lock);
- 会参与锁冲突判定;
- 用于做"先查后改"的场景防止并发脏写。
4.3 UPDATE / DELETE 自动加锁
sql
UPDATE t SET balance = balance - 100 WHERE id = 1;
DELETE FROM t WHERE id = 2;
- InnoDB 自动对匹配的记录加排他锁(行锁);
- 其他事务不能修改这些行,直到事务提交/回滚。
五、锁与索引的关系
一个非常重要的点:行锁是基于索引的。
- 如果 WHERE 条件能用到索引:只锁命中的那几行;
- 如果没用上索引:可能退化为锁住更多记录甚至全表。
例子:
sql
-- id 上有索引
SELECT * FROM user WHERE id = 10 FOR UPDATE;
-- ✅ 只锁 id = 10 对应的那条索引记录
-- name 上无索引
SELECT * FROM user WHERE name = 'Tom' FOR UPDATE;
-- ❌ 可能会锁更大范围(扫描整个表,行锁挨个加,甚至接近表锁效果)
优化建议:
- 对经常锁定某个字段的场景,要确保该字段有索引;
- 避免在没有合适索引的条件上使用 FOR UPDATE / FOR SHARE。
六、MySQL 锁相关的典型问题
6.1 死锁(Deadlock)
典型死锁场景:
text
事务 A:锁记录 1 -> 再锁记录 2
事务 B:锁记录 2 -> 再锁记录 1
两边互相等对方释放锁,形成死锁。
InnoDB 会:
- 自动检测死锁;
- 回滚其中一个事务,报错:
Deadlock found when trying to get lock。
避免方案:
- 访问多行时,尽量按照固定顺序加锁;
- 控制事务粒度和时间,避免大事务;
- 理解你的索引和锁范围。
6.2 锁等待
- 如果锁冲突但不是死锁,就会出现等待;
- 超过
innodb_lock_wait_timeout(默认 50s)后报错。
排查手段:
sql
SHOW ENGINE INNODB STATUS \G;
或在新版本里用 performance_schema 里的锁视图。
七、锁与隔离级别的关系(简要)
在不同隔离级别下:
- 读未提交:大量读直接读最新版本,几乎不加锁,但允许脏读;
- 读已提交:读时只看到已提交事务的最新版本,通常不加间隙锁;
- 可重复读(默认) :
- 使用 MVCC 保证同一事务内多次读取一致;
- 加上 Next-Key Lock / 间隙锁 避免/减少幻读;
- 串行化:很多读都会退化为锁定读,强制串行执行,锁竞争最激烈。
简化理解:
隔离级别越高,加的锁越多或锁得越久,并发性能越差,但数据越"安全"。
八、常见锁类型速查表
| 锁类型 | 粒度 | 作用对象 | 主要用途 |
|---|---|---|---|
| 全局锁 | 实例 | 所有库表 | 全库备份,阻止写入 |
| 表锁 | 表 | 整张表 | 简单并发控制(MyISAM、多数不用) |
| 元数据锁 MDL | 表 | 表结构 / 元数据 | DDL 与 DML 并发安全 |
| 行锁(记录锁) | 行 | 单条记录(索引项) | 控制并发更新单行 |
| 间隙锁 | 行 | 索引间隙 | 防止插入,避免幻读 |
| Next-Key Lock | 行 | 记录 + 间隙 | InnoDB 默认可重复读的主要锁 |
| 插入意向锁 | 行 | 插入位置 | 多事务在不同位置插入时协调 |
| S 锁 | 行/表 | 共享读 | 允许多读,不允许写 |
| X 锁 | 行/表 | 排他写 | 写时独占,阻塞其它读写 |
| 意向锁 IS/IX | 表 | 声明将要在行上加 S/X 锁 | 加速表锁与行锁冲突检测 |
九、小结
- MySQL 锁分层很多:全局锁、表锁、MDL、行锁等等。
- InnoDB 的并发控制核心是:行锁 + 间隙锁 + Next-Key Lock + MVCC。
- 行锁基于索引实现,没索引容易锁更多数据甚至锁全表。
- 锁的细粒度在带来高并发能力的同时,也带来了死锁、锁等待 等问题,需要通过:
- 合理设计索引;
- 控制事务范围和顺序;
- 使用
EXPLAIN、SHOW ENGINE INNODB STATUS等工具来排查。
真正摸清楚锁的行为,一般要结合:隔离级别 + 索引结构 + 实际 SQL 反复实验和看执行计划。
这篇可以当成"概念地图",后面你如果有具体 SQL,我可以帮你一起分析"加了哪些锁、锁到了哪一段"。