登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL InnoDB 隔离级别并发更新为什么出现间隙锁等待为什么结果为空或不符合预期

来源:17golang原创

时间:2026-11-10 00:14:29 436浏览 收藏

InnoDB 并发 UPDATE 等待不一定是两个事务修改同一条记录。REPEATABLE READ 下范围锁定可能保护索引记录之间的间隙;另一事务修改索引列时,新的索引项需要进入该间隙,也可能等待。纯间隙锁彼此可以共存,真正需要辨别的是记录冲突、插入意向请求与间隙保护的关系。

我会先查请求锁落在哪个索引,再考虑调整隔离级别。看到 UPDATE 或空结果就归因于“整张表锁住”,会把解决方向带偏。下面按 MySQL 8.4 文档组织受控示例,没有在本机执行,不提供虚构的锁输出。

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

先辨别记录锁与间隙约束

记录锁针对索引记录,间隙锁抑制往受保护区间插入,next-key 锁包含记录与其前方间隙。它们的对象是索引结构,不能只看 SQL 返回多少业务行来推断锁范围。

完整唯一键等值命中已有记录,与范围条件不是同一场景;不存在的键、联合唯一索引只指定部分列,以及非唯一索引条件,都不能套用同一种锁结论。普通一致性读的快照结果也不能说明锁定读或 UPDATE 没有等待。

范围条件关联二级索引,间隙保护与待插入索引项属于不同锁对象
范围条件关联二级索引,间隙保护与待插入索引项属于不同锁对象;静态结构说明图,不是锁等待截图或实测结果。

用两个会话理解索引值迁移

使用独立练习库和两个连接。示例显式指定隔离级别,并为讲解固定 idx_score 访问路径;FORCE INDEX 是演示条件,不是生产优化建议。

-- 仅在独立练习库创建,不改动业务表。
CREATE TABLE gap_update_demo (
  id INT NOT NULL PRIMARY KEY,
  score INT NOT NULL,
  note VARCHAR(40) NOT NULL,
  KEY idx_score (score)
) ENGINE=InnoDB;
INSERT INTO gap_update_demo VALUES (1,10,'甲'),(2,30,'乙');
-- 会话A:事务保持打开以观察范围保护,结束后必须回滚。
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT id,score FROM gap_update_demo FORCE INDEX (idx_score)
WHERE score>10 AND score

score 在10和30之间没有现存行,因此 A 可以返回空结果;这不代表该索引间隙没有被保护。

-- 会话B:修改另一条记录的索引值,目标值位于A保护的间隙。
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
UPDATE gap_update_demo SET score=20 WHERE id=1;
-- 此语句可能等待;完成或超时后均应明确执行 ROLLBACK。

B 按主键选择 id=1,但 score 改为20还涉及维护 idx_score。新索引项位于 A 的范围保护内,所以应关注插入意向等待,而不能只比较两条语句是否返回同一 id。具体锁标记与附加锁以实际环境为准。

若只改未参与该索引的 note,涉及的索引维护不同,不能拿它证明修改 score 也不会等待。真正生产语句往往还有外键、唯一性约束和其他索引,需要将它们一起纳入分析。

关联等待事务与索引对象

-- 在有监控表读取权限的第三个会话执行,保留实际返回而非猜测锁类型。
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_TYPE,r.LOCK_MODE AS requested_mode,
       r.LOCK_STATUS,r.LOCK_DATA,
       b.LOCK_MODE AS blocking_mode
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_update_demo';

data_lock_waits 记录请求与阻塞锁的关联,data_locks 提供表、索引和锁模式。关联时同时使用 ENGINE 和锁ID。查询结果是当前观测,不是历史永久记录;等待结束后为空很正常,权限或对象过滤错误也要分开处理。

等待关联表通过请求锁ID和阻塞锁ID连接两份锁明细
等待关联表通过请求锁ID和阻塞锁ID连接两份锁明细;静态结构说明图,不是锁等待截图或实测结果。

不能仅凭 LOCK_MODE 含 X 就判断是同一行写冲突,也不能见到 GAP 就断言两个纯间隙锁互相排斥。要结合 INDEX_NAME、LOCK_DATA、请求类型、阻塞关系和事务里的前序语句。

比较改索引和改隔离级别的代价

改动方向可能收益需要承担的代价
缩小扫描范围减少涉及的索引区域索引维护与计划变化
缩短事务缩短锁持有时间调整业务事务边界
评估 READ COMMITTED减少搜索扫描的间隙保护重新评估一致性与幻读
统一访问顺序降低交叉等待风险多入口共同遵守

READ COMMITTED 通常不为搜索和索引扫描使用间隙锁,但外键检查和重复键检查仍有例外;记录写冲突也不会消失。改变隔离级别属于业务语义选择,不是清除所有等待的开关。

我倾向先看事务有没有把远程调用、用户交互或大批量循环包在持锁区间,再决定是否改变隔离策略。只把超时调大,可能只是让排队更久。也不要用全局隔离级别改动替代对某个连接或任务的评估。

收尾事务并定义重试

观察完在 A 执行 ROLLBACK,让 B 从等待中结束;随后在 B 也执行 ROLLBACK,明确释放事务。若 B 已因超时退出,仍应由应用明确结束整笔事务,不要以为报错后连接里的所有状态都自动复原。

业务重试要有次数与退避上限,并基于幂等任务或唯一业务键。等待超时、死锁和语句成功但受影响行数为零是不同状态;“无结果”不能代替对错误码、提交结果与过滤条件的判断。

查询没有返回行,为什么还能挡住更新?

受保护的是符合范围的索引间隙,空业务结果并不能排除范围保护。

两个间隙锁一定冲突吗?

不一定,纯间隙锁可以共存。需要进入间隙的索引插入请求则可能被抑制。

主键 UPDATE 为何还涉及二级索引?

定位记录的路径与修改后需要维护的索引不是同一回事。更新索引列要处理新旧索引项。

参考:https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html;隔离级别:https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html;等待关联:https://dev.mysql.com/doc/refman/8.4/en/performance-schema-data-lock-waits-table.html。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>