锁的分类:表锁、行锁、页锁与意向锁
在上一篇中我们深入探讨了事务的 ACID 特性和四种隔离级别。其中提到隔离性是通过锁和MVCC共同实现的。如果把数据库比作一个繁忙的办公室锁就是“使用中”的牌子——它告诉其他事务这个数据我正在用请你等一等。本文将从宏观到微观系统梳理 MySQL InnoDB 中各种锁的分类和使用方式让你彻底搞清楚按粒度分的表锁、行锁、页锁按模式分的共享锁S与独占锁XInnoDB 特有的意向锁IS/IX及其作用记录锁、间隙锁、临键锁的概念自增锁与插入意向锁实战观察各种锁的现象1. 按锁的粒度分类锁粒度决定了锁定数据范围的大小直接影响并发度和系统开销。1.1 表锁Table Lock表锁是 MySQL 中最粗粒度的锁它会锁定整张表。MyISAM 引擎主要使用表锁InnoDB 在特定情况下也会使用如ALTER TABLE、LOCK TABLES等 DDL 操作。显式加表锁LOCKTABLESbooksREAD;-- 共享表锁其他会话可读不可写LOCKTABLESbooksWRITE;-- 独占表锁其他会话不可读写UNLOCKTABLES;-- 释放表锁表锁的优点在于实现简单、开销小不需要精确定位行但并发度极差——所有写操作都会互相阻塞。现代 OLTP 系统中几乎不应该使用表锁DDL 除外。1.2 行锁Row Lock行锁是 InnoDB 的默认锁粒度只锁定被访问的行不锁整张表。它由存储引擎层实现MySQL 服务层并不知道。行锁的优点并发度高不同事务可以同时修改不同行。死锁的可能性虽高于表锁但可以通过检测和回滚处理。行锁的缺点锁管理开销大内存占用、获取/释放成本高。如果 where 条件没有合适的索引行锁会退化为表锁或锁住大量行。行锁加锁方式对于SELECT ... FOR UPDATE对扫描到的所有行加排他行锁X lock。对于SELECT ... LOCK IN SHARE MODEMySQL 8.0 改名为SELECT ... FOR SHARE加共享行锁S lock。对于UPDATE、DELETE加排他行锁。1.3 页锁Page Lock页锁是介于表锁和行锁之间的粒度锁定一个数据页16KB。主要用于 BDBBerkeleyDB引擎InnoDB 几乎不使用页锁。在极少数内部操作中可能出现但用户层面不需要关注。2. 按锁的模式分类锁模式定义了锁的“互斥关系”两个锁能否同时存在于同一资源上。2.1 共享锁Shared LockS 锁持有 S 锁的事务可以读一行数据其他事务也可以同时获得 S 锁来读但任何人不能获取 X 锁来写。多个事务可以同时持有同一行的 S 锁。S 锁与 S 锁兼容S 锁与 X 锁不兼容。加 S 锁SELECT*FROMbooksWHEREid1FORSHARE;-- MySQL 8.0-- 或旧语法LOCK IN SHARE MODE2.2 独占锁Exclusive LockX 锁持有 X 锁的事务可以读和写一行数据其他事务既不能加 S 锁也不能加 X 锁必须等待。X 锁与任何锁包括 S、X都不兼容。UPDATE、DELETE、INSERT对插入行自动加 X 锁。加 X 锁SELECT*FROMbooksWHEREid1FORUPDATE;2.3 兼容矩阵S 锁X 锁S 锁✅ 兼容❌ 不兼容X 锁❌ 不兼容❌ 不兼容3. 意向锁Intention Lock3.1 为什么需要意向锁假设事务 T1 想要对表books加表级 X 锁。如果没有意向锁InnoDB 必须遍历整张表的每一行检查是否存在其他事务加的行锁——这在行数很多时极不现实。意向锁就是为解决这个“粒度冲突”而引入的当事务要对某行加行锁时它会先在表级加上一个对应类型的意向锁相当于在表上插了一面旗子“注意里面有行锁”3.2 意向共享锁IS与意向排他锁IX意向共享锁Intention SharedIS事务打算给某些行加 S 锁。先获得 IS 锁然后再获取行 S 锁。意向排他锁Intention ExclusiveIX事务打算给某些行加 X 锁。先获得 IX 锁然后再获取行 X 锁。意向锁之间总是兼容的IS 与 IS、IS 与 IX、IX 与 IX 都兼容因为它们只是“意向”。只有表级 S/X 锁与意向锁之间才有互斥关系。3.3 表级锁兼容矩阵扩展ISIXSXIS✅✅✅❌IX✅✅❌❌S✅❌✅❌X❌❌❌❌这张表揭示了如果某个事务想加表级 X 锁它只需检查表上是否有 IS 或 IX 锁而不必逐行扫描。有则等待没有则可以安全加锁。4. InnoDB 的行锁细分类InnoDB 的行锁并不是简单的“锁住某一行”在REPEATABLE READ隔离级别下为了防止幻读它进化出了更精细的锁类型。4.1 记录锁Record Lock锁定单个行记录。作用在该行的聚簇索引上。对于SELECT ... FOR UPDATE WHERE id 1会在id1的聚簇索引记录上加 X 锁。这是最基础的行锁。4.2 间隙锁Gap Lock锁定索引记录之间的“间隙”但不包含该记录本身。间隙锁的目的是防止其他事务向这个间隙中插入新记录从而避免幻读。对于SELECT ... FOR UPDATE WHERE id BETWEEN 5 AND 10如果现有记录有 id3,7,12那么间隙锁可能会锁住 (3,7) 和 (7,12) 等区间。间隙锁是相互兼容的即多个事务可以在同一个间隙上加锁因为它只是“防插入”不防读。注意间隙锁只在REPEATABLE READ及更高隔离级别下生效。在READ COMMITTED下间隙锁会被禁用。4.3 临键锁Next-Key Lock记录锁 间隙锁的组合。它锁定一个左开右闭的区间。例如对于索引记录 5、10Next-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 准备环境USElibrary_db;-- 确保有测试表CREATETABLEIFNOTEXISTSlock_test(idINTPRIMARYKEY,nameVARCHAR(20))ENGINEInnoDB;INSERTINTOlock_testVALUES(10,ten),(20,twenty),(30,thirty);5.2 观察行锁互斥会话 ASTARTTRANSACTION;SELECT*FROMlock_testWHEREid10FORUPDATE;-- X 锁会话 BSTARTTRANSACTION;SELECT*FROMlock_testWHEREid10FORUPDATE;-- 会阻塞等待会话 A 释放在第三个会话中查看锁等待SELECT*FROMperformance_schema.data_locks\G-- 或者 SHOW ENGINE INNODB STATUS\G 查看 TRANSACTIONS 部分data_locks表会显示LOCK_MODE、LOCK_TYPE、LOCK_DATA等字段你可以直观看到 X 锁和等待关系。5.3 观察间隙锁会话 ASETSESSIONTRANSACTIONISOLATIONLEVELREPEATABLEREAD;STARTTRANSACTION;SELECT*FROMlock_testWHEREidBETWEEN15AND25FORUPDATE;-- 锁定间隙会话 BINSERTINTOlock_testVALUES(18,eighteen);-- 会阻塞间隙锁阻止插入切换到READ COMMITTED再试SETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;你会发现插入不会被阻塞因为 RC 下没有间隙锁。6. 小结本文系统梳理了 MySQL 中锁的全家福按粒度表锁粗粒度并发差→ 行锁细粒度并发高→ 页锁极少使用按模式共享锁S可同时读→ 独占锁X排斥一切读写意向锁IS/IX 是表级旗子解决表锁与行锁的冲突检测意向锁之间永远兼容行锁细分记录锁锁单行→ 间隙锁锁区间防插入→ 临键锁记录锁间隙锁RR 默认→ 插入意向锁特殊的间隙锁插入前加→ 自增锁管理 AUTO_INCREMENT隔离级别影响RC 下不使用间隙锁RR 及更高使用临键锁防幻读理解锁的分类和兼容性是后续深入死锁分析、MVCC 原理以及性能优化的基础。下一篇文章我们将专门聚焦于幻读与 Next-Key Lock通过实战案例看清 InnoDB 在 RR 级别下到底是如何打败幻读的。思考题意向锁为什么不之间锁住表而只是“意向”在 RC 隔离级别下为什么可以忽略间隙锁这会带来什么问题用performance_schema.data_locks查看你系统当前持有的锁试着理解每一行的意义。参考资料MySQL 8.0 Reference Manual - InnoDB LockingMySQL 8.0 Reference Manual - LOCK TABLESMySQL 8.0 Reference Manual - Performance Schema Lock Tables