当前位置:首页 > 文章列表 > 数据库 > MySQL > MySQL gap lock 为什么阻塞范围外的插入

MySQL gap lock 为什么阻塞范围外的插入

来源:17golang原创 2026-10-28 23:23:00 0浏览 收藏

MySQL gap lock 阻塞“范围外”插入,通常是因为插入值虽然不满足 WHERE 条件,却落在同一个被锁住的索引间隙里。InnoDB 按索引记录及记录之间的空隙组织这类锁,不能把 SQL 条件两端的数字直接当作锁的精确边界。

官方文档地址:https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html

本文说明 MySQL 8.4 InnoDB 现有锁机制的设计理由和使用影响,不将其描述为某个新版本刚引入的变化。示例用于读者在独立测试库操作,没有本机执行结果或真实线上故障记录。

为什么空结果也可能保护一段间隙?

在 REPEATABLE READ 下,范围锁定读可能使用 gap lock 或 next-key lock,限制其他事务向相关索引间隙插入,以支持相应的并发隔离语义。记录锁针对索引记录,gap lock 针对间隙;next-key lock 则同时包含记录及其前方间隙。

代价是并发插入可能受影响。返回零行只意味着没有现有记录符合查询,不代表“没有需要保护的插入位置”。普通一致性读取与 SELECT ... FOR UPDATE 不是同一种读法,排查时应先确认实际 SQL。

假设主键索引只有 10 和 30 两个值。查询 id >= 18 AND id 没有命中行,但目标位于相邻记录 10、30 之间。若锁定读保护了间隙 (10,30),插入 12 也会进入这个间隙;12 不满足查询条件,仍可能等待。

查询条件与相邻索引记录之间间隙的静态范围关系
结构说明图:条件范围位于索引间隙内部,插入值 12 虽在条件之外,仍属于间隙 (10,30)。图示解释关系,不是锁监控截图。

先把示例的版本、索引与事务固定下来

使用同一 MySQL 实例上的两个独立连接 A、B,必要时再开一个只读监控连接 C。选择已有的独立测试库,创建下表;不能使用会话临时表,因为另一个连接需要访问它。不要在业务库直接实验。

-- 准备会话:在独立测试库执行一次,表名不得指向真实业务表。
SELECT VERSION();
CREATE TABLE gap_scope_demo (
    id INT NOT NULL PRIMARY KEY,
    note VARCHAR(30) NOT NULL
) ENGINE = InnoDB;

-- 只保留两个相邻主键,便于解释中间间隙。
INSERT INTO gap_scope_demo (id, note)
VALUES (10, '左端记录'), (30, '右端记录');

接着在 A 中执行以下片段。隔离级别在事务开始前设置;FORCE INDEX (PRIMARY) 只是让这个演示明确沿主键索引查找,不是建议在生产 SQL 中随意强制索引。

-- 会话 A:设置下一事务隔离级别,再开启显式事务。
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;

-- 空范围锁定读;执行完后暂时不要提交或回滚。
SELECT id
FROM gap_scope_demo FORCE INDEX (PRIMARY)
WHERE id >= 18 AND id 

A 的查询按样本应返回空集。接下来到 B 中执行插入:它不满足 A 的条件,却处于相邻索引记录之间。正常示例条件下可观察到等待;实际锁形态应结合工具链版本、计划和锁信息判断,不能照抄一份固定锁输出作为证据。

-- 会话 B:限制测试等待时间,防止把等待误认为客户端无响应。
SET SESSION innodb_lock_wait_timeout = 30;
START TRANSACTION;

-- 12 不在查询区间 [18,22),但在索引间隙 (10,30) 中。
INSERT INTO gap_scope_demo (id, note)
VALUES (12, '插入候选');

等待期间不要在同一个被阻塞的连接里继续提交监控语句。应使用连接 C 读取锁信息,或者先在 A 中结束事务。若 B 已超时,先显式回滚 B;需要重新观察时,从清理后的事务状态重做,不要不断叠加操作。

怎样证明它在等哪把锁?

有相应 Performance Schema 查询权限时,在 C 中把等待表与锁表关联。下面同时展示请求方与阻塞方的索引、锁模式及锁数据,避免只看“有一条 X 锁”就认定原因。

