MySQL锁机制揭秘:记录锁、间隙锁与临键锁详解‌

以下内容将从 MySQL InnoDB 存储引擎的锁机制入手,重点讲解记录锁(Record Lock)间隙锁(Gap Lock)临键锁(Next-key Lock)的概念与区别,配合实际代码示例ASCII 图解详细说明,帮助你系统地理解它们在并发控制与防止幻读中的作用。


目录

  1. MySQL 锁机制总览
  2. InnoDB 行级锁简介

    1. 什么是记录锁(Record Lock)
    2. 什么是间隙锁(Gap Lock)
    3. 什么是临键锁(Next-key Lock)
  3. 锁粒度示意与 ASCII 图解

    1. 索引与记录空间划分
    2. 记录锁示意图
    3. 间隙锁示意图
    4. 临键锁示意图
  4. 锁类型的触发条件与示例演示

    1. 示例表及数据准备
    2. 记录锁:SELECT … FOR UPDATE
    3. 间隙锁:REPEATABLE READ 隔离级别下的 INSERT 与 SELECT FOR UPDATE
    4. 临键锁:NEXT-KEY LOCK 的综合示例
  5. 防止幻读与隔离级别

    1. REPEATABLE READ 与幻读
    2. READ COMMITTED 模式下的锁行为
  6. 实战演练:多 session 并发场景

    1. 场景一:事务 A、B 同时更新同一行
    2. 场景二:事务 A、B 并发插入相邻数据引发间隙锁冲突
    3. 场景三:事务 A、B 读取相同范围,防止幻读
  7. 实用技巧与注意事项
  8. 小结

1. MySQL 锁机制总览

MySQL 中,常见的锁类型包括:

  • 表级锁(Table Lock):针对整张表加锁,粒度最粗,在 MyISAM 引擎中较常用。
  • 行级锁(Row-level Lock):InnoDB 等事务型存储引擎支持,粒度更细,可大幅提高并发。

在行级锁内部,InnoDB 进一步细分为:

  1. 记录锁(Record Lock):针对索引上的单条记录加锁。
  2. 间隙锁(Gap Lock):针对索引记录之间空隙(两个相邻索引值)加锁。
  3. 临键锁(Next-key Lock):等同于“记录锁 + 间隙锁”,即对记录本身及其左侧的间隙同时加锁。

这三种锁的组合与使用,决定了 InnoDB 在不同隔离级别下如何防止“幻读”(phantom reads)与保证一致性。


2. InnoDB 行级锁简介

2.1 什么是记录锁(Record Lock)

  • 定义:记录锁直接作用于索引中的单条记录,防止其他事务对该记录进行更新或删除。
  • 触发场景:最典型的是使用 SELECT … FOR UPDATEUPDATE … WHERE …DELETE … WHERE … 等语句,InnoDB 会定位到满足条件的行并加上记录锁。
  • 作用:保证同一行不会被多个事务同时修改,避免数据不一致。

举例说明

-- 会话 1
START TRANSACTION;
SELECT * FROM t WHERE id = 5 FOR UPDATE;  -- 对主键 id=5 加记录锁

-- 会话 2
START TRANSACTION;
UPDATE t SET name='X' WHERE id = 5;       -- 需要先获得记录锁,会等待会话1释放

2.2 什么是间隙锁(Gap Lock)

  • 定义:间隙锁只锁定索引节点之间的空隙(gap),并不锁定行本身。例如,若索引上存在键值 10 与 20,则 10–20 之间的区间就是一个“间隙”;间隙锁将阻止其他事务在该区间插入新行。
  • 触发场景:在 InnoDB 的 REPEATABLE READ 隔离级别下,用 SELECT … FOR UPDATEUPDATEDELETE 等操作,若 WHERE 条件不是精确匹配某个索引值,而是范围扫描,就会隐式对扫描到范围中的间隙加锁。
  • 作用:防止“幻读”:即在同一事务中,多次执行相同的范围查询,防止其他事务在这个范围内插入新行导致结果集合不同。

举例说明

假设表中有索引值 100 和 200,当事务 A 使用

