# 锁
作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)
# InnoDB 锁的分类原理
# 锁的概述
InnoDB 是 MySQL 默认的事务型存储引擎,支持多种类型的锁机制,主要分为 锁的粒度 与 锁的类型(模式与算法) 两个维度来理解。
# 锁的粒度
锁的粒度指的是 锁定数据的范围大小,粒度越小并发性能越高,但开销越大。
| 锁粒度 | 描述 | 应用场景 |
|---|---|---|
| 表锁 | 锁住整张表 | 并发控制简单,但并发性差 |
| 行锁 | 锁住单行记录(本质是索引) | 并发性好,开销大,InnoDB 默认使用 |
# 锁的类型
按锁的模式分
锁模式 粒度 作用 说明 共享锁(S) 行级 允许多个事务读取同一数据,但不能修改 通常用于 SELECT ... LOCK IN SHARE MODE排他锁(X) 行级 只允许一个事务读取并修改数据 用于 SELECT ... FOR UPDATE、DML 操作意向锁(IS / IX) 表级 表示事务打算对某些行加 S/X 锁,用于协调表锁和行锁 InnoDB 自动加锁,无需手动处理 按 实现算法(Locking Algorithm) 分
锁算法类型 描述 Record Lock(记录锁) 锁住一条索引记录,防止其他事务修改或删除该记录 Gap Lock(间隙锁) 锁住两条记录之间的“间隙”,防止插入新数据(避免幻读) Next-Key Lock(临键锁) Record Lock + Gap Lock,锁住当前记录及其前后间隙 Insert Intention Lock(插入意向锁) 一种特殊的 Gap Lock,多个插入操作之间不互相冲突,提高插入并发性能
# 锁的模式
InnoDB 中常见的锁包括共享锁、排它锁和意向锁。
# 共享锁(Shared Lock,S 锁)
共享锁是行级锁,又称为读锁。多个事务可以同时获取同一行的共享锁,但不能有其他事务对其加排它锁。
- 加锁方式:
SELECT ... LOCK IN SHARE MODE - 解锁方式:提交事务或回滚事务时释放。
# 排它锁(Exclusive Lock,X 锁)
排它锁也是行级锁,又称为写锁。当某个事务持有排它锁时,其他事务无法再对这行加任何锁(包括读锁和写锁)。
- 加锁方式:
- 自动加锁:执行
INSERT、DELETE、UPDATE时自动加排它锁。 - 手动加锁:
SELECT ... FOR UPDATE
- 自动加锁:执行
- 解锁方式:提交事务或回滚事务。
# 意向锁(Intent Lock)
意向锁是表级锁,用于表示事务打算对表中某些行加什么类型的锁,由 InnoDB 自动加锁。
- 意向共享锁(IS):表示打算对某些行加共享锁。
- 意向排它锁(IX):表示打算对某些行加排它锁。
作用: 避免加表锁时扫描整个表判断是否已有行锁,起到加表锁的优化作用。
注意事项
在一个事务中,如果先加共享锁,再尝试对同一行加排它锁,会因锁升级失败而造成等待。例如:
-- 事务 1
BEGIN;
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
UPDATE user SET name = 'Tom' WHERE id = 1; -- 等待自身读锁释放,陷入阻塞或死锁
2
3
4
# 行锁的原理(基于索引实现)
InnoDB 的行锁是基于 索引 来实现的,而不是直接锁住某一行的物理记录。下面通过几个例子说明这一点:
准备三张表:
t1:没有任何索引t2:主键为idt3:唯一索引为name
没有索引(t1)
事务1 事务2 begin; SELECT * FROM t1 WHERE id =1 FOR UPDATE; select * from t1 where id=3 for update; -- 被阻塞 INSERT INTO t1(`id, 'name') VALUES (5, '5'); -- 被阻塞步骤说明:
- 事务 1 通过
WHERE id = 1加排它锁。 - 事务 2 试图对另一行(id = 3)加锁,甚至插入新行(id = 5),都被阻塞。
原因分析:
虽然看起来只锁了 id=1 的行,但由于 没有索引,InnoDB 只能扫描整张表并对所有记录加锁,即“锁表”。
✅ 结论:在未命中索引的情况下,行锁会退化为表锁。
- 事务 1 通过
有主键索引 id(t2)
事务1 事务2 begin; select * from t2 where id=1 for update; select * from t2 where id=1 for update; -- 被阻塞 select * from t2 where id=4 for update; // OK 步骤说明:
- 事务 1 对主键为 id=1 的记录加锁。
- 事务 2 尝试对相同主键加锁 → 阻塞;
- 事务 2 对不同主键加锁 → 成功。
原因分析:
主键查询是基于聚簇索引的,能直接定位到具体记录,加的是精确的行锁。
✅ 结论:主键索引命中后,加的是精确行锁,互不干扰。
唯一索引为 name(t3)
事务1 事务2 begin; select * from t3 where name= '4' for update; select * from t3 where name = '4' for update; -- 被阻塞 select * from t3 where id = 4 for update; -- 被阻塞 步骤说明:
- 事务 1 通过唯一索引
name = '4'加锁; - 事务 2 尝试对相同
name加锁 → 阻塞; - 事务 2 尝试通过
id = 4(主键)加锁 → 也被阻塞。
原因分析:
查询
name='4'命中的是二级索引。InnoDB 为了读取完整记录,需要回表到聚簇索引查主键,再取出整行数据。这一过程会导致:- 对二级索引
name = '4'加锁; - 同时对回表时访问到的主键 id = 4 也加锁。
✅ 结论:访问二级索引时,会同时锁住对应的主键记录,可能导致额外的锁冲突。
两个核心问题解析:
- 为什么没有索引会锁全表? 因为不能通过索引快速定位记录,InnoDB 被迫对隐藏的聚簇索引进行全表扫描和加锁(扫描叶子节点链表),实际上是对所有记录都上锁。
- 为什么二级索引加锁会连主键索引也被锁住? 因为二级索引的叶子节点只存储主键值,InnoDB 为了获取完整行,需要通过主键“回表”,因此对主键也加锁,这就是造成主键也被阻塞的根本原因。
- 事务 1 通过唯一索引
# 锁的算法
InnoDB 为了实现高并发下的数据一致性,设计了多种锁的算法,主要包括:记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock)。这些锁机制在不同的 SQL 场景下自动触发,极大地影响了并发性能与事务安全。
# 什么是记录锁(Record Lock)?
定义: 记录锁是加在索引记录上的锁,只锁住某一行数据。这是最常见的一种行级锁。
生效场景:
- 精确等值匹配主键或唯一索引,并命中索引。
示例:
数据行:
| id = 8 | id = 9 | id = 10 | id = 11 | id = 12 |
SQL:
SELECT * FROM user WHERE id = 10 FOR UPDATE;
-- 加锁:[🔒10]
-- 说明:只锁住 id=10 这一行,其他记录可并发操作。
2
3
4
5
6
7
8
# 什么是间隙锁(Gap Lock)?
定义: 间隙锁是加在索引之间的“空隙”上的锁,不锁具体记录,只锁住“某段范围”,防止其他事务插入数据。
生效场景:
范围查询 + 加锁语句(
FOR UPDATE/LOCK IN SHARE MODE)。没有命中记录时,为了防止“幻读”,锁住“空隙”。
示例:
数据行:
| id = 8 | id = 9 | id = 11 | id = 12 |
SQL:
SELECT * FROM user WHERE id > 9 AND id < 11 FOR UPDATE;
-- 加锁:(🔒9, 11)
-- 说明:虽然 id=10 不存在,但锁住 9~11 之间的“空隙”,禁止插入 id=10。
2
3
4
5
6
7
8
# 什么是临键锁(Next-Key Lock)?
定义: 临键锁是记录锁 + 间隙锁的组合,锁住某行记录的同时,也锁住它前面的间隙。
生效场景:
- 可重复读(RR)隔离级别下的大多数加锁查询。
- 范围查询中锁住已存在的记录(防止读到新插入的数据,解决幻读问题)。
示例:
数据行:
| id = 8 | id = 9 | id = 10 | id = 11 | id = 12 |
SQL:
SELECT * FROM user WHERE id >= 10 AND id < 12 FOR UPDATE;
-- 加锁:(9,10] (10,11] (11,12)
🔒 🔒 🔒
-- 说明:范围查询命中, 锁住前间隙锁(9, 10], 当前行(10, 11], 以及next(11,12)
-- 每个临键锁锁住前间隙 + 当前行,如 (9,10] 锁住 id=10 和它前的间隙。主要是防止前面有数据插入导致幻读
2
3
4
5
6
7
8
9
10
# 隔离级别的实现(InnoDB)
# Read Uncommitted(读未提交)
- 普通 SELECT:不使用 MVCC,读到未提交的数据(脏读),不加任何锁。
- 风险:容易读到其他事务尚未提交的数据,可能导致业务错误。
# Read Committed(读已提交)
- 普通 SELECT:使用 MVCC(快照读),单个事务中每次查询都会生成ReadView, 每次读都是最新已提交版本。
- 加锁 SELECT(如
SELECT ... FOR UPDATE):- 使用 记录锁(Record Lock)
- 不加 Gap Lock,也不加临键锁
- INSERT/UPDATE/DELETE:会使用记录锁(锁定具体行)
✅ 优点:避免脏读 ⚠️ 问题:可能出现不可重复读和幻读
# Repeatable Read(可重复读)——InnoDB 默认级别
- 普通 SELECT:使用 MVCC(快照读), 单个事务中只有第一次查询生成ReadView, 后面查询使用相同的ReadView。
- 多次读取结果一致,除非事务主动提交
- 加锁 SELECT:
SELECT ... FOR UPDATE、SELECT ... IN SHARE MODE- 使用 当前读(Current Read)
- 会触发 记录锁 + 间隙锁 + 临键锁(Next-Key Lock),防止幻读
- UPDATE / DELETE:
- 同样属于当前读,也使用上述锁机制
✅ 优点:可重复读,避免幻读 ⚠️ 缺点:并发性比 RC 略低
# Serializable(串行化)
所有的 SELECT 自动变为:
SELECT ... LOCK IN SHARE MODE1无论是否加锁,都会进行加锁读取(共享锁或排他锁),使并发事务串行执行。
会触发大量锁等待或死锁,性能最差,但最安全。
✅ 优点:完全避免并发问题,严格一致性 ⚠️ 缺点:性能极差,基本不建议在高并发下使用