-- 会话 C:只读定位等待关系,ENGINE 也作为关联条件。
SELECT w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx,
       w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx,
       r.OBJECT_SCHEMA, r.OBJECT_NAME, r.INDEX_NAME,
       r.LOCK_MODE AS requested_mode,
       r.LOCK_DATA AS requested_data,
       b.LOCK_MODE AS blocking_mode,
       b.LOCK_DATA AS blocking_data
FROM performance_schema.data_lock_waits AS w
JOIN performance_schema.data_locks AS r
  ON r.ENGINE = w.ENGINE
 AND r.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID
JOIN performance_schema.data_locks AS b
  ON b.ENGINE = w.ENGINE
 AND b.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID
WHERE r.OBJECT_NAME = 'gap_scope_demo';

结合实际库名、索引和事务信息,确认等待属于这次实验。插入方可能显示带 INSERT_INTENTION 的请求,阻塞方要结合是否包含 GAP 语义解释。锁数据里的某个索引记录值也可能用于表示其前方间隙,不能看到 30 就理解成“只锁住 id=30”。锁数据缺失时也不能凭空补全锁范围。

这里的等待关系是某一时刻的快照。事务结束、超时或被选作死锁牺牲者后,相关等待可能消失,因此事后查不到行不能证明此前没有等待。还要区分事务内部 ID、Performance Schema 线程 ID 与客户端连接 ID,不能拿到一种 ID 就直接当作另一种使用。

请求锁阻塞锁以及等待表之间的静态关联结构
结构说明图:data_lock_waits 保存依赖,data_locks 提供锁详情;ENGINE 与锁 ID 共同关联请求锁和阻塞锁,不表示执行顺序。

释放等待后,怎样调整业务代码?

实验中先在 A 执行 ROLLBACK。若 B 的插入随后成功,再在 B 回滚,避免改变样本数据;如果 B 已超时,也要清理其事务。最后确认两边事务都结束,再删除示例表。

-- 在会话 A 中执行:结束持有范围锁的事务。
ROLLBACK;
-- 在会话 B 中执行:插入完成或超时报错后,都显式清理事务。
ROLLBACK;
-- 两个事务结束后,在测试库清理演示表。
DROP TABLE gap_scope_demo;

业务修复优先检查事务是否夹带了网络请求、人工等待或无关计算,缩短持锁时间;再检查索引与谓词是否让扫描范围超出需求。唯一索引完整键值且命中现有单行的查找,与范围查找、未命中查找不同;不能为了减少等待把“查空并准备插入”的语义误改成不加保护的判断。

READ COMMITTED 下,普通搜索和索引扫描通常不使用 gap locking,但外键检查与重复键检查仍会使用。改变隔离级别会改变并发语义,必须按业务不变量设计,不应仅因示例不再等待就直接改全局配置。对于“同一业务键只能创建一次”,仍应靠合适的唯一约束和正确错误处理承接并发结果。

发现的情况下一步
插入值在 WHERE 外确认它在实际索引顺序中属于哪个间隙
锁定读返回空集检查间隙锁,不以返回行数推断锁数
换成普通 SELECT 后不等待先确认是否丢失业务需要的锁定语义
调整隔离级别同时评估约束、读一致性及重试策略

两个事务都拿到 gap lock,为什么还会卡?

gap lock 之间可以共存,它们的主要作用是阻止向间隙插入。两个事务随后都尝试插入时,仍可能出现等待甚至死锁;“都取得间隙锁”不等于后续写入一定成功。

只看 EXPLAIN 能确定锁范围吗?

不能。EXPLAIN 能帮助解释访问路径,但实际锁还依赖事务隔离级别、索引数据分布和运行时行为。应把实际 SQL、表定义、事务状态及等待依赖放在一起判断。

参考:InnoDB 锁类型与间隙锁、data_lock_waits、data_locks。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
MySQL 一致性读为什么看不到刚提交的数据MySQL 一致性读为什么看不到刚提交的数据
上一篇
MySQL 一致性读为什么看不到刚提交的数据
MySQL utf8mb4_0900_ai_ci 怎样处理重音字符比较
下一篇
MySQL utf8mb4_0900_ai_ci 怎样处理重音字符比较
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    761次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    758次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    842次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    810次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    640次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码