一、锁的分类
MySQL 锁按不同维度划分:
| 维度 | 分类 |
|---|---|
| 粒度 | 全局锁、表级锁、页级锁(BDB)、行级锁 |
| 机制 | 共享锁(S锁)、排他锁(X锁) |
| 思想 | 悲观锁(SELECT FOR UPDATE)、乐观锁(版本号/CAS) |
| 加锁方式 | 隐式锁(自动加)、显式锁(手动 SQL) |
InnoDB 是最常用的引擎,下面主要围绕 InnoDB 展开。
二、全局锁
对整个数据库实例加锁,让整个库只读。
FLUSH TABLES WITH READ LOCK; -- 加全局读锁
UNLOCK TABLES; -- 释放
典型场景:全库逻辑备份(mysqldump 加 --single-transaction 可替代,避免锁库)。
注意:加了全局锁后任何 DML/DDL 都会被阻塞,生产慎用。
三、表级锁
1. 表锁(Table Lock)
LOCK TABLES t1 READ; -- 共享表锁
LOCK TABLES t1 WRITE; -- 排他表锁
UNLOCK TABLES;
MyISAM 默认用表锁,开销小但并发差。InnoDB 一般不用显式表锁。
2. 元数据锁(MDL)
MySQL 5.5 引入,自动隐式加锁,用于保证 DDL 和 DML 的冲突:
- 执行 CRUD → 自动加 MDL 读锁
- 执行 ALTER TABLE → 自动加 MDL 写锁
常见坑:一个慢查询持有 MDL 读锁不释放,后续 ALTER TABLE 尝试加 MDL 写锁被阻塞,然后写锁后面的所有 CRUD 也排队等 MDL 读锁→ 整个表"卡死"。
排查:SELECT * FROM performance_schema.metadata_locks;
3. 意向锁(Intention Lock)
InnoDB 特有,隐式自动加锁,解决表锁和行锁的冲突判断:
- 加行锁前先加对应的意向锁(IS 或 IX)
- 表锁只需检查意向锁,不用逐行遍历
| 意向共享锁(IS) | 意向排他锁(IX) | |
|---|---|---|
| 表 S 锁 | ✅ 兼容 | ❌ 冲突 |
| 表 X 锁 | ❌ 冲突 | ❌ 冲突 |
记忆:意向锁之间永远兼容(IS/IS、IS/IX、IX/IX 均兼容),它只和表级锁冲突。
4. AUTO-INC 锁
自增主键插入时使用,保证 AUTO_INCREMENT 值的连续性。MySQL 8.0 默认用轻量级互斥量替代。
四、行级锁(InnoDB)
1. 记录锁(Record Lock)
锁定单条索引记录。
SELECT * FROM t WHERE id = 1 FOR UPDATE; -- 对 id=1 加 X 锁
关键:行锁是加在索引上的,不是加在数据行上。如果查询条件不走索引,会退化为表锁。
2. 间隙锁(Gap Lock)
锁定索引记录之间的间隙,不包含记录本身。只在 RR 隔离级别下存在。
索引值: 5 10 15 20
间隙: (-∞,5) (5,10) (10,15) (15,20) (20,+∞)
SELECT * FROM t WHERE id BETWEEN 10 AND 15 FOR UPDATE;
-- 锁定 (5,10)、(10,15)、(15,20) 三个间隙 + 10、15 两条记录
作用:防止幻读 —— 间隙锁阻止其他事务在间隙中 INSERT 新数据。
3. 临键锁(Next-Key Lock)
记录锁 + 间隙锁的组合,锁定"左开右闭"区间。InnoDB RR 级别的默认行锁算法。
锁住的是 (5, 10] 区间:间隙锁 (5,10) + 记录锁 [10]
效果:既防止当前记录被修改,也防止间隙插入,彻底杜绝幻读。
4. 插入意向锁(Insert Intention Lock)
INSERT 前自动加的间隙锁的一种,多个事务对同一间隙的插入意向锁互相兼容(只要不插入相同位置),提升并发插入性能。
5. 加锁规则
两个原则 + 两个优化 + 一个 bug:
原则:
- 加锁的基本单位是 Next-Key Lock(左开右闭)
- 查找中访问到的对象才加锁
优化:
- 唯一索引等值查询且记录存在 → 退化为 Record Lock
- 等值查询向右遍历到第一个不满足条件的值 → 最后一个间隙锁退化为 Gap Lock(右开)
Bug(MySQL 8.0.18 前):唯一索引范围查询会多锁一个 Next-Key Lock
五、死锁
产生条件(四要素,缺一不可)
- 互斥:资源不能被共享
- 持有并等待:持有已获得的锁,同时等待其他锁
- 不可剥夺:锁只能由持有者主动释放
- 循环等待:事务间形成等待环路
典型场景
-- 事务 A
UPDATE t SET v = 1 WHERE id = 1; -- 获得 id=1 的 X 锁
UPDATE t SET v = 2 WHERE id = 2; -- 等待 id=2 的锁 → 被 B 持有
-- 事务 B(并发)
UPDATE t SET v = 3 WHERE id = 2; -- 获得 id=2 的 X 锁
UPDATE t SET v = 4 WHERE id = 1; -- 等待 id=1 的锁 → 被 A 持有 → 死锁
排查
SHOW ENGINE INNODB STATUS; -- 查看最近死锁详情
SELECT * FROM information_schema.INNODB_TRX; -- 当前事务
SELECT * FROM information_schema.INNODB_LOCKS; -- 当前锁(8.0 废弃)
SELECT * FROM performance_schema.data_locks; -- 当前锁(8.0+)
避免策略
- 固定加锁顺序(所有事务按相同顺序访问资源)
- 拆分大事务,缩短锁持有时间
- 合理使用索引(减少锁范围)
- 降低隔离级别(RC 去掉间隙锁,减少锁冲突)
- 死锁检测(
innodb_deadlock_detect=ON)→ 自动回滚代价最小的事务
六、MVCC 与锁
MVCC(多版本并发控制)是 InnoDB 实现读不加锁的核心机制,与锁体系互补:
| 对比 | 锁机制 | MVCC |
|---|---|---|
| 读操作 | 当前读(加锁) | 快照读(不加锁) |
| 实现 | 行锁 + 间隙锁 + 临键锁 | undo log + ReadView |
| 隔离级别 | 所有级别 | RC + RR(RU 无,序列化退化为锁) |
当前读(加锁):SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE、INSERT
快照读(无锁):普通 SELECT
MVCC 通过 DB_TRX_ID(事务ID)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID(行ID)三个隐藏字段 + undo log 版本链实现。
七、常见面试题
MySQL 有哪些锁? → 全局锁、表锁(表锁/MDL/意向锁/AUTO-INC)、行锁(Record/Gap/Next-Key/插入意向)
RR 级别如何解决幻读? → Next-Key Lock(记录锁+间隙锁)阻止其他事务在间隙 INSERT
为什么 RR 级别下仍然有幻读? → 快照读无幻读(MVCC),但当前读(FOR UPDATE)如果不用 Next-Key Lock,仍可能幻读
行锁是加在数据行上的吗? → 不是,加在索引上;不走索引退化为表锁
SELECT FOR UPDATE 和 LOCK IN SHARE MODE 区别? → FOR UPDATE 是 X 锁(排他,阻塞读写),LOCK IN SHARE MODE 是 S 锁(共享,阻塞写不阻塞读)
RC 和 RR 的锁有什么区别? → RC 没有间隙锁,只用 Record Lock;RR 有完整 Gap Lock + Next-Key Lock
死锁了怎么办? → MySQL 自动检测并回滚最小事务;应用层加锁失败重试即可