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