在 MySQL (特别是 InnoDB 存储引擎) 中,锁机制分为表级锁和行级锁。你提到的 IS, IX, S, X 是四种核心的锁模式,它们共同构成了 InnoDB 的多粒度锁协议(Multiple Granularity Locking)。
以下是详细解析:
1. 四种锁的含义
表格
| 锁模式 | 全称 | 中文名称 | 作用范围 | 含义 |
|---|---|---|---|---|
| S | Share Lock | 共享锁 (读锁) | 行 / 表 | 允许事务读取一行数据。其他事务也可以获取该行的 S 锁,但不能获取 X 锁。 |
| X | Exclusive Lock | 排他锁 (写锁) | 行 / 表 | 允许事务更新或删除一行数据。其他事务不能获取该行的 S 锁或 X 锁。 |
| IS | Intention Share Lock | 意向共享锁 | 仅表级 | 表示事务打算在表中的某些行上获取 S 锁。它本身不锁住任何行,只是告诉别人:“我要读表里的某些行”。 |
| IX | Intention Exclusive Lock | 意向排他锁 | 仅表级 | 表示事务打算在表中的某些行上获取 X 锁。它告诉别人:“我要修改表里的某些行”。 |
关键点:意向锁(IS/IX)只存在于表级别,不存在于行级别。它们的存在是为了让数据库能快速判断“这张表里有没有行被锁住”,而不需要逐行检查。
2. 什么时候会加这些锁?
(1) S 锁 (共享锁)
-
触发场景:
- 普通查询:
SELECT ... LOCK IN SHARE MODE(MySQL 8.0+ 语法为SELECT ... FOR SHARE)。 - 某些隔离级别下的普通
SELECT(如可重复读隔离级别下,快照读不加锁,但当前读可能涉及)。
- 普通查询:
-
目的:保证读取期间数据不被修改。
(2) X 锁 (排他锁)
-
触发场景:
- 写操作:
INSERT,UPDATE,DELETE。 - 显式加锁查询:
SELECT ... FOR UPDATE。
- 写操作:
-
目的:保证修改期间数据不被读取(脏读)或被其他事务修改。
(3) IS 锁 (意向共享锁)
-
触发场景:
- 当你在表中的某一行加 S 锁 之前,InnoDB 会自动先在表级别加上 IS 锁。
- 例如:执行
SELECT ... FOR SHARE。
-
流程:先申请表级 IS 锁 -> 成功后 -> 再申请行级 S 锁。
(4) IX 锁 (意向排他锁)
-
触发场景:
- 当你在表中的某一行加 X 锁 之前,InnoDB 会自动先在表级别加上 IX 锁。
- 例如:执行
UPDATE,DELETE,INSERT或SELECT ... FOR UPDATE。
-
流程:先申请表级 IX 锁 -> 成功后 -> 再申请行级 X 锁。
3. 锁的共存情况 (兼容性矩阵)
这是理解锁机制的核心。如果两个锁兼容,它们可以同时存在;如果不兼容,后请求的锁会被阻塞。
表级锁兼容性矩阵 (针对 IS, IX, 表级 S, 表级 X)
表格
| 请求锁 \ 当前持有锁 | IS | IX | S (表锁) | X (表锁) |
|---|---|---|---|---|
| IS | ✅ 兼容 | ✅ 兼容 | ✅ 兼容 | ❌ 冲突 |
| IX | ✅ 兼容 | ✅ 兼容 | ❌ 冲突 | ❌ 冲突 |
| S (表锁) | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 |
| X (表锁) | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 |
-
解读:
- IS 和 IX 互不冲突:因为一个想读行,一个想改行,只要不是改同一行,理论上可以共存(具体行锁会处理细节)。
- 表级 S 锁与 IX 冲突:如果有人拿了全表的 S 锁(
LOCK TABLES t READ),其他人就不能拿 IX 锁(意味着不能去改任何行),反之亦然。 - 意向锁之间永远兼容:这就是意向锁设计的精妙之处。多个事务可以同时声明“我要读行”或“我要改行”,具体的冲突留给行锁去解决,而不需要在表级别就阻塞。
行级锁兼容性矩阵 (针对 S, X)
表格
| 请求锁 \ 当前持有锁 | S (行) | X (行) |
|---|---|---|
| S (行) | ✅ 兼容 | ❌ 冲突 |
| X (行) | ❌ 冲突 | ❌ 冲突 |
-
解读:
- S + S:大家都能读,没问题。
- S + X:有人要读,有人要改,冲突。
- X + X:两个人都要改,冲突。
4. 为什么要设计意向锁 (IS/IX)?
如果没有意向锁,当一个事务想要给整张表加锁(例如 LOCK TABLES ... WRITE,即表级 X 锁)时,数据库必须逐行扫描整张表,检查每一行是否已经被其他事务加了行锁(S 或 X)。如果表有千万行数据,这个检查过程会非常慢。
有了意向锁:
-
事务要加表级 X 锁时,只需要检查表上有没有 IS 或 IX 锁。
- 如果有 IS/IX,说明表里有行被锁住了,直接拒绝表锁请求(阻塞)。
- 如果没有,说明没有任何行被锁定,可以直接加表锁。
-
这极大地提高了判断“能否加表锁”的效率,从 O(N)O(N) 降低到了 O(1)O(1) 。
5. 总结示例
假设事务 A 执行了 SELECT ... FOR UPDATE (锁定 ID=1 的行):
-
步骤 1:事务 A 向 InnoDB 申请 表级 IX 锁。
- 检查:表上是否有冲突的锁(如表级 S 锁 or 表级 X 锁)?如果没有,授予 IX 锁。
-
步骤 2:事务 A 向 InnoDB 申请 行级 X 锁 (针对 ID=1)。
- 检查:ID=1 这一行是否有冲突的锁?如果没有,授予 X 锁。
此时,如果事务 B 想执行 LOCK TABLES t WRITE (申请表级 X 锁):
- InnoDB 检查表锁兼容性:发现表上已经有 IX 锁 (由事务 A 持有)。
- 查表可知:IX 与 表级 X 冲突。
- 结果:事务 B 被阻塞,无需扫描每一行就知道表不可用。
如果事务 C 想执行 SELECT ... FOR UPDATE (锁定 ID=2 的行):
- 申请 表级 IX 锁:与事务 A 的 表级 IX 锁 兼容 -> 成功。
- 申请 行级 X 锁 (针对 ID=2):该行无锁 -> 成功。
- 结果:事务 A 和 C 同时运行,互不干扰(因为它们锁的是不同的行)。
如果事务 D 想执行 SELECT ... FOR UPDATE (锁定 ID=1 的行):
- 申请 表级 IX 锁:兼容 -> 成功。
- 申请 行级 X 锁 (针对 ID=1):发现事务 A 已经持有了 行级 X 锁。
- 查行锁矩阵:X 与 X 冲突。
- 结果:事务 D 在行级别被阻塞。