Skip to content
Go back

MySQL插入意向锁——间隙锁的并发优化变体

MySQL 插入意向锁:同一间隙为何能并发插入?

一句话结论(30s)

插入意向锁是间隙锁的并发优化变体,本质是同一间隙内不同位置的 INSERT 锁住的是各自待插入的”点”而非整个间隙,所以相互兼容、能并发插入。关键设计是:它只被”真正的间隙锁”(如 SELECT…FOR UPDATE 持有)阻塞,不被其他插入意向锁阻塞。权衡在于:用”点级”锁替代”区间级”锁,既保住 RR 防幻读语义,又最大化并发插入吞吐。

核心原理(2min)

间隙锁 (a, b) 会锁住整个区间,导致同一间隙内的并发 INSERT 互相阻塞、完全串行。插入意向锁把锁粒度缩小到待插入的具体位置:事务 A INSERT 15 和事务 B INSERT 17 在 (10, 20) 上各取一个插入意向锁,因为位置不同所以互不冲突。但当某事务用 SELECT ... WHERE id > 10 AND id < 20 FOR UPDATE 持有了 (10, 20) 的间隙锁/临键锁时,任何在该间隙内的 INSERT 都会因拿不到插入意向锁而阻塞。这样既保留了间隙锁对防幻读的兜底,又让不同位置的插入能并发执行。

底层深入(5-10min)

插入意向锁的标志位

插入意向锁在源码里不是独立的锁类型,而是 LOCK_GAPLOCK_INSERT_INTENTION 两个标志位的组合,定义在 include/lock0lock.h

/** when this bit is set, it means that the lock holds only on the gap before
  the record; for instance, an x-lock on the gap does not give permission to
  modify the record on which the bit is set; locks of this type are created
  when records are removed from the index chain of records */
constexpr uint32_t LOCK_GAP = 512;
/** this bit is set when we place a waiting gap type record lock request in
   order to let an insert of an index record to wait until there are no
   conflicting locks by other transactions on the gap; note that this flag
   remains set when the waiting lock is granted, or if the lock is inherited to
   a neighboring record */
constexpr uint32_t LOCK_INSERT_INTENTION = 2048;

分析:插入意向锁 = LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,即”排他 + 锁间隙 + 意图标记”。它本质上是间隙锁(带 LOCK_GAP),但多出的 LOCK_INSERT_INTENTION 标记用于在冲突检测时把它和”真正的间隙锁”区分开。

💭 想一想:既然它”本质上还是间隙锁”,凭什么能和别的插入意向锁共存?——关键就在那个多出来的 LOCK_INSERT_INTENTION 标志:冲突检测时只要看到对方是”插入意向锁”,就直接判不冲突、不排队。也就是说,它保留间隙锁的”壳”来参与 RR 防幻读语义,却用这个标志把”点级”插入彼此之间解锁了。

加锁入口:lock_rec_insert_check_and_lock

INSERT 落锁走 lock_rec_insert_check_and_locklock/lock0lock.cc),它只检查插入点后继记录上的间隙锁是否冲突:

      const ulint type_mode = LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION;

      const auto conflicting =
          lock_rec_other_has_conflicting(type_mode, block, heap_no, trx);

      /* LOCK_INSERT_INTENTION locks can not be allowed to bypass waiting locks,
      because they allow insertion of a record which splits the gap which would
      lead to duplication of the waiting lock, violating the constraint that
      each transaction can wait for at most one lock at any given time */
      ut_a(!conflicting.bypassed);

      if (conflicting.wait_for != nullptr) {
        RecLock rec_lock(thr, index, block, heap_no, type_mode);

        trx_mutex_enter(trx);

        err = rec_lock.add_to_waitq(conflicting.wait_for);

        trx_mutex_exit(trx);
      }

分析:type_mode = LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION 就是插入意向锁的完整位组合。lock_rec_other_has_conflicting 检查后继记录上是否有冲突锁,有就把自己挂进等待队列(add_to_waitq)——这正是”被真正的间隙锁阻塞”的落地。

兼容规则:为什么插入意向锁互不阻塞

冲突判断的核心在 rec_lock_has_to_wait,其中处理插入意向锁的几条规则:

  if (!(type_mode & LOCK_INSERT_INTENTION) && lock_rec_get_gap(lock2)) {
    /* Record lock (LOCK_ORDINARY or LOCK_REC_NOT_GAP
    does not need to wait for a gap type lock */

    return Conflict::NO_CONFLICT;
  }

  if ((type_mode & LOCK_GAP) && lock_rec_get_rec_not_gap(lock2)) {
    /* Lock on gap does not need to wait for
    a LOCK_REC_NOT_GAP type lock */

    return Conflict::NO_CONFLICT;
  }

  if (lock_rec_get_insert_intention(lock2)) {
    /* No lock request needs to wait for an insert
    intention lock to be removed. This is ok since our
    rules allow conflicting locks on gaps. This eliminates
    a spurious deadlock caused by a next-key lock waiting
    for an insert intention lock; when the insert
    intention lock was granted, the insert deadlocked on
    the waiting next-key lock.

    Also, insert intention locks do not disturb each
    other. */

    return Conflict::NO_CONFLICT;
  }

