索引访问方法必须处理多个进程对索引的并发更新。核心 PostgreSQL 系统在索引扫描期间获取索引上的 AccessShareLock
,并在更新索引时(包括普通的 VACUUM
)获取 RowExclusiveLock
。由于这些锁类型不冲突,因此访问方法负责处理它可能需要的任何细粒度锁定。仅在索引创建、销毁或 REINDEX
期间才会对整个索引获取 ACCESS EXCLUSIVE
锁(使用 CONCURRENTLY
时会改为获取 SHARE UPDATE EXCLUSIVE
)。
构建支持并发更新的索引类型通常需要对所需行为进行广泛而细致的分析。对于 b 树和哈希索引类型,您可以阅读 src/backend/access/nbtree/README
和 src/backend/access/hash/README
中涉及的设计决策。
除了索引自身的内部一致性要求外,并发更新还会产生关于父表(堆)和索引之间一致性的问题。由于 PostgreSQL 将对堆的访问和更新与对索引的访问和更新分开,因此存在索引可能与堆不一致的时间窗口。我们通过以下规则来处理这个问题
在创建索引条目之前,会先创建一个新的堆条目。(因此,并发索引扫描很可能看不到堆条目。这是可以的,因为索引读取器对未提交的行不感兴趣。但请参见第 62.5 节。)
当要删除堆条目(通过 VACUUM
)时,必须先删除其所有索引条目。
索引扫描必须在索引页上维护一个针脚,该索引页保存着 amgettuple
最后返回的项,并且 ambulkdelete
不能删除其他后端已固定的页面中的条目。下面解释此规则的必要性。
如果没有第三条规则,索引读取器可能会在 VACUUM
删除索引条目之前看到该条目,然后在 VACUUM
删除相应堆条目后到达该条目。如果当读取器到达时该项编号仍未使用,则这不会产生严重问题,因为 heap_fetch()
将忽略空项槽。但是,如果第三个后端已经将该项槽重新用于其他内容怎么办?在使用符合 MVCC 的快照时,没有问题,因为槽的新占用者肯定太新而无法通过快照测试。但是,使用不符合 MVCC 的快照(例如 SnapshotAny
)时,可能会接受并返回实际上不匹配扫描键的行。我们可以通过要求在所有情况下都针对堆行重新检查扫描键来防御这种情况,但这代价太高了。相反,我们使用索引页上的一个针脚作为代理,以指示读取器可能仍然“在从索引条目到匹配的堆条目之间”。使 ambulkdelete
阻塞在这种针脚上可确保 VACUUM
不会在读取器完成之前删除堆条目。此解决方案在运行时几乎不产生任何成本,并且仅在实际存在冲突的极少数情况下才会增加阻塞开销。
此解决方案要求索引扫描是“同步的”:我们必须在扫描相应的索引条目后立即获取每个堆元组。由于多种原因,这非常耗费资源。在“异步”扫描中,我们从索引中收集许多 TID,并且仅在稍后访问堆元组,这种方式所需的索引锁定开销要少得多,并且可以允许更有效的堆访问模式。根据以上分析,我们必须将同步方法用于不符合 MVCC 的快照,但异步扫描对于使用 MVCC 快照的查询是可行的。
在 amgetbitmap
索引扫描中,访问方法不会对任何返回的元组保留索引针脚。因此,仅将此类扫描与符合 MVCC 的快照一起使用才是安全的。
当 ampredlocks
标志未设置时,任何在可序列化事务中使用该索引访问方法的扫描都将在整个索引上获取一个非阻塞谓词锁。这将与并发可序列化事务将任何元组插入到该索引中产生读写冲突。如果在并发可序列化事务集中检测到某些读写冲突模式,则可能会取消其中一个事务以保护数据完整性。当设置该标志时,它表示索引访问方法实现了更细粒度的谓词锁定,这将倾向于减少此类事务取消的频率。
如果您在文档中发现任何不正确、与您对特定功能的体验不符或需要进一步澄清的内容,请使用此表单报告文档问题。