SELECT * FROM t WHERE id BETWEEN 100 AND 200 FOR UPDATE;

这条范围查询不仅对 100、200 位置的记录加记录锁,同时会对 (100,200) 区间加间隙锁,其他事务无法在 (101…199) 之间插入新行。

2.3 什么是临键锁(Next-key Lock)

  • 定义:临键锁(Next-key Lock)是 InnoDB 默认的锁定粒度,它是“记录锁 + 间隙锁”的合体。即对索引的某个记录和该记录左侧的间隙同时加锁。
  • 触发场景:在 REPEATABLE READ 隔离级别下,对索引列进行 SELECT … FOR UPDATEUPDATEDELETE 等操作时,InnoDB 会对每个匹配行加一个临键锁。这比单纯的记录锁更严格,因为它同时锁定前面的间隙,进而防止幻读。
  • 作用:在默认隔离级别下,保证读或写一个索引记录时,其左侧区间也被锁定,阻止在此范围插入,这样可以彻底避免幻读。

3. 锁粒度示意与 ASCII 图解

3.1 索引与记录空间划分

假设我们有一张简单的表 t,其主键为 id,当前表中存在以下几条记录:

+----+--------+
| id |  name  |
+----+--------+
| 10 | Alice  |
| 20 | Bob    |
| 30 | Cathy  |
+----+--------+

在 B+Tree 索引上,逻辑上会分为:

...  (−∞,10)  10  (10,20)  20  (20,30)  30  (30, +∞)  ...
  • 102030 等节点就是“记录”(Record)。
  • (−∞,10)(10,20)(20,30)(30,+∞) 等区间即“间隙”(Gap)。

3.2 记录锁示意图

当事务 A 执行:

SELECT * FROM t WHERE id = 20 FOR UPDATE;

InnoDB 会将主键为 20 的那条记录加上记录锁,不加任何间隙锁。示意图如下,用 X 表示被锁定的记录:

...  (−∞,10)  10  (10,20)  [20:X]  (20,30)  30  (30, +∞)  ...
  • 只有 20 这条记录被锁,其他事务仍可在 20 左右的间隙插入新行(例如插入 id=15 或 id=25 均无阻碍)。

3.3 间隙锁示意图

当事务 A 执行:

SELECT * FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE;

InnoDB 会对 id=10id=20 两条记录加记录锁,同时对 [10,20] 区间的间隙间隙锁。示意图(粗略表示):

...  (−∞,10)  [10:X]  [(10,20):LOCKED]  [20:X]  (20,30)  30  (30,+∞)  ...
  • X 表示记录锁;LOCKED 表示该区间被间隙锁。
  • 事务 B 无法向 (10,20)(即 11..19)之间插入新行,但仍可插入 (−∞,10)(20,30) 区间。

3.4 临键锁示意图

当事务 A 执行相同的范围查询(在 REPEATABLE READ 下默认使用临键锁):

SELECT * FROM t WHERE id >= 20 FOR UPDATE;

此时 InnoDB 会对 id=20 及其左侧的间隙 (10,20) 同时加锁。示意图:

...  (−∞,10)  10  (10,20:LOCKED)  [20:X]  (20,30)  30  (30,+∞)  ...
  • [20:X]:对 20 记录加记录锁。
  • (10,20:LOCKED):对紧邻 20 左侧的间隙加间隙锁。
  • 如果套用下一条记录 30,同样对 (20,30)(30,+∞) 视查询条件会加相应锁。

因为它既锁了记录也锁了左侧间隙,这种组合即称“临键锁(Next-key Lock)”。


4. 锁类型的触发条件与示例演示

4.1 示例表及数据准备

首先创建一个示例表,并插入若干测试数据,以便后续演示锁行为:

-- 创建数据库与表
CREATE DATABASE test_locks;
USE test_locks;

CREATE TABLE t (
    id INT NOT NULL PRIMARY KEY,
    name VARCHAR(20)
) ENGINE=InnoDB;

