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