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