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