这次面试被问到了"说一下意向锁",以及后续的"MySQL 里的锁是加在什么上的,底层是如何做的"。复盘时发现,我对意向锁的理解停留在表面,尤其是 S、X、IS、IX 的兼容关系部分,回答得太笼统,缺乏深度。以下是重新整理的思路,力求把问题讲透。
意向锁的本质与作用
意向锁(Intention Lock)是 MySQL InnoDB 引擎中一种表级锁 ,它的核心作用是提高锁冲突检测的效率 ,在行锁和表锁共存的场景下充当"桥梁"。InnoDB 支持细粒度的行锁,但也允许表级锁(比如显式的 LOCK TABLES
或某些 DDL 操作)。如果一个事务想对整个表加锁,逐行检查是否有锁冲突显然不现实,意向锁就解决了这个问题。
意向锁有两种:
- 意向共享锁(IS Lock):表示事务打算在某些行上加共享锁(S Lock)。
- 意向排他锁(IX Lock):表示事务打算在某些行上加排他锁(X Lock)。
它的逻辑是:当事务在行上加锁前,先在表上加一个对应的意向锁,作为"意向声明"。这样,其他事务在尝试加表级锁时,只需检查表上的锁状态,就能快速判断是否会冲突。
S、X、IS、IX 的兼容关系(全面解析)
面试时我提到兼容性矩阵,但只列了部分情况,显得不够严谨。实际上,S(共享锁)、X(排他锁)、IS(意向共享锁)、IX(意向排他锁)之间的兼容关系可以用一个完整的矩阵来表示:
S | X | IS | IX | |
---|---|---|---|---|
S | 是 | 否 | 是 | 否 |
X | 否 | 否 | 否 | 否 |
IS | 是 | 否 | 是 | 是 |
IX | 否 | 否 | 是 | 是 |
逐项解析:
- S 和 S 兼容:多个事务可以同时持有表级共享锁(S Lock),因为共享锁允许并发读。
- S 和 X 不兼容:表级共享锁和排他锁冲突,因为 X 锁要求独占资源。
- S 和 IS 兼容:IS 表示"打算加行级 S 锁",不会影响表级 S 锁,因为两者都是读操作的意向。
- S 和 IX 不兼容:IX 表示"打算加行级 X 锁",而表级 S 锁不允许任何写操作,所以冲突。
- X 和任何锁都不兼容:表级 X 锁是最高权限的锁,无论是 S、X、IS 还是 IX,都会被阻塞。
- IS 和 IS 兼容:多个事务都可以声明"打算加行级 S 锁",互不干扰。
- IS 和 IX 兼容:IS 和 IX 是表级意向锁,本身不直接锁资源,只表示意向,所以兼容。但如果具体到行级,S 和 X 会冲突。
- IX 和 IX 兼容:同样,IX 只是表级意向,多个事务可以同时声明"打算加行级 X 锁"。
为什么 IS 和 IX 兼容?
这点我之前没讲清楚。IS 和 IX 的兼容性只在表级成立,因为意向锁本身不锁具体数据,只是"声明"。真正的冲突发生在行级锁上。比如:
- 事务 A 加了 IX,想对行 R1 加 X 锁。
- 事务 B 加了 IS,想对行 R2 加 S 锁。 表级上看,IX 和 IS 兼容,事务可以继续。但如果 R1 和 R2 是同一行,行级的 X 和 S 会冲突,导致阻塞。
举个实际例子
假设有个表 users
,字段有 id
和 name
:
- 事务 A 执行
SELECT ... FOR SHARE
(加行级 S 锁),InnoDB 会在表上加 IS 锁。 - 事务 B 执行
UPDATE ... WHERE id = 5
(加行级 X 锁),表上加 IX 锁。 - 事务 C 想执行
LOCK TABLES users WRITE
(表级 X 锁)。
此时,事务 C 检查表上的锁,发现有 IS 和 IX,表明表内有行被锁,直接阻塞,不需要逐行检查。这就是意向锁的高效之处。
MySQL 里的锁是加在什么上的?
锁的加锁对象因类型而异:
- 表锁:直接加在表上,比如意向锁(IS、IX)或显式表锁。
- 行锁 :加在索引记录上。InnoDB 的行锁依赖索引实现:
- 有索引时,锁加在对应的索引项上(主键、唯一索引或二级索引)。
- 无索引时,退化到锁整个表,或者用隐藏的聚簇索引(row id)。
- 间隙锁和 Next-Key Lock:锁住索引间的范围,防止幻读。
比如 UPDATE users SET name = 'Tom' WHERE id = 5
,锁会加在 id = 5
的索引记录上。如果 id
没索引,InnoDB 会扫描全表,锁可能退化为表锁。
底层是如何做的?
这部分我之前回答得很弱,现在补充一些细节:
- 锁数据结构 :InnoDB 在内存中维护一个锁管理器,每个锁对象包含:
- 锁类型(S、X、IS、IX 等)。
- 锁资源(表或索引记录的指针)。
- 事务 ID。
- 加锁过程 :
- 行锁通过 B+ 树定位索引记录,在内存中为该记录创建锁结构。
- 表锁(包括意向锁)记录在表的元数据中。
- 冲突检测:锁管理器用哈希表或链表跟踪所有锁,当新锁请求到来时,检查兼容性矩阵。
- 等待与死锁 :
- 如果锁冲突,请求者进入等待队列。
- InnoDB 用等待图(wait-for graph)检测死锁,比如事务 A 等 B,B 等 A,会回滚一个事务。
- 存储开销:锁信息存在内存中,事务提交后释放。如果内存不足,可能触发 OOM。
底层具体代码我没研究过,但大致猜想是用 C 实现的锁链表或哈希表,结合 B+ 树的索引定位机制。
反思与改进
这次复盘让我发现,S、X、IS、IX 的兼容关系不只是背矩阵,还得理解背后的设计逻辑。意向锁看似简单,但结合行锁和表锁的协作场景,复杂度一下就上来了。下次面试如果再遇到,我会先讲定义,再用矩阵和例子把兼容性讲透,最后简单提一下底层实现。接下来打算看一下 InnoDB 源码的锁模块,至少搞清楚锁结构的定义,争取下次不留遗憾。