锁的分类:表锁、行锁、页锁与意向锁

在上一篇中,我们深入探讨了事务的 ACID 特性和四种隔离级别。其中提到,隔离性是通过MVCC 共同实现的。如果把数据库比作一个繁忙的办公室,锁就是"使用中"的牌子------它告诉其他事务:这个数据我正在用,请你等一等。

本文将从宏观到微观,系统梳理 MySQL InnoDB 中各种锁的分类和使用方式,让你彻底搞清楚:

  • 按粒度分的表锁、行锁、页锁
  • 按模式分的共享锁(S)与独占锁(X)
  • InnoDB 特有的意向锁(IS/IX)及其作用
  • 记录锁、间隙锁、临键锁的概念
  • 自增锁与插入意向锁
  • 实战:观察各种锁的现象

1. 按锁的粒度分类

锁粒度决定了锁定数据范围的大小,直接影响并发度和系统开销。

1.1 表锁(Table Lock)

表锁是 MySQL 中最粗粒度的锁,它会锁定整张表。MyISAM 引擎主要使用表锁,InnoDB 在特定情况下也会使用(如 ALTER TABLELOCK TABLES 等 DDL 操作)。

显式加表锁

sql 复制代码
LOCK TABLES books READ;   -- 共享表锁,其他会话可读不可写
LOCK TABLES books WRITE;  -- 独占表锁,其他会话不可读写
UNLOCK TABLES;            -- 释放表锁

表锁的优点在于实现简单、开销小(不需要精确定位行),但并发度极差------所有写操作都会互相阻塞。现代 OLTP 系统中几乎不应该使用表锁(DDL 除外)。

1.2 行锁(Row Lock)

行锁是 InnoDB 的默认锁粒度,只锁定被访问的行,不锁整张表。它由存储引擎层实现,MySQL 服务层并不知道。

行锁的优点:

  • 并发度高,不同事务可以同时修改不同行。
  • 死锁的可能性虽高于表锁,但可以通过检测和回滚处理。

行锁的缺点:

  • 锁管理开销大(内存占用、获取/释放成本高)。
  • 如果 where 条件没有合适的索引,行锁会退化为表锁(或锁住大量行)。

行锁加锁方式

  • 对于 SELECT ... FOR UPDATE,对扫描到的所有行加排他行锁(X lock)。
  • 对于 SELECT ... LOCK IN SHARE MODE(MySQL 8.0 改名为 SELECT ... FOR SHARE),加共享行锁(S lock)。
  • 对于 UPDATEDELETE,加排他行锁。

1.3 页锁(Page Lock)

页锁是介于表锁和行锁之间的粒度,锁定一个数据页(16KB)。主要用于 BDB(BerkeleyDB)引擎,InnoDB 几乎不使用页锁。在极少数内部操作中可能出现,但用户层面不需要关注。


2. 按锁的模式分类

锁模式定义了锁的"互斥关系":两个锁能否同时存在于同一资源上。

2.1 共享锁(Shared Lock,S 锁)

持有 S 锁的事务可以一行数据,其他事务也可以同时获得 S 锁来读,但任何人不能获取 X 锁来写。

  • 多个事务可以同时持有同一行的 S 锁。
  • S 锁与 S 锁兼容,S 锁与 X 锁不兼容。

加 S 锁

sql 复制代码
SELECT * FROM books WHERE id = 1 FOR SHARE;  -- MySQL 8.0+
-- 或旧语法:LOCK IN SHARE MODE

2.2 独占锁(Exclusive Lock,X 锁)

持有 X 锁的事务可以读和写一行数据,其他事务既不能加 S 锁也不能加 X 锁,必须等待。

  • X 锁与任何锁(包括 S、X)都不兼容。
  • UPDATEDELETEINSERT(对插入行)自动加 X 锁。

加 X 锁

sql 复制代码
SELECT * FROM books WHERE id = 1 FOR UPDATE;

2.3 兼容矩阵

S 锁 X 锁
S 锁 ✅ 兼容 ❌ 不兼容
X 锁 ❌ 不兼容 ❌ 不兼容

3. 意向锁(Intention Lock)