-- 插入测试数据
INSERT INTO t (id, name) VALUES
 (10, 'Alice'),
 (20, 'Bob'),
 (30, 'Cathy'),
 (40, 'David'),
 (50, 'Eve');

此时,主键索引上存在 id=10,20,30,40,50 五条记录。

注意:为了观察锁的粒度与范围,务必使用 InnoDB 存储引擎,并确保隔离级别为 REPEATABLE READ(InnoDB 默认)。

可以通过以下命令查看当前隔离级别与锁模式:

SELECT @@tx_isolation;         -- 或 @@transaction_isolation
SELECT @@innodb_autoinc_lock_mode;

确保隔离级别是 REPEATABLE-READ


4.2 记录锁:SELECT … FOR UPDATE

在会话 A 中:

-- 会话 A
START TRANSACTION;
SELECT * FROM t WHERE id = 20 FOR UPDATE;
  • 这条语句会在 id=20 记录上加一个记录锁,如下示意:
...  (−∞,10)  10  (10,20)  [20:X]  (20,30)  30  (30,40)  40  ...

此时在另一个会话 B 中,如果尝试:

-- 会话 B
START TRANSACTION;
UPDATE t SET name='Bob2' WHERE id = 20;

会被阻塞,直到会话 A 执行 COMMITROLLBACK 释放锁后,才可继续执行。

而如果会话 B 执行:

INSERT INTO t (id, name) VALUES (20, 'NewBob');

也会被阻塞,因为插入相同主键等价于先定位 id=20,再加记录锁,发现已有记录锁会等待。

但是,如果会话 B 执行:

INSERT INTO t (id, name) VALUES (25, 'NewGuy');

由于 id=25 处于 (20,30) 区间,会话 A 并未对该间隙加锁(仅加了记录锁),因此插入能立即成功。


4.3 间隙锁:REPEATABLE READ 隔离级别下的 INSERT 与 SELECT FOR UPDATE

示例:事务 A 对范围加锁

-- 会话 A
START TRANSACTION;
SELECT * FROM t WHERE id BETWEEN 20 AND 40 FOR UPDATE;
  • InnoDB 会对 id=20id=30id=40 的记录加记录锁,同时对这些记录之间的间隙 (20,30)(30,40) 加间隙锁,示意:
...  (−∞,10)  10  (10,20)  [20:X]  [(20,30):LOCKED]  [30:X]  [(30,40):LOCKED]  [40:X]  (40,50)  50  ...
  • 注意 (10,20) 并未加锁,因为范围是 [20,40],左端点处不会锁前一个间隙;而 (40,50) 同理不锁。

会话 B 测试插入

-- 会话 B
START TRANSACTION;

-- 尝试在 (20,30) 区间插入:id=25
INSERT INTO t (id,name) VALUES (25,'XChris');  -- 会被阻塞

-- 尝试在 (10,20) 区间插入:id=15
INSERT INTO t (id,name) VALUES (15,'YAnna');   -- 立即成功

-- 尝试在 (40,50) 区间插入:id=45
INSERT INTO t (id,name) VALUES (45,'ZAndy');   -- 立即成功
  • (20,30) 已被间隙锁锁定,无法插入 id=25;
  • (10,20) 未锁可插入 id=15;
  • (40,50) 未锁可插入 id=45。

当会话 A 执行 COMMIT 释放锁后,会话 B 才能继续插入 25。


4.4 临键锁:NEXT-KEY LOCK 的综合示例

临键锁是 InnoDB 在 REPEATABLE READ 下默认加的锁类型,它等同于记录锁与其左侧间隙锁的组合。

示例:事务 A 对单条记录加锁,触发临键锁

-- 会话 A
START TRANSACTION;
SELECT * FROM t WHERE id = 30 FOR UPDATE;
  • 虽然条件是单行 id=30,但在 REPEATABLE READ 模式下,InnoDB 默认会对 (20,30) 区间加间隙锁,再对 id=30 加记录锁,示意:
...  (−∞,10)  10  (10,20)  [20]  [(20,30):LOCKED]  [30:X]  (30,40)  40  ...

会话 B 测试插入

