Замечания по блокировкам индексов
Методы доступа к индексу должны обрабатывать одновременное обновление индекса несколькими процессами. Основная система PostgreSQL получает AccessShareLock
на индекс во время сканирования индекса и RowExclusiveLock
при обновлении индекса (включая простой VACUUM
). Поскольку эти типы блокировок не конфликтуют, метод доступа отвечает за обработку любой тонкой блокировки, которая может ему понадобиться. Блокировка ACCESS EXCLUSIVE
на индекс в целом будет снята только при создании, разрушении или обновлении индекса (вместо этого снимается блокировка SHARE UPDATE EXCLUSIVE
при CONCURRENTLY
).
Создание типа индекса, поддерживающего одновременные обновления, обычно требует обширного и тонкого анализа требуемого поведения. Для типов индексов b-tree и hash можно прочитать о соответствующих проектных решениях в src/backend/access/nbtree/README
и src/backend/access/hash/README
.
Помимо внутренних требований к согласованности индекса, одновременные обновления создают проблемы с согласованно стью между родительской таблицей (кучей) и индексом. Поскольку PostgreSQL разделяет доступ и обновления кучи и обновления индекса, существуют окна, в которых индекс может быть несовместим с кучей. Эта проблема решается с помощью следующих правил:
- Новая запись в куче создается до создания записей в индексе. (Поэтому одновременное сканирование индекса, скорее всего, не увидит запись в куче. Это нормально, потому что читателю индекса в любом случае будет неинтересна незафиксированная строка (см. раздел «Проверки уникальности индексов»).
- При удалении элемента кучи (с помощью
VACUUM
) сначала должны быть удалены все его индексные записи. - При сканировании индекса должен сохраняться пин на странице индекса, содержащей элемент, последний раз возвращенный кортежем
amgettuple
, иambulkdelete
не может удалять записи со страниц, которые пиннованы другими бэкендами. Необходимость этого правила объясняется ниже.
Без третьего правила читатель индекса может увидеть элемент индекса непосредственно перед его удалением с помощью VACUUM
, а затем прийти к соответствующему элементу кучи уже после его удаления с помощью VACUUM
. Это не создаст серьезных проблем, если номер элемента все еще не используется, когда читатель добирается до него, поскольку пустой слот элемента будет проигнорирован функцией heap_fetch()
. Но что, если третий бэкенд уже повторно использовал слот элемента для чего-то другого? При использовании MVCC-совместимого снапшота проблем не возникает, поскольку новый обитатель слота наверняка будет слишком новым, чтобы пройти тест снапшота. Однако при использовании несовместимого с MVCC моментального снимка (например, SnapshotAny
) можно принять и вернуть строку, которая на самом деле не соответствует ключам сканирования. Можно было бы защититься от этого сценария, требуя, чтобы ключи сканирования во всех случаях перепроверялись по строке кучи, но это слишком дорого. Вместо этого, мы закрепляем страницу индекса как промежуточный объект, показывающий, что читатель может все еще быть «в пути» от записи индекса к соответствующей строке данных. Блокировка ambulkdelete
на таком пине гарантирует, что VACUUM
не может удалить зап ись в куче до того, как читатель закончит работу с ней. Это решение не требует больших затрат времени выполнения и добавляет накладные расходы на блокировку только в редких случаях, когда действительно возникает конфликт.
Это решение требует, чтобы сканирование индекса было «синхронным»: мы должны получать каждый кортеж из кучи сразу же после сканирования соответствующей записи в индексе. Это дорого по ряду причин. Асинхронное сканирование, при котором мы собираем множество TID из индекса и обращаемся к кортежам кучи только некоторое время спустя, требует гораздо меньше накладных расходов на блокировку индекса и может обеспечить более эффективную схему доступа к куче. Согласно приведенному выше анализу, мы должны использовать синхронный подход для не MVCC-совместимых снимков, но асинхронное сканирование вполне применимо для запросов, использующих MVCC-снимок.
При индексном сканировании amgetbitmap
метод доступа не сохраняет индексный пин ни для одного из возвращаемых кортежей. Поэтому такие сканирования безопасно использовать только с MVCC-совместимыми снапшотами.
Когда флаг ampredlocks
не установлен, любое сканирование с использованием этого метода доступа к индексу в рамках сериализуемой транзакции получит неблокируемую предикатную блокировку на полный индекс. Это вызовет конфликт чтения-записи при вставке любого кортежа в этот индекс параллельной сериализуемой транзакцией. Если среди множества параллельных сериализуемых транзакций обнаруживаются определенные конфликты чтения-записи, одна из этих транзакций может быть отменена для защиты целостности данных. Если флаг установлен, это указывает на то, что метод доступа к индексу реализует более тонкую блокировку предикатов, что позволит снизить частоту отмены таких транзакций.