首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【深度揭秘】MySQL 8.4 一张空表为何无法插入数据?

【深度揭秘】MySQL 8.4 一张空表为何无法插入数据?

作者头像
俊才
发布2026-01-19 14:01:21
发布2026-01-19 14:01:21
880
举报
文章被收录于专栏:数据库干货铺数据库干货铺

表里一条数据都没有, 为什么 INSERT 全部被阻塞? 而 DELETE 却畅通无阻? —— 今天带你彻底搞懂 InnoDB Gap 锁的“宇宙级”封锁逻辑。

1. 一个诡异现象:空表也能“锁死”写入?

1.1 诡异现象

当前是这样一个场景:

代码语言:javascript
复制
-- 表 t1刚创建,完全为空
CREATE TABLE t1(id INT PRIMARY KEY, name VARCHAR(10)) ENGINE=InnoDB;
-- Session A
BEGIN;
SELECT * FROM t1 WHERE id = 5 FOR UPDATE; -- 返回空
-- Session B
INSERT INTO t1 VALUES (1, 'a');   -- ❌ 卡住了!
INSERT INTO t1 VALUES (100, 'b'); -- ❌ 也卡住了!

表是空的,查询也没结果,凭什么不让插数据?更神奇的是删除毫无阻塞,瞬间返回结果:

代码语言:javascript
复制
-- Session C
DELETE FROM t1  WHERE id = 999;   -- ✅ 瞬间返回,毫无阻塞!
DELETE FROM t1  WHERE id = 1;     -- ✅ 瞬间返回,毫无阻塞!
DELETE FROM t1  WHERE id = 5;     -- ✅ 瞬间返回,毫无阻塞!

这背后,正是InnoDB的Gap锁机制在默默运作。今天我们就以MySQL8.4为基准,彻底拆解Gap锁在不同数据状态下的行为边界。

1.2 什么是Gap锁?

在InnoDB中,除了常见的记录锁(Record Lock),还有两种关键锁类型:

Gap 锁(Gap Lock):锁定索引记录之间的“间隙”,防止新记录插入

Next-Key 锁:记录锁 + Gap 锁,是 RR 隔离级别的默认加锁方式

⚠️ Gap 锁只在 可重复读(Repeatable Read) 下生效,在READ COMMITTED 中基本被禁用。

它的核心使命:防止幻读(Phantom Read)

2. 各种场景

2.1 场景一:空表 + FOR UPDATE → 锁住整个宇宙!

🔍 锁范围:(-∞, +∞)

当表为空时,InnoDB 的 B+ 树中只有两个伪记录:

Infimum(负无穷)

Supremum(正无穷)

执行:

代码语言:javascript
复制
SELECT * FROM t1 WHERE id = 5 FOR UPDATE;

由于没有真实记录,InnoDB 将查询定位到Supremum,并在其上加一个 X,GAP 锁。通过 performance_schema.data_locks 可看到:

LOCK_MODE: X,GAP

LOCK_DATA: supremum pseudo-record

这意味着:整个主键空间都被 Gap 锁覆盖!影响如下:

操作

结果

原因

INSERT id=1

❌ 阻塞

插入意向锁与 Gap 锁冲突

INSERT id=100

❌ 阻塞

同上

DELETE WHERE id=999

✅ 成功

无匹配行,不申请任何锁

💡 Gap 锁只防“插入”,不防“删除不存在的行”

2.2 场景二:表中有 id=10 → 锁住 (-∞, 10)

假设表中已有id=10的记录:

代码语言:javascript
复制
INSERT INTO t1  VALUES (10, 'test');

再执行:

代码语言:javascript
复制
SELECT * FROM t1  WHERE id = 5 FOR UPDATE;

InnoDB 会在 id=10 上加 X,GAP 锁,覆盖范围:(-∞, 10)。

此时在进行插入及删除操作如下:

代码语言:javascript
复制
mysql> INSERT INTO t1 VALUES (3, 'b');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> INSERT INTO t1 VALUES (9, 'c');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> INSERT INTO t1 VALUES (11, 'd');
Query OK, 1 row affected (0.00 sec)
mysql> delete from t1 where id=10;
Query OK, 1 row affected (0.00 sec)

影响如下:

操作

结果

原因

INSERT id=3

❌ 阻塞

插入意向锁与 Gap 锁冲突

INSERT id=9

❌ 阻塞

插入意向锁与 Gap 锁冲突

INSERT id=11

✅ 成功

成功,不在锁范围内

DELETE id=10

✅ 成功

除非被其他事务锁住记录本身

2.3 场景三:表中有 id=4 和 id=10 → 锁住 (4, 10)