3.1 为什么需要意向锁?

假设事务 T1 想要对表 books 加表级 X 锁。如果没有意向锁,InnoDB 必须遍历整张表的每一行,检查是否存在其他事务加的行锁------这在行数很多时极不现实。

意向锁 就是为解决这个"粒度冲突"而引入的:当事务要对某行加行锁时,它会先在表级加上一个对应类型的意向锁,相当于在表上插了一面旗子:"注意,里面有行锁!"

3.2 意向共享锁(IS)与意向排他锁(IX)

  • 意向共享锁(Intention Shared,IS):事务打算给某些行加 S 锁。先获得 IS 锁,然后再获取行 S 锁。
  • 意向排他锁(Intention Exclusive,IX):事务打算给某些行加 X 锁。先获得 IX 锁,然后再获取行 X 锁。

意向锁之间总是兼容的(IS 与 IS、IS 与 IX、IX 与 IX 都兼容),因为它们只是"意向"。只有表级 S/X 锁与意向锁之间才有互斥关系。

3.3 表级锁兼容矩阵(扩展)

IS IX S X
IS
IX
S
X

这张表揭示了:如果某个事务想加表级 X 锁,它只需检查表上是否有 IS 或 IX 锁(而不必逐行扫描)。有则等待,没有则可以安全加锁。


4. InnoDB 的行锁细分类

InnoDB 的行锁并不是简单的"锁住某一行",在 REPEATABLE READ 隔离级别下,为了防止幻读,它进化出了更精细的锁类型。

4.1 记录锁(Record Lock)

锁定单个行记录。作用在该行的聚簇索引上。

  • 对于 SELECT ... FOR UPDATE WHERE id = 1,会在 id=1 的聚簇索引记录上加 X 锁。
  • 这是最基础的行锁。

4.2 间隙锁(Gap Lock)

锁定索引记录之间的"间隙",但不包含该记录本身。间隙锁的目的是防止其他事务向这个间隙中插入新记录,从而避免幻读。

  • 对于 SELECT ... FOR UPDATE WHERE id BETWEEN 5 AND 10,如果现有记录有 id=3,7,12,那么间隙锁可能会锁住 (3,7) 和 (7,12) 等区间。
  • 间隙锁是相互兼容的,即多个事务可以在同一个间隙上加锁(因为它只是"防插入",不防读)。

注意 :间隙锁只在 REPEATABLE READ 及更高隔离级别下生效。在 READ COMMITTED 下,间隙锁会被禁用。

4.3 临键锁(Next-Key Lock)

记录锁 + 间隙锁的组合 。它锁定一个左开右闭的区间。例如对于索引记录 5、10,Next-Key Lock 可能锁住区间 (-∞, 5]、(5, 10]、(10, +∞)。

这是 InnoDB 在 RR 隔离级别下默认的行锁形式,既防修改已有记录,又防插入新记录。绝大多数情况下,我们看到的"行锁"其实都是 Next-Key Lock。

4.4 插入意向锁(Insert Intention Lock)

这是一种特殊的间隙锁 ,由 INSERT 操作在插入前加在目标间隙上。它表明"我想在这个间隙插入",不与其他插入意向锁互斥(只要不是同一位置),但会与间隙锁或 Next-Key Lock 冲突。

  • 多个事务可以同时持有同一间隙的插入意向锁(不同行),实现并发插入。
  • 但如果该间隙上有间隙锁(由其他事务的 SELECT ... FOR UPDATE 产生),插入意向锁会阻塞,从而防止幻读。

4.5 自增锁(AUTO-INC Lock)

这是表级锁,专门用于处理 AUTO_INCREMENT 列。当一个事务插入一行到带有自增列的表时,需要获取自增锁来确保自增值的唯一性和连续性。

MySQL 通过 innodb_autoinc_lock_mode 参数控制其行为:

  • 0(传统):所有 INSERT 都持有表级自增锁直到语句结束。
  • 1(默认,简单 INSERT 批量插入):简单插入使用轻量级锁(分配完即释放),批量插入仍用表级锁。
  • 2(交叉):所有 INSERT 使用轻量级锁,但可能导致自增值不连续(对复制不安全)。

