59.4. 索引锁定考虑
索引访问方法必须支持多个进程对索引的并发更新。在索引扫描期间,核心PostgreSQL系统在索引上获取 AccessshareLock,并且在更新索引时(包括普通VACUUM)获取RowExclusiveLock。因为这些锁类型不会冲突,所以访问方法负责处理它可能需要的任何细粒度锁。把索引作为一个整体的排他锁只会在索引创建、删除或REINDEX时被使用。
创建一个支持并发更新的索引类型通常要求对所需的行为进行广泛并且细致的分析。对于b-tree 和哈希索引类型,你可以阅读src/backend/Access/nbtree/README和src/backend/access/hash/README中的设计决策。
除了索引自己内部的一致性要求之外,并发更新带来了一些父表(堆)和索引之 间的一致性问题。因为 PostgreSQL是把堆的访问和 更新与索引的访问和更新分开的,所以存在一些窗口期,在其间索引可能会与堆不一致。我们用下面的规则处理这样的问题:
一个新堆项在其索引项之前被制作(因此并发的索引扫描很可能看不到堆项。这么做应该是可以的,因为索引的读取者对未提交的行不感兴趣。见第 59.5 节)。
当一个堆项要被删除(通过VACUUM)时,它的所有索引项都必须首先删除。
一次索引扫描必须在保存有
amgettuple
最后返回项的索引页面上维护一个 pin, 并且ambulkdelete
不能从页面中删除被其他后端加 pin 的项。下面会解释需要这条规则的原因。
没有第三条规则,那么一个索引读取者是可以在一条索引项被VACUUM删除之前看到它的,并且然后在VACUUM删除它之后找到其对应的堆项。如果读取者到达该项时,该项编号仍然没有被使用,那么这种 情况不会导致严重的问题,因为空的项槽位会被heap_fetch()
忽略。 但是如果第三个后端已经为其它什么东西重用了这个项槽位又会怎样?在使用 MVCC 兼容的快照时,那么就不会有问题,因为槽位的新占据者太新了以至于无法通过快照测试。但是,对于非 MVCC 兼容的快照(例如 SnapshotAny),那么就有可能接受并返回一个实际上并不匹配扫描键的行。可以通过要求扫描键在所有情况下都在堆行上重新检查来避免这种情况,但是这种方法开销太大了。取而 代之的是,通过在索引页面上使用一个 pin 作为一个代理来表示,读取者可能还处于从索引项到匹配的堆项的"飞行中"。用ambulkdelete
来操作这样一个 pin 上的块确保VACUUM无法在读取者完成之前删除堆项。这种解决方案在运行时只有一点开销,而只是在真有一个冲突的非常罕见情况下才导致阻塞开销。
这个解决方法要求索引扫描是"同步的":我们不得不在扫描完对应的索引项之后马上去取每个堆元组。这样的方案开销比较大,原因有多个。而一个"异步的"扫描可以先从索引里收集很多 TID ,并且在稍后的某个时间只访问堆元组,这样要求更少的索引锁定负荷并且能够允许一种更高效的堆访问模式。但是按照上面的分析,在非 MVCC 兼容的快照上我们必须使用同步方法,而异步扫描则适合于使用 MVCC 快照的查询。
在一个amgetbitmap
索引扫描中,访问方法不会在任何被返回的元组上保持一个索引 pin。因此只有把这种扫描与 MVCC 兼容的快照一起使用才是安全的。
当ampredlocks标志没有被设置时,在一个可序列化事务中使用该索引访问方法的任何扫描将在整个索引上获取一个非阻塞的谓词锁。这将和一个并发可序列化事务中项索引中插入任何元组发生读-写冲突。如果在一组并发可序列化事务之间检测到特定模式的读-写冲突,其中一个事务可能会被取消来保护数据完整性。当该标志被设置,它表示该索引访问方法实现了细粒度的谓词锁,这将有望缩减这种事务取消的频率。