如果当前表里有id=4 及id=10的记录时,再来看一下是什么现象。

代码语言:javascript
复制
INSERT INTO t1  VALUES (4, 'tt');
mysql> select * from t1;
+----+------+
| id | name |
+----+------+
|  4 | tt   |
| 10 | test |
+----+------+
2 rows in set (0.00 sec)

执行 WHERE id=5 FOR UPDATE 后,InnoDB 定位到 id=10,但知道前一个记录是 id=4,于是只锁住两者之间的间隙,Gap 范围:(4, 10)

再做相关插入及删除操作

代码语言:javascript
复制
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t1 VALUES (3, 'b');
Query OK, 1 row affected (0.00 sec)
mysql> select * from t1;
+----+------+
| id | name |
+----+------+
|  3 | b    |
|  4 | tt   |
| 10 | test |
+----+------+
3 rows in set (0.00 sec)
mysql> INSERT INTO t1 VALUES (5, 'c');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> INSERT INTO t1 VALUES (9, 'c');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> INSERT INTO t1 VALUES (11, 'c');
Query OK, 1 row affected (0.00 sec)
mysql> delete from t1 where id=4;
Query OK, 1 row affected (0.00 sec)
mysql> delete from t1 where id=10;
Query OK, 1 row affected (0.00 sec)
mysql> 

影响如下:

操作

结果

原因

INSERT id=3

✅ 成功(≤4)

不在(4,10)范围内

INSERT id=5~9

❌ 阻塞

锁的范围是(4,10)

INSERT id=11

✅ 成功

不在(4,10)范围内

DELETE id=4或 id=10

✅ 成功

Gap锁不锁已有记录

3. 为什么 DELETE 不会被阻塞?

3.1 不锁DELETE的原因

这是很多人困惑的点,根本原因在于:

DELETE 只操作“已存在的记录”,而 Gap 锁只保护“不存在的间隙”

如果 DELETE 的条件找不到记录 → 不申请任何锁 → 立即返回

如果 DELETE 的条件命中真实记录 → 申请记录锁(X),与 Gap 锁无关

只有当该记录已被其他事务加了记录锁时,DELETE 才会阻塞 —— 但这和 Gap 锁无关

✅ 所以:Gap 锁 ≠ 万能锁,它只对 INSERT 敏感。

3.2 如何避免“意外全局锁”?

生产环境要尽量避免意外的全局锁,因此可以按照如下方式进行:

慎用空表的FOR UPDATE查询:尤其在初始化阶段,避免无意义的加锁

考虑使用READ COMMITTED隔离级别:在 RC 下,Gap 锁基本不生效,INSERT 并发性更高(但需接受可能的幻读)

缩短事务时间:即使加了锁,尽快提交可减少影响窗口

监控锁信息:使用 performance_schema.data_locks 实时观察锁状态:

代码语言:javascript
复制
SELECT * FROM performance_schema.data_locks;

4. 小结

本文举例了Gap锁的三种典型范围,对应的影响小结如下:

表状态

查询条件

Gap 锁范围

被阻塞的INSERT

空表

id=5 FOR UPDATE

(-∞, +∞)

所有INSERT

有 id=10

同上

(-∞, 10)

id < 10

有 id=4,10

同上

(4, 10)

4 < id < 10

Gap 锁越“精准”,并发性越高;数据越稀疏,锁范围越大。

Gap锁是InnoDB实现高隔离级别的“隐形守护者”,但它也是一把双刃剑——用得好,杜绝幻读;用不好,拖垮并发。

作为开发者或 DBA,理解它在空表、单记录、多记录下的不同行为,是写出高性能、高可靠 SQL 的关键一步。

下次再遇到神秘的 INSERT 阻塞,别慌——打开 data_locks,看看是不是 Gap 锁在“守护”那个你从未注意过的间隙。

关于数据库锁相关的问题,可以在留言区留言交流,也欢迎点赞、转发,给更多需要的朋友们看到此文章。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-01-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据库干货铺 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 一个诡异现象:空表也能“锁死”写入?
    • 1.1 诡异现象
    • 1.2 什么是Gap锁?
  • 2. 各种场景
    • 2.1 场景一:空表 + FOR UPDATE → 锁住整个宇宙!
    • 2.2 场景二:表中有 id=10 → 锁住 (-∞, 10)
    • 2.3 场景三:表中有 id=4 和 id=10 → 锁住 (4, 10)
  • 3. 为什么 DELETE 不会被阻塞?
    • 3.1 不锁DELETE的原因
    • 3.2 如何避免“意外全局锁”?
  • 4. 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档