04.1 - InnoDB 锁分类
定位:理解 InnoDB 的锁类型和兼容性矩阵——这是分析所有锁问题的基础 面试高频度:⭐⭐⭐⭐ 考查方式:共享锁 vs 排他锁、意向锁的作用、锁兼容性矩阵、隐式锁
一、这是什么?为什么需要它?
是什么
InnoDB 的锁是一套多粒度的并发控制机制,按粒度分为行锁和表锁,按模式分为共享锁和排他锁。
为什么需要不同的锁类型?
一个简单的"一行"加锁逻辑,实际上需要解决三个问题:
- 行与行之间怎么互斥? → 行锁(S/X Lock)
- 表和行之间的冲突怎么快速检测? → 意向锁(IS/IX Lock)
- 范围插入怎么阻止? → 间隙锁(Gap Lock,下一节)
核心 insight:锁类型的设计宗旨是"在保证安全的前提下,尽可能减少阻塞范围"。用最小粒度的锁解决最特定问题。
二、原理拆解
2.1 锁模式:共享锁 vs 排他锁
共享锁(S Lock)
sql
-- 显式加共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 或 MySQL 8.0 语法:
SELECT * FROM users WHERE id = 1 FOR SHARE;共享锁的特性:
- 允许其他事务读取同一行(也加 S 锁)
- 阻止其他事务修改该行(X 锁会被阻塞)
排他锁(X Lock)
sql
-- 显式加排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- DML 语句自动加排他锁
UPDATE users SET name = 'new' WHERE id = 1; -- 自动加 X 锁
DELETE FROM users WHERE id = 1; -- 自动加 X 锁
INSERT INTO users ...; -- 自动加 X 锁排他锁的特性:
- 阻止其他事务读取或修改该行
- 互斥性最强
兼容性矩阵
| 当前持有 | 请求 S | 请求 X |
|---|---|---|
| S | ✅ 兼容 | ❌ 等待 |
| X | ❌ 等待 | ❌ 等待 |
sql
-- 演示 S 锁兼容
-- 会话 A
BEGIN;
SELECT * FROM users WHERE id = 1 FOR SHARE; -- 加 S 锁
-- 会话 B(兼容)
SELECT * FROM users WHERE id = 1 FOR SHARE; -- ✅ 也能加 S 锁
-- 两个 S 锁共存,互不阻塞
-- 会话 C(不兼容)
UPDATE users SET name = 'new' WHERE id = 1; -- ❌ 等待 X 锁
-- 被会话 A 的 S 锁阻塞,直到会话 A COMMIT2.2 意向锁(Intention Locks)
为什么需要意向锁?
假设事务 A 在 users 表的 id=1 和 id=2 上加了行锁,现在事务 B 想要 LOCK TABLES users WRITE(加表级 X 锁)。
没有意向锁时:
sql
-- 事务 B 必须扫描整张表,检查是否有任何行被加锁
-- users 表有 1000 万行?那就扫描 1000 万行!有意向锁时:每个事务在加行锁之前,先给表加一个"预告"标记。
意向锁的两种类型
| 锁 | SQL 表示 | 含义 |
|---|---|---|
| 意向共享锁 (IS) | SELECT ... LOCK IN SHARE MODE 前自动加 | "我要在行上加 S 锁" |
| 意向排他锁 (IX) | UPDATE/DELETE/INSERT 前自动加 | "我要在行上加 X 锁" |
完整的锁兼容性矩阵(包含意向锁):
| 当前持有 | IS | IX | S | X |
|---|---|---|---|---|
| IS | ✅ | ✅ | ✅ | ❌ |
| IX | ✅ | ✅ | ❌ | ❌ |
| S | ✅ | ❌ | ✅ | ❌ |
| X | ❌ | ❌ | ❌ | ❌ |
关键规则:
- IS 和 IX 是兼容的(只是"预告",不真的锁行)
- IX 和 S 不兼容(有人要写,就不能读锁整表)
- 意向锁之间总是兼容的(多个事务可以同时在行上做不同操作)
例:事务 A 持有 IX 锁(正在修改某些行)
事务 B 请求 S 锁(想锁表查数据)
→ ❌ 被阻塞,因为 IX 和 S 不兼容
→ 但 B 不需要扫描所有行来判断是否有行锁2.3 自增锁(AUTO-INC Lock)
用于自增主键的特殊锁:
传统模式 (innodb_autoinc_lock_mode=0):
┌──────────────────────────────┐
│ INSERT INTO t (auto_col) ... │
│ → 加 AUTO-INC 表级锁 │
│ → 语句结束后释放 │
│ → 保证自增值连续且有序 │
└──────────────────────────────┘
连续模式 (innodb_autoinc_lock_mode=1, 默认):
┌──────────────────────────────┐
│ 普通 INSERT: 不加锁 │
│ → 用轻量级 mutex 取自增值 │
│ 批量 INSERT: 加 AUTO-INC 锁 │
│ → 如 INSERT ... SELECT │
└──────────────────────────────┘
交错模式 (innodb_autoinc_lock_mode=2, 8.0 默认):
┌──────────────────────────────┐
│ 全部用轻量级 mutex │
│ → 性能最高 │
│ → 批量插入自增值可能不连续 │
│ → 但主从复制可能不一致 │
└──────────────────────────────┘2.4 隐式锁(Implicit Lock)
InnoDB 的优化:不主动创建锁对象,而是在需要时才"升级"为显式锁。
INSERT INTO users VALUES (1, 'Tom');
↓
插入完成后,该行的 DB_TRX_ID=200
↓
事务 B 想修改同一条记录,检查 DB_TRX_ID
↓
发现 DB_TRX_ID=200 的事务未提交
↓
自动将事务 A 的插入操作的"隐式锁"转换为"显式锁"
↓
事务 B 开始等待好处:如果改行只有当前事务访问,不需要锁对象开销。只有发生冲突时才创建锁结构。
三、图解全景
InnoDB 锁兼容性矩阵
谁持有锁 → IS IX S X
┌──────────┐────────────────────────────────────────
│ IS │ ✅ ✅ ✅ ❌
│ IX │ ✅ ✅ ❌ ❌
│ S │ ✅ ❌ ✅ ❌
│ X │ ❌ ❌ ❌ ❌
└──────────┘
✅ = 可获取锁(兼容)
❌ = 阻塞等待(不兼容)
注意:
- IS/IX 之间永远是兼容的(这只是"预告",不是真正的锁)
- S/S 是兼容的(读读不互斥)
- X 和任何锁都不兼容(写独占)
- IX 阻止了表级的 S 锁(让我写完了你再读全表)四、实战验证
查看锁信息
sql
-- MySQL 8.0: 查看当前锁信息
SELECT * FROM performance_schema.data_locks\G
-- 可以查看:
-- ENGINE_TRANSACTION_ID
-- OBJECT_SCHEMA/TABLE
-- LOCK_TYPE: TABLE / RECORD
-- LOCK_MODE: S / X / IX / IS
-- LOCK_STATUS: GRANTED / WAITING
-- LOCK_DATA: 行数据(加在哪条记录上)
-- MySQL 8.0: 查看锁等待
SELECT * FROM performance_schema.data_lock_waits\G
-- 5.7 及更早版本
SHOW ENGINE INNODB STATUS\G
-- 看 LATEST DETECTED DEADLOCK 和 TRANSACTIONS 部分验证锁互斥
sql
-- 准备数据
CREATE TABLE test_lock (
id INT PRIMARY KEY,
val INT
);
INSERT INTO test_lock VALUES (1, 100), (2, 200), (3, 300);
-- 测试 1: S 锁兼容 S 锁
-- 会话 A
BEGIN;
SELECT * FROM test_lock WHERE id = 1 FOR SHARE; -- S 锁
-- 会话 B
SELECT * FROM test_lock WHERE id = 1 FOR SHARE; -- ✅ 兼容(两个 S 锁)
-- 测试 2: S 锁阻塞 X 锁
-- 会话 A 保持
-- 会话 C
UPDATE test_lock SET val = 999 WHERE id = 1; -- ❌ 等待 X 锁
-- 会话 A
COMMIT; -- 释放 S 锁
-- 会话 C 立即执行成功
-- 测试 3: IX 和 S 的冲突
-- 会话 A
BEGIN;
UPDATE test_lock SET val = 150 WHERE id = 2; -- 自动加 IX + X
-- 会话 B
LOCK TABLES test_lock READ; -- 尝试加表级 S 锁
-- ❌ 被 IX 阻塞五、面试视角
| 追问 | 答案要点 |
|---|---|
| S 锁和 X 锁的区别? | S 锁读读不互斥,X 锁任何锁都不兼容 |
| 意向锁有什么用? | 快速判断表级锁是否与行级锁冲突,避免扫描全表检查行锁 |
| LOCK IN SHARE MODE 和 FOR UPDATE 的区别? | 前者加 S 锁(允许其他人读),后者加 X 锁(阻止任何人读写) |
| INSERT 会加什么锁? | 隐式锁(通过 DB_TRX_ID 实现),冲突时转为显式 X 锁 |
| 自增锁有几种模式? | 0=传统表锁,1=连续(默认,普通 INSERT 用轻量锁),2=交错(8.0 默认,性能最高) |
📚 相关链接
- 行锁深入:**行锁与间隙锁**
- 死锁分析:**死锁分析与排查**
- 前置知识:**事务隔离级别**
- ← 返回 **锁机制索引**