-- 会话 B
START TRANSACTION;

-- 尝试在 (20,30) 区间插入:id=25
INSERT INTO t (id,name) VALUES (25,'XChris');  -- 会被阻塞

-- 尝试在 (30,40) 区间插入:id=35
INSERT INTO t (id,name) VALUES (35,'YAnna');   -- 可以插入,因为 (30,40) 未锁
  • id=25 所在的 (20,30) 区间被临键锁锁定,不能插入;
  • id=35(30,40),能够顺利插入。
Tip:如果在会话 A 上执行 SELECT * FROM t WHERE id = 30 LOCK IN SHARE MODE;,则会得到同样的临键锁(共享模式),仍会锁住 (20,30)

5. 防止幻读与隔离级别

5.1 REPEATABLE READ 与幻读

  • 幻读(Phantom Read):在同一事务中,执行两次相同的范围查询,若其他事务在范围内插入新行,第一次与第二次查询结果集不一致,即为幻读。
  • InnoDB 的默认隔离级别REPEATABLE READ,它通过临键锁来防止幻读,即在读取范围时会对记录本身及左侧间隙加锁,阻止其他事务在该范围内插入新行。

示例:防止幻读

-- 会话 A
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM t WHERE id BETWEEN 20 AND 40;  -- 结果包含 20,30,40

-- 会话 B
START TRANSACTION;
INSERT INTO t (id,name) VALUES (25,'NewGuy');
COMMIT;

-- 回到会话 A
SELECT * FROM t WHERE id BETWEEN 20 AND 40;  -- 结果仍然仅包含 20,30,40,无 25
COMMIT;
  • 即使会话 B 插入了 id=25,会话 A 的第二次查询也不会看到新行,因为在第一次查询时就对 (20,30)(30,40) 等间隙加了锁,阻止会话 B 插入。

5.2 READ COMMITTED 模式下的锁行为

  • 如果将隔离级别设置为 READ COMMITTED,InnoDB 不会对单行查询额外加间隙锁,只加记录锁。因此无法防止幻读,只能保证读到的是已提交数据。
  • READ COMMITTED 下,前例中会话 A 第二次查询会返回 id=25。
-- 会话 A
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM t WHERE id BETWEEN 20 AND 40;  -- 包含 20,30,40

-- 会话 B
START TRANSACTION;
INSERT INTO t (id,name) VALUES (25,'NewGuy');
COMMIT;

-- 会话 A
SELECT * FROM t WHERE id BETWEEN 20 AND 40;  -- 结果包含 20,25,30,40
COMMIT;

6. 实战演练:多 session 并发场景

下面通过真实的 MySQL 会话演练,让你一步步体验记录锁、间隙锁和临键锁的行为。

6.1 场景一:事务 A、B 同时更新同一行

步骤演示

  1. 会话 A

    START TRANSACTION;
    SELECT * FROM t WHERE id = 20 FOR UPDATE;
    -- 此时锁定 id=20 记录
  2. 会话 B(不同终端):

    START TRANSACTION;
    UPDATE t SET name='Bob2' WHERE id = 20;
    -- 由于会话 A 未提交,会话 B 被阻塞,等待记录锁释放
  3. 会话 A

    UPDATE t SET name='Bob1' WHERE id = 20;
    COMMIT;  -- 释放锁
  4. 会话 B 恢复:

    -- B 此时获得锁,执行 UPDATE
    COMMIT;

此时,最终 name='Bob2',因为会话 B 在 A 提交后获得锁并执行更新。

6.2 场景二:事务 A、B 并发插入相邻数据引发间隙锁冲突

步骤演示

  1. 会话 A

    START TRANSACTION;
    SELECT * FROM t WHERE id BETWEEN 20 AND 30 FOR UPDATE;
    -- 锁定 20、30 记录及 (20,30) 区间
  2. 会话 B

    START TRANSACTION;
    INSERT INTO t (id, name) VALUES (25, 'XChris');
    -- 会话 B 在 (20,30) 区间插入,因间隙锁被阻塞
  3. 会话 A

    -- 仍可插入 (30,40) 区间,不影响会话 B
    INSERT INTO t (id,name) VALUES (35, 'YAnna');  -- 立即成功
    COMMIT;  -- 释放锁
  4. 会话 B 恢复:

    -- B 重新尝试插入 id=25
    -- 此时 (20,30) 区间无锁
    COMMIT;

