MySQL的锁
MySQL 锁全面扫盲
一、按锁的粒度分
1. 全局锁(Global Lock)
代码块收起展开
FLUSH TABLES WITH READ LOCK; -- 加全局读锁
UNLOCK TABLES; -- 释放- 整个数据库实例变为只读,所有写操作、DDL、更新事务全部阻塞
- 使用场景:全库逻辑备份(mysqldump)
- 问题:业务完全停摆
- 更好的替代:mysqldump —single-transaction(利用 MVCC 一致性快照,仅限 InnoDB)
2. 表级锁(Table-Level Lock)
a) 表锁
代码块收起展开
LOCK TABLES t READ; -- 表读锁(共享)
LOCK TABLES t WRITE; -- 表写锁(排他)- MyISAM 默认用表锁,InnoDB 一般不用这个
- 粒度大,并发低
b) 元数据锁(MDL, Metadata Lock)
- 不需要显式加,MySQL 5.5+ 自动加
- 对表做 CRUD → 自动加 MDL 读锁
- 对表做 DDL(ALTER TABLE 等)→ 自动加 MDL 写锁
- 经典坑:一个长事务持有 MDL 读锁不提交 → 你去 ALTER TABLE → 被阻塞且后续所有查询全部排队 → 线上雪崩
c) 意向锁(Intention Lock)
- InnoDB 特有的表级标记锁
- 意向共享锁(IS):事务打算对某些行加共享锁前,先在表上加 IS
- 意向排他锁(IX):事务打算对某些行加排他锁前,先在表上加 IX
- 作用:让表级锁判断能快速知道”表里有没有行锁”,避免逐行扫描
- IS/IX 之间互不冲突,只与表级 S/X 锁冲突
d) AUTO-INC 锁
- 对自增列插入时加的特殊表级锁
- MySQL 8.0 默认 innodb_autoinc_lock_mode = 2(交叉模式),只在分配自增值时短暂持有一个轻量级互斥量,不再持有整条 INSERT 语句级别的表锁
3. 行级锁(Row-Level Lock)— InnoDB 核心
这是你重点要掌握的部分。
InnoDB 的行锁都是加在索引上的,不是加在数据行上。
a) 记录锁(Record Lock)
- 锁住索引上的一条确切记录
SELECT * FROM t WHERE id = 5 FOR UPDATE;→ 对 id=5 这条索引记录加 X 型记录锁
b) 间隙锁(Gap Lock)
- 锁住索引记录之间的间隙,不锁记录本身
- 是一个开区间 (a, b)
- 唯一目的:防止其他事务在间隙中插入新记录 → 解决幻读
- 间隙锁之间不冲突(两个事务可以同时对同一间隙加 Gap Lock)
- 只在 RR(REPEATABLE READ)隔离级别下生效,RC 下没有间隙锁
c) 临键锁(Next-Key Lock)
- Record Lock + Gap Lock = 左开右闭区间 (a, b]
- InnoDB 在 RR 级别下,默认加的就是 Next-Key Lock
- 举例:索引上有记录 5, 10, 15,那么 Next-Key Lock 可能锁的区间是
(-∞, 5]、(5, 10]、(10, 15]、(15, +∞)
d) 插入意向锁(Insert Intention Lock)
- 一种特殊的间隙锁,在 INSERT 操作时加
- 多个事务向同一个间隙的不同位置插入时互不阻塞
- 但如果间隙上已有 Gap Lock,INSERT 意向锁会被阻塞 → 这就是很多死锁的根源
二、按锁的模式/兼容性分
共享锁(S Lock / 读锁)
代码块收起展开
SELECT ... LOCK IN SHARE MODE; -- MySQL 5.x
SELECT ... FOR SHARE; -- MySQL 8.0+- 允许其他事务也加 S 锁,阻止其他事务加 X 锁
排他锁(X Lock / 写锁)
代码块收起展开
SELECT ... FOR UPDATE;
INSERT / UPDATE / DELETE; -- 自动加 X 锁- 阻止其他事务加任何锁
兼容矩阵
| 请求 S | 请求 X | |
|---|---|---|
| 持有 S | ✅ | ❌ |
| 持有 X | ❌ | ❌ |
三、按加锁方式分
乐观锁(应用层面,不是 MySQL 内置)
代码块收起展开
UPDATE t SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;
-- 影响行数为 0 → 说明被别人改过了,重试悲观锁(数据库层面)
代码块收起展开
SELECT * FROM t WHERE id = 1 FOR UPDATE; -- 先锁住
UPDATE t SET stock = stock - 1 WHERE id = 1;
COMMIT;四、行锁加锁规则(高频)
InnoDB 在 RR 下的加锁规则可以总结为几条。
两个原则:
- 加锁的基本单位是 Next-Key Lock(左开右闭)
- 只有访问到的索引记录才会加锁
两个优化:
- 唯一索引上的等值查询,命中记录 → Next-Key Lock 退化为 Record Lock
- 普通索引上的等值查询,向右遍历到第一个不满足条件的值 → Next-Key Lock 退化为 Gap Lock
一个 bug(特性):
- 唯一索引上的范围查询,会访问到不满足条件的第一个值为止
举例说明
表数据 id(主键):5, 10, 15, 20
| 查询 | 加锁范围 | 解释 |
|---|---|---|
WHERE id = 10 FOR UPDATE | 仅锁 id=10 这一条(Record Lock) | 唯一索引等值命中 → 退化为记录锁 |
WHERE id = 7 FOR UPDATE | 间隙锁 (5, 10) | 唯一索引等值未命中 → Next-Key Lock (5,10] 退化为 Gap Lock (5,10) |
WHERE id >= 10 AND id < 15 FOR UPDATE | Next-Key Lock (5,10] + (10,15] | 范围查询,锁到不满足条件的第一个值 |
五、死锁
经典死锁场景
代码块收起展开
事务A: SELECT * FROM t WHERE id = 1 FOR UPDATE; -- 锁住 id=1
事务B: SELECT * FROM t WHERE id = 2 FOR UPDATE; -- 锁住 id=2
事务A: SELECT * FROM t WHERE id = 2 FOR UPDATE; -- 等 B 释放
事务B: SELECT * FROM t WHERE id = 1 FOR UPDATE; -- 等 A 释放 → 死锁!InnoDB 处理方式
- 等待超时:innodb_lock_wait_timeout(默认 50s)
- 死锁检测:innodb_deadlock_detect = ON(默认开启),主动回滚代价最小的事务
- 查看最近死锁:
SHOW ENGINE INNODB STATUS;
六、一张图总结
- 按粒度
- 全局锁(FTWRL)
- 表级锁
- 表锁(LOCK TABLES)
- 元数据锁(MDL)
- 意向锁(IS/IX)
- AUTO-INC 锁
- 行级锁(InnoDB only,加在索引上)
- 记录锁(Record Lock)
- 间隙锁(Gap Lock)
- 临键锁(Next-Key Lock)
- 插入意向锁(Insert Intention Lock)
- 按模式
- 共享锁(S)
- 排他锁(X)
- 按思想
- 乐观锁(版本号/CAS,应用层)
- 悲观锁(FOR UPDATE,DB 层)
七、常见问题速答
| 问题 | 答案 |
|---|---|
| InnoDB 行锁锁的是什么? | 索引,不是数据行。没有索引会退化为表锁 |
| 间隙锁解决了什么问题? | 幻读(Phantom Read) |
| RC 隔离级别有间隙锁吗? | 没有,只有 Record Lock |
| 死锁怎么排查? | SHOW ENGINE INNODB STATUS 看 LATEST DETECTED DEADLOCK |
| 怎么减少锁冲突? | 控制事务大小、合理使用索引、降低隔离级别(RC) |
核心记住一句话:InnoDB 的锁加在索引上,没索引就锁全表;RR 级别默认是 Next-Key Lock,等值查询会根据情况退化。
把第四部分的加锁规则吃透,理解这些规则就足以处理常见问题。
4.11 rc & rr & 幻 & 读2
一条链串完所有概念。
起点:两种读
- 普通 SELECT → 快照读(Snapshot Read):不加锁,走 MVCC
- SELECT … FOR UPDATE、INSERT / UPDATE / DELETE → 当前读(Current Read):加锁,读最新已提交数据
这是一切的分水岭,后面所有东西都从这里分叉。
第二层:每种读用什么机制保护
- 快照读 → MVCC 保护
- 每行有隐藏列 trx_id + roll_pointer
- undo log 串成版本链
- ReadView 判断可见性
- RC:每条 SELECT 新建 ReadView
- RR:事务内只建一次,后续复用
- 当前读 → 锁保护
- Record Lock(锁住确切的行)
- Gap Lock(锁住间隙,不让插入)
- Next-Key Lock(Record + Gap)
第三层:RC 和 RR 到底差在哪
| RC | RR | |
|---|---|---|
| 快照读 | 每次 SELECT 新建 ReadView → 能看到别人中途提交的 | 第一次 SELECT 建一次,后续复用 → 看不到别人中途提交的 |
| 当前读 | 只有 Record Lock → 没有间隙锁 | Next-Key Lock(Record+Gap)→ 间隙也锁住 |
第四层:三大读异常,谁解决了谁
| 异常 | RC | RR |
|---|---|---|
| 脏读(读到未提交数据) | ✅ 解决(ReadView 只看已提交的) | ✅ 解决(同左) |
| 不可重复读(同一行两次读不一样) | ❌ 没解决(ReadView 每次重建,看到新值) | ✅ 解决(ReadView 固定,看不到新值) |
| 幻读(两次查行数不一样) | ❌ 没解决(没 Gap Lock 也没固定 ReadView) | ✅ 基本解决:快照读靠 MVCC 挡住,当前读靠 Gap Lock 挡住 |
第五层:RR 下幻读的唯一漏网之鱼
代码块收起展开
T1: SELECT(快照读) → 2行
T2: INSERT age=25; COMMIT;
T1: UPDATE(当前读) → 碰到了25这行,trx_id变成T1自己的
T1: SELECT(快照读) → 3行! 幻读!原因:
- UPDATE 是当前读 → 无视 ReadView → 改到了新插入的行
- 新版本的 trx_id = T1 自己
- 后续快照读判断 creator_trx_id 匹配 → 可见
这是唯一的漏网场景:快照读中间穿插了当前读。
纯快照读或纯当前读(加了锁)都不会幻读。
最终总结:一张决策图
你要执行一条 SELECT,先问需不需要加锁。
- 不需要 → 普通 SELECT → 快照读 → MVCC
- RC:新 ReadView → 能看到别人提交的
- RR:复用 ReadView → 看不到别人提交的
- 需要 → FOR UPDATE → 当前读 → 加锁
- RC:只加 Record Lock → 防不了幻读
- RR:加 Next-Key Lock → 间隙锁住,防幻读
- UPDATE / DELETE / INSERT → 天然就是当前读 → 同上加锁逻辑
记住两句话就够了:
- 快照读靠 MVCC 判”能不能看见”,RC 和 RR 的区别就是 ReadView 建几次
- 当前读靠锁判”让不让进来”,RC 和 RR 的区别就是有没有 Gap Lock
当前读
SELECT ... FOR UPDATE→ 当前读,加排他锁(X锁)SELECT ... LOCK IN SHARE MODE / FOR SHARE→ 当前读,加共享锁(S锁)
两者的区别在于放不放别人进来读:
- FOR SHARE(S 锁):我读的时候,别人也能读,但不能改
- FOR UPDATE(X 锁):我读的时候,别人不能读也不能改