它的本质是**间隙锁不是锁在“数据”上而是锁在索引之间的“空隙” (Gaps between Index Records)上。核心矛盾普通的行锁 (Record Lock) 只能锁定已存在的记录。如果事务 A 查询id 10此时并没有id11的记录。如果没有间隙锁事务 B 可以插入id11。当事务 A 再次查询时会多出一行数据这就是幻读 (Phantom Read)。解决方案InnoDB 不仅锁定匹配的记录还锁定记录之前和之后的范围。任何试图在这个范围内插入 (Insert)新记录的操作都会被阻塞。核心逻辑别把间隙锁当成“锁住空气”。把它当成划定禁区 (No-Fly Zone)。虽然现在天上没飞机但我划定了这片空域禁止飞行确保我的观测结果不会突然多出东西来。如果把索引比作排队的人群记录锁 (Record Lock)是抓住某个人如张三。别人不能推他、打他、带走他。间隙锁 (Gap Lock)是站在两个人中间如张三和李四之间或者队首/队尾。你张开双臂挡住中间的空位。如果有新人新插入的数据想挤进这个位置你会拦住他“这里禁止入内”。价值确保队伍的相对顺序和成员构成在你观察期间不发生变化。核心逻辑间隙锁的核心目的是防止插入 (Prevent Insertion)从而保证范围查询的可重复性。一、底层实现机制锁住“不存在”的东西1. 锁存储在索引节点中事实间隙锁同样存储在B 树索引的内存结构中。机制InnoDB 为每个索引记录维护一个锁列表 (Lock List)。除了标记该记录本身的锁Record Lock还会标记该记录左侧间隙的锁状态Gap Lock。例如对于索引值10和20InnoDB 会在10的记录上标记“右侧间隙被锁定”或在20的记录上标记“左侧间隙被锁定”。2. 区间表示法符号(a, b)表示开区间不包含 a 和 b。Next-Key Lock(a, b]表示左开右闭包含 b 但不包含 a。示例表中有索引值5, 10, 15。执行SELECT * FROM t WHERE id 10 FOR UPDATE。如果id是非唯一索引InnoDB 会锁定(5, 10]和(10, 15)两个间隙具体取决于实现细节通常是 Next-Key Lock。这意味着id6, 7, 8, 9的插入会被阻塞id11, 12, 13, 14的插入也会被阻塞。 核心洞察间隙锁是逻辑上的占位符。它不占用磁盘空间只在内存的锁管理器中存在直到事务结束。二、加锁规则什么时候触发间隙锁间隙锁主要在RR (Repeatable Read)隔离级别下生效且在当前读 (Current Read)场景中触发。1. 范围查询 (Range Query)SQLSELECT ... WHERE id 10 AND id 20 FOR UPDATE行为锁定所有满足条件的记录Record Lock。锁定条件范围内的所有间隙Gap Lock。效果阻止任何id在(10, 20)之间的新记录插入。2. 非唯一索引的等值查询SQLSELECT ... WHERE name Alice FOR UPDATE(name是非唯一索引)行为因为可能有多个 ‘Alice’InnoDB 无法确定是否还有未扫描到的 ‘Alice’。它会锁定所有 ‘Alice’ 记录并锁定‘Alice’ 之前和之后的间隙。效果防止在 ‘Alice’ 附近插入新的 ‘Alice’ 或其他值确保扫描完整性。3. 唯一索引的等值查询 (特殊情况)SQLSELECT ... WHERE id 10 FOR UPDATE(id是主键/唯一索引)行为退化如果id10存在只加Record Lock不加 Gap Lock。因为唯一性保证了不会有重复也不需要防止插入相同的 ID。例外如果id10不存在则会加Gap Lock在10所在的间隙上防止其他事务插入id10。 核心洞察唯一索引能“消除”间隙锁这是优化并发性能的关键。尽量使用唯一索引进行精确查找。三、与 MVCC 的关系为什么普通 SELECT 不受影响1. 快照读 (Snapshot Read)语句普通SELECT。机制读取 Undo Log 中的历史版本。关系间隙锁不阻塞快照读。即使间隙锁禁止了插入快照读依然可以看到“过去”的状态或者看不到“未来”插入的数据取决于可见性算法。价值保证了极高的读并发。2. 当前读 (Current Read)语句SELECT ... FOR UPDATE,INSERT,UPDATE,DELETE。机制读取最新数据并加锁。关系间隙锁阻塞当前读的插入操作。如果事务 B 尝试INSERT INTO t VALUES (11, ...)而事务 A 持有(10, 20)的间隙锁事务 B 会进入等待状态 (Waiting)直到事务 A 提交或回滚。四、PHP 开发者的实战指南1. 如何避免不必要的间隙锁策略 1使用唯一索引将频繁查询的字段设为UNIQUE。WHERE unique_id 1只会加行锁不会加间隙锁。策略 2降低隔离级别将隔离级别设为READ COMMITTED (RC)。在 RC 级别下InnoDB不使用间隙锁只使用 Record Lock。代价会出现幻读但并发性能大幅提升。大多数互联网应用如阿里系默认使用 RC。策略 3精确查询避免BETWEEN等范围查询改用IN列表如果数据量小。IN列表会对每个值加行锁间隙锁的影响较小但仍可能存在。2. 死锁风险场景事务 A锁定(10, 20)间隙想插入15。事务 B锁定(10, 20)间隙想插入15。结果两者互相等待对方释放间隙锁不间隙锁是兼容的注意多个事务可以同时持有同一个间隙的 Gap Lock。但是当它们都尝试插入同一行时会转化为插入意向锁 (Insert Intention Lock)这时才会互斥并可能导致死锁。 核心洞察间隙锁本身是共享的 (Shared)但插入意向锁是排他的 (Exclusive)。死锁通常发生在多个事务试图向同一个间隙插入数据时。五、认知牢笼常见误区1. 误区“间隙锁会阻塞更新 (UPDATE)。”真相间隙锁只阻塞插入 (INSERT)。它不阻塞对已有记录的更新。对策如果业务主要是更新间隙锁的影响很小。2. 误区“RC 级别下完全没有间隙锁。”真相基本正确。RC 级别下InnoDB 仅使用 Record Lock。对策如果业务允许幻读优先使用 RC 级别以提升并发。3. 误区“间隙锁很消耗内存。”真相锁信息存储在内存中但相比数据量锁的开销很小。对策真正的瓶颈是等待时间而非内存占用。4. 误区“无索引查询也会产生间隙锁。”真相无索引查询会扫描全表并对每一行及其间隙加锁。这实际上等同于表锁且效率极低。对策永远确保查询走索引。5. 误区“间隙锁会导致所有并发插入失败。”真相只有插入到被锁定的间隙才会失败。插入到其他间隙不受影响。对策合理设计索引分布避免热点间隙竞争。 总结原子化“MySQL 间隙锁”全景图维度关键点本质锁定索引记录之间的空隙防止插入新记录核心目的解决幻读问题保证范围查询的一致性触发条件RR 隔离级别 当前读 (FOR UPDATE/INSERT/UPDATE) 非唯一索引/范围查询阻塞对象仅阻塞插入 (INSERT)操作不阻塞更新和快照读优化策略使用唯一索引、切换 RC 隔离级别、避免范围查询PHP 隐喻No-Fly Zone (Gap Lock) vs. Air Traffic Control公式Consistency (Gap_Lock × Range_Query) ^ Isolation_Level终极心法间隙锁的本质是“对未来的预判与封锁”。它不仅保护现在还保护即将发生的变化。它是数据库一致性的最后一道防线。于虚空中见秩序于间隙中见严谨以范围为尺解幻读之牛于并发控制中求纯净之真。行动指令实验间隙锁在 RR 级别下开启两个事务。事务 A 执行SELECT ... WHERE id 10 FOR UPDATE。事务 B 尝试INSERT INTO t VALUES (11, ...)。观察事务 B 是否阻塞。对比 RC 级别将隔离级别改为 RC重复上述实验观察事务 B 是否还能插入。检查索引确保你的范围查询字段上有索引否则间隙锁会变成全表锁。思维升级记住间隙锁是双刃剑。它带来了强一致性也牺牲了部分并发写入能力。根据业务场景是否需要绝对一致选择隔离级别和索引策略。