什么是意向锁(Intention Lock)?IS / IX 锁如何避免行锁与表锁冲突?
What is an Intention Lock? How do IS / IX locks avoid conflicts between row locks and table locks
考察要点
这道题面试官想看你是否深刻理解 InnoDB 的多粒度锁(Multi-Granularity Locking)机制。核心考察点在于:
- 问题根源:你是否明白为什么需要一种机制来协调表锁和行锁,即如果没有意向锁,检查锁冲突的成本有多高。
- 核心作用:你是否能准确描述意向锁作为一种“信号”或“标志”,其核心价值在于快速判断是否存在表级和行级的锁冲突,从而避免遍历全表。
- 锁兼容性:你是否熟知意向锁(IS/IX)与共享锁(S)/排他锁(X)之间的兼容关系,这是理解并发控制的关键。
核心概念
意向锁(Intention Lock)是 InnoDB 存储引擎中的一种表级锁,它的存在是为了协调更高粒度的表锁与更低粒度的行锁之间的冲突。意向锁本身不直接锁定任何数据,而是表明一个事务意图在稍后对表中的某些行加 S 锁或 X 锁。
1. 为什么需要意向锁?
想象一个没有意向锁的场景:
- 事务 T1 对表
t中的某一行r加了行级排他锁(X 锁)。sql1-- T12SELECT * FROM t WHERE id = 10 FOR UPDATE; -- 对 id=10 的行加 X 锁 - 事务 T2 此时想对整个表
t加表级共享锁(S 锁)。sql1-- T22LOCK TABLES t READ; -- 尝试对表 t 加 S 锁
为了保证数据一致性,T2 必须检查表 t 中是否存在任何行级排他锁(X 锁),因为表级 S 锁与行级 X 锁是冲突的。在没有意向锁的情况下,T2 唯一的办法就是遍历表 t 的每一行,检查是否存在行锁。如果表 t 有上亿行数据,这个检查过程的开销将是灾难性的,并发性能会急剧下降。
意向锁的解决方案: 引入意向锁后,流程变为:
- T1 在对行
r加 X 锁之前,必须先在表t上加一个意向排他锁(IX)。 - T2 尝试对表
t加 S 锁时,它不需要遍历所有行,只需要检查表t上是否存在意向排他锁(IX)。 - 因为 T1 已经持有了表
t的 IX 锁,而表级 S 锁与 IX 锁是冲突的,所以 T2 的加锁请求会被阻塞。
这个检查从 (N为行数)的复杂度降低到了 ,极大地提升了并发性能。
2. 意向锁的类型
意向锁分为两种,均是表级锁:
-
意向共享锁 (Intention Shared Lock, IS)
- 含义:事务打算对表中的某些行设置共享锁(S 锁)。
- 触发 SQL:
SELECT ... LOCK IN SHARE MODE; - 加锁过程:在对行加 S 锁之前,必须先获得该表的 IS 锁。
-
意向排他锁 (Intention Exclusive Lock, IX)
- 含义:事务打算对表中的某些行设置排他锁(X 锁)。
- 触发 SQL:
SELECT ... FOR UPDATE;,INSERT,UPDATE,DELETE。 - 加锁过程:在对行加 X 锁之前,必须先获得该表的 IX 锁。
3. 锁兼容性矩阵
这是理解意向锁如何工作的核心。下表展示了各种锁类型之间的兼容关系(行代表已持有的锁,列代表请求的锁):
| 已持有 \ 请求 | X (表级) | S (表级) | IX (表级) | IS (表级) |
|---|---|---|---|---|
| X (表级) | 冲突 | 冲突 | 冲突 | 冲突 |
| S (表级) | 冲突 | 兼容 | 冲突 | 兼容 |
| IX (表级) | 冲突 | 冲突 | 兼容 | 兼容 |
| IS (表级) | 冲突 | 兼容 | 兼容 | 兼容 |
核心解读:
- 意向锁之间互相兼容 (IX-IX, IS-IS, IX-IS):两个不同的事务可以同时持有对同一张表的 IX 锁,因为它们可能意图锁定的是不同的行,这并不会立即产生冲突。真正的冲突发生在它们试图锁定同一行时,由行锁来处理。
- 意向锁与表锁的冲突关系:
- 请求表级 S 锁时,必须不能有事务持有该表的表级 X 锁或意向排他锁 (IX)。因为 IX 锁意味着有事务正在或将要修改行,这与读取整个表的快照是冲突的。
- 请求表级 X 锁时,必须不能有事务持有该表的任何锁(X, S, IX, IS)。
常见低分回答
-
混淆意向锁和行锁:“IX 锁是加在行上的排他锁”。
- 错误点:这是概念性错误。IX/IS 锁是表级锁,它们只是一个意图的标志,而不是作用于具体数据行的锁。真正的行锁是
Record Lock,Gap Lock,Next-Key Lock。
- 错误点:这是概念性错误。IX/IS 锁是表级锁,它们只是一个意图的标志,而不是作用于具体数据行的锁。真正的行锁是
-
认为意向锁会阻塞其他意向锁:“一个事务拿了表的 IX 锁,另一个事务就不能再拿 IX 锁了”。
- 错误点:从兼容性矩阵可知,意向锁之间是完全兼容的。这正是允许多个事务并发更新表中不同行的基础。如果意向锁互斥,那么多粒度锁就失去了意义,退化成了表锁。
-
只说定义,不说“为什么”:“意向锁就是 IS 和 IX 两种,分别在
SELECT...SHARE MODE 和 FORUPDATE时加”。- 错误点:这样的回答只停留在表面背诵,没有展现出对问题本质的理解。高分回答必须解释意向锁是为了解决什么问题(避免全表扫描检查锁冲突),以及它是如何解决的(通过 的表级锁状态检查)。
-
无法解释
LOCK TABLES和意向锁的关系:当被问到LOCK TABLES t WRITE为何会和SELECT ... FOR UPDATE冲突时,无法清晰地解释是因为前者请求表级 X 锁,后者持有表级 IX 锁,而 X 和 IX 在表级别是冲突的。
工程实践与优化
- 隐式加锁:在日常的
INSERT,UPDATE,DELETE操作中,InnoDB会自动为我们加上 IX 锁,开发者通常无需关心。SELECT ... FOR UPDATE/SHARE MODE也是如此。 - 避免显式表锁:在
InnoDB中,应极力避免使用LOCK TABLES。这会严重降低并发性,因为它会与所有意图修改或读取行的事务发生冲突。InnoDB的行级锁和MVCC机制已经能很好地处理并发,LOCK TABLES往往是性能杀手。 - 问题排查:当遇到死锁或长时间的锁等待时,可以通过
SHOW ENGINE INNODB STATUS命令来查看锁信息。在输出的LATEST DETECTED DEADLOCK或TRANSACTIONS部分,你会看到类似lock_mode IX,lock_mode IS waiting的信息。这能帮助你理解事务正在等待什么锁,以及是哪个事务持有了冲突的锁。例如,看到一个事务在等待lock type TABLE lock_mode S,而阻塞它的事务持有lock_mode IX,就能快速定位是表锁与行锁意向发生了冲突。 - DDL 操作的锁:
ALTER TABLE等 DDL 操作通常需要获取表上的元数据锁(Metadata Lock),这会等待所有持有该表意向锁的事务结束。因此,在一个高并发更新的表上执行 DDL,很容易导致长时间等待,甚至阻塞整个业务。通常需要选择业务低峰期执行,或使用pt-online-schema-change、gh-ost等在线 DDL 工具。
常见追问与延伸题目
-
追问:除了 IS/IX 锁,
InnoDB中还有哪些重要的锁?它们分别解决了什么问题?- 回答要点:
Record Lock(记录锁):锁定单个索引记录,这是最基本的行锁。Gap Lock(间隙锁):锁定一个开区间范围,但不包括记录本身。它解决了可重复读(RR)隔离级别下的“幻读”问题,防止其他事务在这个范围内INSERT新数据。Next-Key Lock(临键锁):Record Lock+Gap Lock的组合,锁定一个左开右闭的区间。这是InnoDB在 RR 级别下默认的行锁算法,既锁定了记录,也锁定了记录前的间隙。- AUTO-INC Lock (自增锁):一种特殊的表级锁,用于并发插入时生成连续的自增 ID。在 MySQL 8.0 中通过引入更轻量级的互斥量(mutex)进行了优化,提高了并发插入性能。
- 回答要点:
-
追问:
MyISAM存储引擎的锁机制是怎样的?和InnoDB的意向锁机制有何根本区别?- 回答要点:
MyISAM:只支持表级锁。当一个会话SELECT时,它会对所有涉及的表加读锁(共享锁);当UPDATE或INSERT时,它会对表加写锁(排他锁)。- 根本区别:
MyISAM不支持行级锁,因此也就不存在表锁与行锁的冲突问题,自然也不需要意向锁这种多粒度协调机制。其优点是实现简单,开销小;缺点是写操作的并发性能极差,一个写操作会阻塞所有其他读写操作。而InnoDB通过引入行级锁和意向锁,实现了更细粒度的并发控制,大大提高了混合读写场景下的性能。
- 回答要点:
-
追问:
SELECT ... FOR UPDATE在加锁时,如果WHERE条件中的列没有索引,会发生什么?- 回答要点:
- 锁升级:如果
WHERE条件没有走索引,MySQL 将无法定位到具体的行,只能进行全表扫描。 - 加锁范围:为了保证数据一致性,
InnoDB会对扫描过程中遇到的所有行都加上行级 X 锁。 - 表锁效应:虽然加的是行锁,但由于每一行都被锁定了,其效果等同于加了表锁,会严重阻塞其他所有对该表的并发操作。
- 意向锁:在这个过程中,事务依然会先获取表级别的 IX 锁,然后再对每一行加 X 锁。这个行为本身没有变,但锁定的范围从“精准的几行”扩大到了“全表所有行”。这是开发中必须避免的严重性能问题。
- 锁升级:如果
- 回答要点: