14 Блокировки разных объектов
PostgreSQL 18 · Издание 2026 года · Китайский перевод доступен для PostgreSQL 18
14.1. Блокировки неотношений
Когда требуется заблокировать ресурс, не являющийся отношением в понимании PostgreSQL, используются тяжелые блокировки типа object1. Таким ресурсом может быть почти все, что можно найти в системном каталоге, например табличные пространства, подписки, схемы, роли, политики, перечислимые типы данных.
Начнем транзакцию и создадим в ней таблицу:
Теперь посмотрим на блокировки неотношений в pg_locks:
Блокируемый ресурс определяется здесь тремя полями:
database — oid базы данных, к которой относится блокируемый объект (или ноль, если это общий объект кластера);
classid — oid из pg_class, который соответствует имени таблицы системного каталога, определяющей тип ресурса;
objid — oid из таблицы системного каталога, на которую указал classid.
В столбце database указана база данных internals, к которой подключен сеанс. Столбец classid указывает на таблицу pg_namespace, содержащую схемы.
Теперь мы можем расшифровать objid:
Таким образом, заблокирована схема public, чтобы никто не мог удалить ее, пока транзакция еще не завершена.
Соответственно, при удалении объектов захватываются исключительные блокировки как самого объекта, так и всех, от которых он зависит2.
14.2. Блокировки расширения отношения
Когда число строк в отношении растет, PostgreSQL по возможности использует для вставки свободное место в имеющихся страницах. Но очевидно, что в какой-то момент приходится добавлять и новые, то есть расширять отношение. Физически страницы добавляются в конец соответствующего файла-сегмента (и это может привести к созданию нового сегмента).
Чтобы два процесса не начали добавлять страницы одновременно, эта операция защищена специальной тяжелой блокировкой с типом extend3. Такая же блокировка используется и при очистке индексов, чтобы другие процессы не могли добавить новые страницы во время сканирования.
Блокировка расширения отношения ведет себя немного иначе, чем рассмотренные ранее:
она снимается сразу по завершении расширения, не дожидаясь конца транзакции;
она не может приводить к взаимоблокировкам, поэтому в процедуре обнаружения для нее сделано исключение: она не попадает в граф ожиданий.
Тем не менее процедура проверки все равно вызывается, если ожидание расширения происходит дольше, чем установлено параметром deadlock_timeout. Это ненормальная ситуация, но она может возникнуть при большом потоке вставок из большого числа параллельно работающих процессов. В этом случае проверка может запускаться многократно, фактически парализуя нормальную работу системы.
При интенсивной вставке блокировка может стать узким местом, поэтому файлы таблиц расширяются не на одну страницу, а сразу на несколько (пропорционально числу ожидающих блокировку процессов, но не более чем на 64 страницы за один раз)4. Однако к файлам индексов на основе B-дерева по-прежнему добавляется только по одной странице5. (v. 16)
14.3. Блокировки страниц
Тяжелая блокировка на уровне страницы с типом page 6 применяется в единственном случае, связанном с особенностями GIN-индексов. (с. 612)
GIN-индексы позволяют ускорять поиск элементов в составных значениях, например слов в текстовых документах. Такие индексы в первом приближении можно представить как обычное B-дерево, в котором хранятся не сами документы, а отдельные слова этих документов. Поэтому при добавлении нового документа индекс приходится перестраивать довольно сильно, внося в него каждое слово, входящее в документ.
Чтобы улучшить производительность, GIN-индексы предоставляют возможность отложенной вставки, которая включается параметром хранения fastupdate. Новые слова сначала быстро добавляются в неупорядоченный список ожидания (pending list), а спустя какое-то время все накопившееся перемещается в основную индексную структуру. Экономия происходит за счет того, что разные документы с большой вероятностью содержат повторяющиеся слова. (по умолчанию: on)
Чтобы исключить перемещение слов из списка ожидания в основной индекс одновременно несколькими процессами, на время переноса метастраница индекса блокируется в исключительном режиме. Это не мешает обычному использованию индекса.
Как и блокировка расширения отношения, блокировка страницы снимается сразу по завершении работы (а не в конце транзакции) и не может приводить к взаимоблокировкам.
14.4. Рекомендательные блокировки
В отличие от других тяжелых блокировок (таких как блокировки отношений), рекомендательные блокировки 7 (advisory locks) никогда не устанавливаются автоматически, ими управляет разработчик приложения. Их удобно использовать, если приложению для каких-то целей требуется нестандартная логика блокирования.
Допустим, имеется ресурс, не соответствующий никакому объекту базы данных (который мы могли бы заблокировать командами типа SELECT FOR или LOCK TABLE). Для блокировки надо сопоставить ресурс с числовым идентификатором. Если у ресурса есть уникальное имя, то простой вариант — взять хеш-код от имени:
Имеется целое семейство функций 8 для управления рекомендательными блокировками. Их названия начинаются на pg_advisory и содержат дополнительные слова, уточняющие назначение функции:
lock — захватить блокировку;
try — захватить блокировку, если это можно сделать без ожидания;
unlock — освободить блокировку;
share — использовать разделяемый режим (по умолчанию используется исключительный режим);
xact — блокировать до конца транзакции (по умолчанию — до конца сеанса).
Для примера захватим исключительную блокировку до конца сеанса:
Чтобы блокирование действительно работало, другие процессы также должны придерживаться установленного порядка получения доступа к ресурсу. За соблюдением этого правила должно следить приложение.
Установленная блокировка не снимается и после завершения транзакции:
После окончания работы с ресурсом освобождаем блокировку явно:
14.5. Предикатные блокировки
Термин предикатная блокировка появился еще при первых попытках реализовать полную изоляцию на основе блокировок9. Проблема, с которой тогда столкнулись, заключалась в том, что даже блокировка всех прочитанных и измененных строк не дает полной изоляции: в таблице могут появиться новые строки, попадающие под прежние условия отбора, что приводит к возникновению фантомов. (с. 51)
Поэтому было предложено блокировать не строки, а условия (предикаты). При выполнении запроса с предикатом a > 10 блокировка самого предиката не даст добавить в таблицу новые строки, попадающие под это условие, и позволит избежать фантомов. Сложность в том, что при появлении запроса с другим условием, например a < 20, требуется определить, пересекаются ли эти условия. В общем случае это алгоритмически неразрешимая задача; на практике ее можно решить только для предикатов, имеющих очень простой вид (как в приведенном примере).
В PostgreSQL уровень изоляции Serializable достигается иначе. Для него используется протокол сериализуемой изоляции на основе снимков (Serializable Snapshot Isolation, SSI) 10. Термин предикатная блокировка остался, но смысл его в корне изменился. Фактически такие «блокировки» ничего не блокируют, а используются для отслеживания зависимостей по данным между транзакциями.
Доказано, что изоляция на основе снимков, реализованная на уровне Repeatable Read, допускает аномалию несогласованной записи и аномалию только читающей транзакции, но никакие другие аномалии невозможны. Двум названным аномалиям соответствуют определенные закономерности в графе зависимостей между транзакциями, которые можно обнаруживать, не затрачивая на это слишком много ресурсов. (с. 67)
Сложность в том, что необходимо различать зависимости двух видов:
одна транзакция читает строку, которая затем изменяется другой транзакцией (RW-зависимость);
одна транзакция изменяет строку, которую затем читает другая транзакция (WR-зависимость).
WR-зависимости можно обнаружить, используя уже имеющиеся обычные блокировки, а вот RW-зависимости приходится отслеживать дополнительно с помощью предикатных блокировок. Такое отслеживание включается при выборе уровня изоляции Serializable, и именно поэтому важно, чтобы все (или по крайней мере все взаимосвязанные) транзакции использовали этот уровень. Если какая-либо транзакция будет работать на другом уровне, она не будет устанавливать (и проверять) предикатные блокировки, и уровень Serializable выродится до Repeatable Read.
Еще раз повторюсь: несмотря на название, предикатные блокировки ничего не блокируют. Вместо этого при фиксации транзакции выполняется проверка, и если обнаруживается «нехорошая» структура зависимостей, которая может свидетельствовать об аномалии, транзакция обрывается.
Создадим таблицу с данными и индекс на ней, занимающий некоторое количество страниц (для этого указано низкое значение fillfactor):
Если запрос выполняется с помощью последовательного сканирования таблицы, предикатная блокировка устанавливается на всю таблицу целиком (даже если под условия фильтрации попадают не все строки).
Хотя предикатные блокировки используют собственную инфраструктуру, они отображаются в представлении pg_locks вместе с тяжелыми блокировками. Любые предикатные блокировки всегда захватываются в одном специальном режиме SIRead (Serializable Isolation Read):
Стоит иметь в виду, что предикатные блокировки не всегда снимаются сразу по завершении транзакции, ведь они нужны, чтобы отслеживать зависимости между транзакциями. Но в любом случае управление ими происходит автоматически.
Если же запрос выполняется с помощью индексного сканирования, ситуация меняется в лучшую сторону. В случае B-дерева достаточно установить предикатную блокировку на прочитанные табличные строки и на просмотренные листовые страницы индекса. Тем самым «блокируются» не только конкретные значения, но и весь прочитанный диапазон.
Число листовых страниц, покрывающих прочитанный диапазон, может измениться, например, из-за расщепления индексных страниц при вставке новых строк. Но реализация это учитывает и блокирует новые страницы:
На каждую прочитанную версию строки создается отдельная блокировка, но потенциально таких версий может быть очень много. Для предикатных блокировок используется отдельный пул, память под который выделяется при старте сервера. Общее количество предикатных блокировок ограничено произведением значений параметров max_pred_locks_per_transaction и max_connections (несмотря на названия параметров, учет по отдельным транзакциям не ведется). (по умолчанию: 64) (по умолчанию: 100)
По сути, возникает та же проблема, что и с блокировкой строк, но решается она по-другому: повышением уровня блокировок 11.
Значения, при которых повышается уровень, можно задать конфигурационными параметрами. Если количество блокировок версий строк, относящихся к одной странице, превышает max_pred_locks_per_page, такие блокировки заменяются на одну блокировку уровня страницы. (по умолчанию: 2)
Вместо трех блокировок типа tuple появилась одна типа page:
По такому же принципу повышается уровень страничных блокировок. Если число блокировок страниц, относящихся к одному отношению, превышает max_pred_locks_per_relation, такие блокировки заменяются на одну блокировку уровня отношения. (При отрицательном значении в качестве предела берется значение max_pred_locks_per_transaction, деленное на модуль значения этого параметра; таким образом, по умолчанию предел составит 32). (по умолчанию: -2) (по умолчанию: 64)
Конечно, повышение уровня блокировок неизбежно приводит к тому, что большее число транзакций напрасно завершается ошибкой сериализации. Пропускная способность системы в итоге снижается. Нужно искать баланс между расходом оперативной памяти на хранение блокировок и производительностью.
Предикатные блокировки поддерживают индексы следующих типов:
B-деревья;
хеш-индексы, GiST и GIN.
Если используется индексный доступ, а индекс не работает с предикатными блокировками, то блокировка накладывается на весь индекс целиком. Конечно, это тоже увеличивает число лишних обрывов транзакций.
Для более эффективной работы на уровне Serializable следует явно помечать только читающие транзакции как READ ONLY. Если менеджер блокировок убедится в невозможности конфликтов читающей транзакции с другими транзакциями12, он сможет освободить уже установленные для нее предикатные блокировки и не задействовать новые. А если дополнительно объявить такую транзакцию откладываемой (DEFERRABLE), это позволит избежать аномалии только читающей транзакции. (с. 71)
backend/storage/lmgr/lmgr.c, функции LockDatabaseObject и LockSharedObject. ↩︎
backend/catalog/dependency.c, функция performDeletion. ↩︎
backend/storage/lmgr/lmgr.c, функция LockRelationForExtension. ↩︎
backend/access/heap/hio.c, функция RelationAddBlocks. ↩︎
backend/access/nbtree/nbtpage.c, функция _bt_allocbuf. ↩︎
backend/storage/lmgr/lmgr.c, функция LockPage. ↩︎
postgrespro.ru/docs/postgresql/18/explicit-locking#ADVISORY-LOCKS. ↩︎
postgrespro.ru/docs/postgresql/18/functions-admin#FUNCTIONS-ADVISORY-LOCKS. ↩︎
K. P. Eswaran, J. N. Gray, R. A. Lorie, I. L. Traiger. The notions of consistency and predicate locks in a database system. ↩︎
backend/storage/lmgr/README-SSI; backend/storage/lmgr/predicate.c. ↩︎
backend/storage/lmgr/predicate.c, функция PredicateLockAcquire. ↩︎
backend/storage/lmgr/predicate.c, макрос SxactIsROSafe. ↩︎