50.4. 索引锁定注意事项 #
索引访问方法必须处理多个进程并发更新同一个索引的情况。PostgreSQL 核心系统会在索引扫描期间对索引获取 AccessShareLock,并在更新索引时(包括普通的 VACUUM)获取 RowExclusiveLock。由于这些锁类型彼此不冲突,所以访问方法必须自行处理它可能需要的任何细粒度锁定。对整个索引的排他锁只会在索引创建、销毁、REINDEX 或 VACUUM FULL 期间获取。
构建一种支持并发更新的索引类型,通常需要对所需行为进行大量而细致的分析。对于 B-树和 hash 索引类型,可以阅读 src/backend/access/nbtree/README 和 src/backend/access/hash/README 中涉及的设计决策。
除了索引自身内部一致性的要求外,并发更新还会带来父表(即堆)与索引之间一致性的问题。由于 PostgreSQL 把对堆的访问和更新与对索引的访问和更新分离开来,因此存在一些窗口期,在这些窗口期内索引可能与堆不一致。我们通过以下规则处理这个问题:
先创建新的堆条目,再创建它对应的索引条目。(因此并发索引扫描很可能看不到这个堆条目。这是可以接受的,因为索引读取者本来也不会关心未提交的行。但也请见第 50.5 节。)
当某个堆条目将要被删除(由
VACUUM执行)时,必须先删除它的所有索引条目。索引扫描必须在保存
amgettuple最近一次返回条目的索引页面上保持钉住状态(pin),而ambulkdelete不能从被其他后端钉住的页面中删除条目。下面会解释为什么需要这条规则。
如果没有第三条规则,索引读取者就有可能在某个索引条目被
VACUUM 删除之前先看到它,然后在对应的堆条目已被
VACUUM 删除之后才到达那里。如果读取者到达时,该项号仍未被重新使用,就不会造成严重问题,因为空项槽位会被
heap_fetch() 忽略。但如果第三个后端已经把这个项槽位重新用作别的东西呢?在使用 MVCC 兼容快照时不会有问题,因为该槽位的新占用者一定太新,无法通过快照测试。然而,对于非 MVCC
兼容的快照(例如 SnapshotNow),就可能错误地接受并返回一行实际上并不匹配扫描键的数据。我们可以要求在所有情况下都对堆行重新检查扫描键,以防范这种情形,但那样代价太高。于是,我们改为把索引页面的钉住状态作为一种代理,表示读取者从索引条目到匹配堆条目的访问过程可能仍“在进行中”。让 ambulkdelete 因这种钉住状态而阻塞,能够保证
VACUUM 不会在读取者处理完之前删除对应的堆条目。这种方案运行时开销很小,只有在真正发生冲突的少数情况下才会增加阻塞开销。
这种解决方案要求索引扫描是“同步的”:我们必须在扫描到相应索引条目之后立刻取出对应的堆元组。由于多种原因,这会比较昂贵。相反,“异步”扫描可以先从索引中收集许多 TID,再在稍后某个时刻访问堆元组,这样索引锁定的开销要小得多,也能实现更高效的堆访问模式。根据前面的分析,对于非 MVCC 兼容快照,我们必须采用同步方式;而对于使用 MVCC 快照的查询,异步扫描则是可行的。
在 amgetmulti 索引扫描中,访问方法不会为任何返回的元组保持索引页面的钉住状态。因此,只有把这种扫描与 MVCC 兼容的快照一起使用才是安全的。