В предыдущих модулях мы изучали, как Postgres хранит данные и как MVCC позволяет читателям и писателям не мешать друг другу на уровне версий. Но MVCC покрывает только видимость кортежей — оно ничего не говорит о том, как защитить структуру таблицы от одновременного DROP COLUMN, или как гарантировать, что две UPDATE-сессии не пишут в одну и ту же строку одновременно.
Эту задачу решают блокировки (locks). В Postgres их иерархия неожиданно сложная: восемь уровней table-level locks плюс отдельные row-level. В этом уроке разбираемся, кто что берёт и почему.
Зачем восемь уровней
В простой системе хватило бы двух блокировок: shared (для чтения) и exclusive (для записи). Но Postgres хочет позволить как можно больше параллелизма: пока один читает таблицу, другой может в неё писать, третий — строить индекс CONCURRENTLY, четвёртый — делать ANALYZE. Для этого нужно различать, что именно делает каждый игрок, чтобы корректно решать, конфликтуют ли они между собой.
Все восемь уровней (в порядке возрастания «эксклюзивности»):
Слева — самые лёгкие (читатели), справа — самые тяжёлые (DDL). Чем выше уровень, тем меньше других уровней с ним совместимы. ACCESS EXCLUSIVE блокирует всё, включая ACCESS SHARE.
Слово «share» и слово «exclusive»
Названия путают всех. Запомни мнемонику:
- SHARE в названии = блокировка разрешает другим брать SHARE на той же таблице. То есть несколько сессий могут одновременно держать
SHARElock, не мешая друг другу. - EXCLUSIVE в названии = эксклюзив на что-то. Например,
ROW EXCLUSIVE— это «эксклюзивный доступ к строкам, но другие тоже могут писать в свои строки»; имеется в виду, что обычный читатель и обычный писатель не мешают друг другу, ноACCESS EXCLUSIVE(например,DROP TABLE) — никому. - ACCESS — про сам факт обращения к таблице. = «я к таблице обращаюсь на чтение».
ACCESS SHAREACCESS EXCLUSIVE= «я к таблице обращаюсь так, что больше никто не может ни читать, ни писать».
Какая команда какой уровень берёт
Главная таблица соответствий:
Большинство OLTP-операций (SELECT, INSERT, UPDATE, DELETE) на третьем уровне или ниже и взаимно неблокирующие. DDL прыгает сразу на 8-й уровень.
Несколько важных деталей:
ALTER TABLEвсегда берётACCESS EXCLUSIVE, даже если ты просто меняешьdefaultколонки. В Postgres 11+ частьALTER TABLE(добавление колонки с дефолтом, который constant) перестало переписывать heap — но lock всё равноACCESS EXCLUSIVEна короткое время.CREATE INDEXбезCONCURRENTLYберётSHARE— это значит, никто не может писать в таблицу, пока строится индекс. На больших таблицах в проде это смерть. ПоэтомуCREATE INDEX CONCURRENTLY— почти всегда правильный выбор: он берётSHARE UPDATE EXCLUSIVEи пишущие команды (INSERT/UPDATE/DELETE) работают параллельно.VACUUM FULL≠VACUUM. Обычный —SHARE UPDATE EXCLUSIVE(4).VACUUM FULL—ACCESS EXCLUSIVE(8). Это объясняет, почемуVACUUM FULLв проде запускать страшно.
Смотрим в pg_locks
Postgres экспонирует все взятые блокировки через системный view pg_locks. Можно посмотреть, что моя сессия сейчас держит:
Что держит моя сессия прямо сейчас. ACCESS SHARE появится на customers/orders при чтении из pg_locks, плюс будут virtual-xid locks — это нормально, каждая активная транзакция держит свой virtual transaction id.
relation::regclass приводит OID таблицы к её имени (customers вместо 12345). granted = true — блокировка взята; false — мы стоим в очереди и ждём.
Теперь начнём транзакцию с UPDATE и посмотрим, что добавится:
Внутри транзакции с UPDATE берётся ROW EXCLUSIVE на таблице — это видно в pg_locks. Плюс появится transactionid lock на самой транзакции (нужен для row-level конфликтов).
Видишь ROW EXCLUSIVE на customers — это lock на таблице. Сам факт, что строка ещё не пробита другой транзакцией, защищается другим механизмом — transactionid lock на нашу собственную транзакцию. Если другая транзакция тоже хочет обновить ту же строку, она увидит xmax, посмотрит наш transactionid lock и заснёт, ожидая нашего COMMIT/ROLLBACK.
Row-level locks
Когда мы делаем UPDATE row WHERE id = 1, Postgres помечает кортеж как занятый: проставляет xmax = my_txid и взводит флаг в infomask («занят на запись»). Это и есть row-level lock. Он живёт до конца транзакции и автоматически снимается при COMMIT/ROLLBACK.
Явно row-level lock можно взять через SELECT FOR UPDATE:
SELECT FOR UPDATE берёт row-level lock на выбранные строки + table-level ROW SHARE. Покажем оба:
Заметь — SELECT FOR UPDATE берёт ROW SHARE на таблице (level 2), не ROW EXCLUSIVE. Это потому что table-level lock защищает только от DDL, а сам факт «я занял строки на запись» отслеживается через xmax в tuple header + tuple-level lock в специальной in-memory структуре. Постгрес не держит N row-level locks в памяти — они хранятся в самих кортежах через xmax. Это экономит память при больших SELECT FOR UPDATE (миллион строк = миллион записей в кортежи, а не миллион записей в lock manager).
Lifecycle: когда блокировки снимаются
Все table-level locks (кроме самых лёгких в некоторых случаях) живут до конца транзакции. Это значит:
BEGIN; UPDATE ...; <долгая работа>; COMMIT;—ROW EXCLUSIVEдержится всё это время. На больших OLAP-инсёртах это критично.BEGIN; SELECT ...; <много времени>; COMMIT;—ACCESS SHAREтоже держится. Это значит, что простойSELECTв долгой транзакции блокирует любойALTER TABLEв эту таблицу.
Это объясняет классический шок прод-DBA: «миграция не проходит, висит на ACCESS EXCLUSIVE». Почти всегда виноват какой-нибудь забытый BEGIN в открытой psql-сессии, держащий ACCESS SHARE на таблице.
Single-statement автокоммит (без явного BEGIN) — это та же транзакция, но в одно statement: lock берётся, statement выполняется, COMMIT, lock снимается. Без BEGIN/COMMIT — окно держания минимально.
Чек-лист
- В Postgres 8 уровней table-level locks: от
ACCESS SHARE(1) доACCESS EXCLUSIVE(8). SELECTберётACCESS SHARE.INSERT/UPDATE/DELETE—ROW EXCLUSIVE.SELECT FOR UPDATE—ROW SHARE.- DDL (
ALTER TABLE,DROP TABLE,TRUNCATE,VACUUM FULL,REINDEX) берётACCESS EXCLUSIVE(8). Это всё блокирует. CREATE INDEXбезCONCURRENTLY—SHARE(5), блокирует писателей. СCONCURRENTLY—SHARE UPDATE EXCLUSIVE(4), пишущие не блокируются.- Row-level locks не хранятся в lock manager в памяти — они живут в
xmaxкортежа. Это позволяет дёшево лочить миллионы строк. - Все table-level locks снимаются только при COMMIT/ROLLBACK. Долгая транзакция с
SELECTблокирует миграции. pg_locks— твой главный диагностический view. Смотриrelation,mode,granted,pid.