分析:最后一条 if (lock_rec_get_insert_intention(lock2)) 是核心——“已有锁是插入意向锁”时,任何新请求都不等待,直接返回 NO_CONFLICT。注释里那句 “Also, insert intention locks do not disturb each other”(插入意向锁互不打扰)正是”同一间隙不同位置可并发插入”的源码依据。

💭 想一想:为什么”位置不同”就天然不冲突?——因为插入意向锁锁的是”待插入的那个点”,而不是整段间隙;事务 A 插 15、事务 B 插 17,两个点互不重叠,谁也不会碍着谁。它把间隙锁的”区间级互斥”降成了”点级兼容”,这正是并发吞吐的来源。

何时会被阻塞

反向看:一个插入意向锁(带 LOCK_INSERT_INTENTION 标志)要等谁?当后继记录上已有一个”不带 INSERT_INTENTIONLOCK_GAP 或临键锁”时,lock_rec_other_has_conflicting 返回非空,INSERT 进入等待队列。也就是说,插入意向锁只被真正的间隙锁(如 SELECT ... FOR UPDATE 持有的 LOCK_GAP / LOCK_ORDINARY)阻塞,不被其他插入意向锁阻塞。

间隙锁的问题

间隙锁 (a, b) 锁住了两个索引记录之间的整个空间。如果事务 A 持有间隙锁,事务 B 想在间隙内 INSERT → 被阻塞。这对并发插入极不友好。

插入意向锁的解决

插入意向锁(Insert Intention Lock)是间隙锁的特殊变体

事务 A: INSERT INTO t VALUES (15);
  → 获得 (10, 20) 间隙上的插入意向锁

事务 B: INSERT INTO t VALUES (17);
  → 也获得 (10, 20) 间隙上的插入意向锁
  
  → 不冲突!两个插入意向锁互相兼容 ✓

同一间隙内的不同位置的插入意向锁相互兼容——因为它们锁的是不同的、待插入的”点”,不是整个间隙。

何时不兼容?

事务 A: SELECT * FROM t WHERE id > 10 AND id < 20 FOR UPDATE;
  → 持有 (10, 20) 的间隙锁(或临键锁)

事务 B: INSERT INTO t VALUES (15);
  → 需要 (10, 20) 的插入意向锁
  → 但 A 持有的间隙锁阻止了 B 的插入意向锁
  → 阻塞 ✓

插入意向锁只被”真正的间隙锁”阻塞,不被其他插入意向锁阻塞。

💭 想一想:为什么它”怕” SELECT…FOR UPDATE 的间隙锁、却”不怕”别的插入意向锁?——因为真正的间隙锁是”锁住整段区间、不让任何新行挤进来”,而插入意向锁恰恰要”往区间里塞一个新点”,二者天然对立,所以插入必须等间隙锁释放;而两个插入意向锁塞的是不同的点,互不侵占,故不冲突。

为什么需要这个机制?

如果所有 INSERT 都像 SELECT…FOR UPDATE 一样用间隙锁互斥,同一间隙内的多线程并发插入将完全串行——间隙越大,并发性能越差。插入意向锁让”不同位置的插入”可以并发,同时仍能阻挡持有整个间隙锁的 SELECT…FOR UPDATE——既保证了 RR 级别的防幻读语义,又最大化并发插入吞吐。

总结

间隙锁插入意向锁
持有者SELECT…FOR UPDATEINSERT
锁范围整个间隙待插入的具体位置
互相兼容×(互斥)√(只要位置不同)
被间隙锁阻塞

章末提问

追问 1:插入意向锁和间隙锁有什么区别?为什么插入意向锁之间不冲突?

回答思路:结论先行——区别在锁的粒度:间隙锁锁”整段区间”,插入意向锁锁”待插入的那个点”。因为插入意向锁是间隙锁的变体(带 LOCK_INSERT_INTENTION 标志),冲突检测时只要对方也是插入意向锁就判 NO_CONFLICT;两个事务插不同位置,锁的是不同的点、互不重叠,所以能并发插入。

追问 2:什么情况下插入意向锁会被阻塞?

回答思路:结论先行——只有当后继记录上已有”真正的间隙锁/临键锁”时才会阻塞。因为插入意向锁要往间隙里塞新行,而 SELECT…FOR UPDATE 这类持有的间隙锁锁住了整段区间、禁止任何新行进入,二者冲突;于是 INSERT 拿不到插入意向锁、进入等待队列。它只被真正的间隙锁阻塞,不被其他插入意向锁阻塞。

追问 3:如果没有插入意向锁,直接用间隙锁做 INSERT 会有什么问题?

回答思路:结论先行——同一间隙内的并发插入会完全串行化,吞吐崩掉。因为间隙锁是区间级互斥,所有要往同一间隙插行的事务都得排队等对方释放;间隙越大、并发插入越多,阻塞越严重。插入意向锁把互斥降到”点级”,让不同位置的插入并发执行,同时仍被整段间隙锁挡住、保住 RR 防幻读语义——是并发与一致性之间的折衷。


Share this post on:

Previous Post
TLAB——线程本地分配缓冲如何让对象分配几乎无锁
Next Post
ZGC——着色指针与读屏障实现亚毫秒级停顿