此时,2535 都插入成功,顺序取决于谁先获锁。

6.3 场景三:事务 A、B 读取相同范围,防止幻读

步骤演示

  1. 会话 A

    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    START TRANSACTION;
    SELECT * FROM t WHERE id >= 20 FOR UPDATE;
    -- 对 (20,30) 区间及 20、30本身加了临键锁
  2. 会话 B

    START TRANSACTION;
    INSERT INTO t (id,name) VALUES (25, 'NewGuy');
    -- 会话 B 在 (20,30) 插入被阻塞,因为临键锁锁住了间隙
  3. 会话 A

    -- 再次查询同一范围
    SELECT * FROM t WHERE id >= 20;
    -- 依然不会看到 id=25,防止幻读
    COMMIT;
  4. 会话 B 恢复:

    COMMIT;  -- 此时可以插入 25

READ COMMITTED 下会话 A 第二次查询会看到插入的 25,从而出现幻读。


7. 实用技巧与注意事项

  1. 合理选择隔离级别

    • 如果对幻读要求严格,可使用默认的 REPEATABLE READ;若对性能要求更高且可容忍幻读,可切换到 READ COMMITTED
  2. 避免范围锁过度

    • 大量范围查询(如 SELECT * FROM t WHERE id > 0 FOR UPDATE)会把整个索引都锁住,影响并发。可加分页或尽量使用精确索引条件。
  3. 理解自增锁模式

    • InnoDB 还有一种 AUTO-INC 锁,用于 INSERT … VALUES 情形。在并发插入同一张表时,MySQL 会对插入缓冲区加表级锁。若希望提高并发,可将 innodb_autoinc_lock_mode 设置为 2(INTERLEAVED),允许并行自增。
  4. 监控锁等待

    • 使用 SHOW ENGINE INNODB STATUS\GINFORMATION_SCHEMA.INNODB_LOCKSINNODB_LOCK_WAITS 等表查看当前锁状况与等待情况,有助于定位死锁或性能瓶颈。
  5. 索引选择要覆盖常见查询

    • 查询时尽量使用覆盖索引(Covering Index),避免回表,提高锁粒度更细。
  6. 尽量减少长事务

    • 长事务会持有锁更久,导致其他事务阻塞。应及时提交或拆分事务。
  7. 了解 InnoDB 行锁与间隙锁的组合行为

    • 在不同版本或配置下(如 innodb_locks_unsafe_for_binloginnodb_autoinc_lock_mode),锁行为可能略有变化,需根据实际版本文档进行验证。

8. 小结

本文深入剖析了 MySQL InnoDB 存储引擎的三种行级锁:

  1. 记录锁(Record Lock):仅锁定单条记录,用于保护该行不被并发更新。
  2. 间隙锁(Gap Lock):仅锁定索引的空隙区间,用于防止其他事务在该区间插入,从而避免幻读。
  3. 临键锁(Next-key Lock):记录锁与左侧间隙锁的组合,是 InnoDB 在 REPEATABLE READ 下默认使用的锁方式,可同时保护记录本身与左侧间隙,彻底避免幻读。

通过 ASCII 图示与真实多会话并发演示,帮助你直观地了解这三种锁在索引树上的实际生效范围及对并发的影响。同时,还对如何在不同隔离级别下防止或允许幻读进行了对比,给出了常见实战场景与调优建议。

掌握这些锁机制后,你可以在设计表结构、编写查询或调优并发事务时,更有信心地控制锁粒度与并发性能,有效避免死锁和幻读。在实际项目中,建议结合 SHOW ENGINE INNODB STATUSINFORMATION_SCHEMA.INNODB_LOCKS 等工具,实时监控锁情况,及时发现并解决并发瓶颈。

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日