5. 实战:观察锁的现象

5.1 准备环境

sql 复制代码
USE library_db;

-- 确保有测试表
CREATE TABLE IF NOT EXISTS lock_test (
    id INT PRIMARY KEY,
    name VARCHAR(20)
) ENGINE=InnoDB;

INSERT INTO lock_test VALUES (10, 'ten'), (20, 'twenty'), (30, 'thirty');

5.2 观察行锁互斥

会话 A

sql 复制代码
START TRANSACTION;
SELECT * FROM lock_test WHERE id = 10 FOR UPDATE;  -- X 锁

会话 B

sql 复制代码
START TRANSACTION;
SELECT * FROM lock_test WHERE id = 10 FOR UPDATE;  -- 会阻塞,等待会话 A 释放

在第三个会话中查看锁等待:

sql 复制代码
SELECT * FROM performance_schema.data_locks\G
-- 或者 SHOW ENGINE INNODB STATUS\G 查看 TRANSACTIONS 部分

data_locks 表会显示 LOCK_MODELOCK_TYPELOCK_DATA 等字段,你可以直观看到 X 锁和等待关系。

5.3 观察间隙锁

会话 A

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM lock_test WHERE id BETWEEN 15 AND 25 FOR UPDATE;  -- 锁定间隙

会话 B

sql 复制代码
INSERT INTO lock_test VALUES (18, 'eighteen');  -- 会阻塞!间隙锁阻止插入

切换到 READ COMMITTED 再试:

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

你会发现插入不会被阻塞,因为 RC 下没有间隙锁。


6. 小结

本文系统梳理了 MySQL 中锁的全家福:

  • 按粒度:表锁(粗粒度,并发差)→ 行锁(细粒度,并发高)→ 页锁(极少使用)
  • 按模式:共享锁(S,可同时读)→ 独占锁(X,排斥一切读写)
  • 意向锁:IS/IX 是表级旗子,解决表锁与行锁的冲突检测,意向锁之间永远兼容
  • 行锁细分:记录锁(锁单行)→ 间隙锁(锁区间,防插入)→ 临键锁(记录锁+间隙锁,RR 默认)→ 插入意向锁(特殊的间隙锁,插入前加)→ 自增锁(管理 AUTO_INCREMENT)
  • 隔离级别影响:RC 下不使用间隙锁,RR 及更高使用临键锁防幻读

理解锁的分类和兼容性是后续深入死锁分析、MVCC 原理以及性能优化的基础。下一篇文章,我们将专门聚焦于幻读与 Next-Key Lock,通过实战案例看清 InnoDB 在 RR 级别下到底是如何打败幻读的。

思考题

  1. 意向锁为什么不之间锁住表,而只是"意向"?
  2. 在 RC 隔离级别下,为什么可以忽略间隙锁?这会带来什么问题?
  3. performance_schema.data_locks 查看你系统当前持有的锁,试着理解每一行的意义。

参考资料


相关推荐
Full Stack Developme1 小时前
SQL 执行顺序 及 全部关键字
数据库·sql
专注API从业者2 小时前
电商选品效率翻倍!基于 Open Claw + 淘宝商品 API 实现自动化监控选品(附完整可运行代码)
大数据·运维·数据结构·数据库·自动化
C137的本贾尼2 小时前
InnoDB 内存架构:Buffer Pool、Change Buffer 与 Log Buffer
数据库·oracle·架构
DigitalOcean2 小时前
深度评测:RAG 向量数据库选型指南 —— OpenSearch、Weaviate、pgvector 怎么选?
数据库·ai编程
云计算磊哥@2 小时前
运维开发宝典025-MySQL01数据库的安装和配置
运维·数据库·运维开发
Bert.Cai2 小时前
SQLPlus简介
数据库·oracle
超梦dasgg2 小时前
Redis ZSet(有序集合)底层数据结构
数据结构·数据库·redis
渣渣盟2 小时前
MySQL DQL全面解析:从入门到精通
数据库·sql·mysql·dql
这个DBA有点耶3 小时前
InnoDB架构深潜:从磁盘到内存,一条SQL的生命周期
数据库·mysql·程序员