55.4. 索引锁的考量

索引访问方法必须支持多个进程对索引的并发更新。 在索引扫描期间,PostgreSQL核心系统在索引上抓取 AccessShareLock ,并且在更新索引期间(包括VACUUM)也会抓取RowExclusiveLock。 因为这些锁类型不会冲突,所以访问方法有责任处理任何它自己需要的更细致的锁需求。 把整个索引锁住的排他锁只是在创建和删除索引或者REINDEX的时候使用。

创建一个支持并发更新的索引类型通常要求对所需的行为进行广泛的并且细致的分析。 对于 b-tree 和 Hash 索引类型,你可以读取在 src/backend/access/nbtree/READMEsrc/backend/access/hash/README里面描述的设计决策。

除了索引自己内部的一致性要求之外,并发更新产生了一些有关父表()和索引之间的一致性问题。 因为PostgreSQL是把堆的访问和更新与索引的访问和更新分开的。 用下面的规则处理这样的问题:

如果没有第三条规则,那么一个索引读者是可能在一条索引记录刚要被VACUUM删除之前看到它, 然后在对应的堆记录已经被VACUUM删除时,去找这条堆记录。 如果读者到达该项时,该项编号仍然没有使用,那么这种情况不会导致严重的问题,因为空的项槽位会被heap_fetch()忽略。 但是如果第三个后端已经为其它什么东西复用了这个项槽位又如何? 如果使用 MVCC 兼容的快照,那么就不会有问题,因为新占据的槽位当然是太新了,因而无法通过快照测试。 但是,对于非 MVCC 兼容的快照(比如SnapshotNow),那么就有可能接受并返回一个实际上并不匹配扫描键字的行。 可以通过要求扫描键字在所有场合下都重新检查的方法来避免这种情况,但是这种方法开销太大了。 取而代之的是,通过在索引页面上使用一个销,当作一个代理,告诉系统说,读者可能还在对应堆记录的索引记录上空"飞行"。 让ambulkdelete在这样的销上阻塞可确保VACUUM无法在读者完成读取之前删除堆记录。 这种解觉办法只增加了一点点运行时开销,并且只是在非常罕见的实际有冲突的情况下才导致阻塞开销。

这个解决方法要求索引扫描必须是"同步的":必须在扫描完对应的索引记录之后马上抓取每个堆记录。 这样的方案开销比较大,原因有若干个。 而"异步的"扫描,可以先从索引里收集很多 TID ,然后在稍后的某个时间访问堆行,这样就会绕开很多索引锁的开销,以及可以允许更有效的堆访问模式。 但是按照上面的分析,在非 MVCC 兼容快照上必须使用同步方法,而对使用 MVCC 快照的查询,使用异步扫描应该是可行的。

amgetbitmap索引扫描里,访问方法不需要保证在任何返回的行上保持一个销。 毕竟,除了给最后一个行加销之外,也没法给其它的加。 因此,只能在 MVCC 兼容的快照里使用这样的扫描。

如果没有设置ampredlocks,任何在可串行化事务中使用这个索引访问方法的扫描将会在整个索引上获得一个非阻塞的谓词锁。 与其并发的另一个可串行化事务向这个索引中插入任何一个元组时都会引发一个读写冲突。 如果在并发的可串行化事务间检测到某种模式的读写冲突,为了保证数据一致性其中一个事务可能会被取消。 如果设置了这个标志,表明这种索引访问方法实现了精细的谓词锁,因而趋向于削减这种事务取消的频度。