Skip to content

04.1 - InnoDB 锁分类

定位:理解 InnoDB 的锁类型和兼容性矩阵——这是分析所有锁问题的基础 面试高频度:⭐⭐⭐⭐ 考查方式:共享锁 vs 排他锁、意向锁的作用、锁兼容性矩阵、隐式锁


一、这是什么?为什么需要它?

是什么

InnoDB 的锁是一套多粒度的并发控制机制,按粒度分为行锁和表锁,按模式分为共享锁和排他锁。

为什么需要不同的锁类型?

一个简单的"一行"加锁逻辑,实际上需要解决三个问题:

  1. 行与行之间怎么互斥? → 行锁(S/X Lock)
  2. 表和行之间的冲突怎么快速检测? → 意向锁(IS/IX Lock)
  3. 范围插入怎么阻止? → 间隙锁(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 COMMIT

2.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 锁"

完整的锁兼容性矩阵(包含意向锁):

当前持有ISIXSX
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 默认,性能最高)

📚 相关链接

  • 行锁深入:**行锁与间隙锁**
  • 死锁分析:**死锁分析与排查**
  • 前置知识:**事务隔离级别**
  • ← 返回 **锁机制索引**

Knowledge4J — Java 知识库