MVCC не отменяет локи писателей. FOR UPDATE берёт row lock без изменения данных. При тупике PostgreSQL прерывает одну транзакцию. Не держите лок, пока ходите в сеть.
PostgreSQL postgres собеседование
Читатели snapshot не блокируют писателей: в этом смысл MVCC. Два UPDATE одной строки — уже другая история. Второй ждёт commit или rollback первого. Это row exclusive lock (исключительная блокировка строки). Он живёт до конца транзакции, не до конца statement.
Явный SELECT … FOR UPDATE берёт тот же класс лока, ещё не меняя данные: «эта строка сейчас моя, я собираюсь её обновить». Так сериализуют «прочитал баланс — сейчас спишу», чтобы сосед не вклинился между SELECT и UPDATE. Это не уровень изоляции и не замена UNIQUE.
Пример. Баланс 100. Два запроса почти одновременно делают SELECT, потом UPDATE. Без FOR UPDATE оба могут увидеть 100 и оба списать. С FOR UPDATE второй ждёт, пока первый закоммитит, и уже видит новый баланс.
В воркерах очереди задач локи становятся паттерном конкурентного потребления: несколько процессов выбирают «свободные» работы и не хотят ждать чужой commit. Для этого есть SKIP LOCKED.
Виды row lock
FOR UPDATE— исключительный лок строки, как у UPDATE.FOR SHARE/FOR KEY SHARE— разделяемый; мешает UPDATE/DELETE, но несколько читателей могут держать его вместе. Нужен реже, в основном вокруг FK.SKIP LOCKED— не ждать занятые строки, взять только свободные. Паттерн воркера.NOWAIT— если строка занята, сразу ошибка, без очереди.
Лок живёт до COMMIT или ROLLBACK. Поэтому BEGIN, затем HTTP, затем COMMIT — это держать чужие UPDATE в очереди на время сети. Отсюда таймауты, deadlock и bloat одновременно.
Очередь задач и SKIP LOCKED
Несколько воркеров делают SELECT … FROM jobs WHERE status = 'new' FOR UPDATE SKIP LOCKED LIMIT n. Кто первый залочил строку — тот её обрабатывает. Остальные не стоят в очереди на эти id, а берут следующие свободные. Без SKIP LOCKED второй воркер спит на первой же занятой строке, даже если в таблице полно работы.
BEGIN;
SELECT id, payload
FROM jobs
WHERE status = 'new'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 10;
-- обработка в той же транзакции или смена status, затем COMMIT
UPDATE jobs SET status = 'taken' WHERE id = ANY($1);
COMMIT;
Паттерн воркера: взять пачку свободных задач, не блокируясь на строках, которые уже обрабатывает сосед.
Deadlock
Тупик: T1 лочит строку A, T2 лочит B, затем T1 хочет B, T2 хочет A. Детектор PostgreSQL прерывает одну транзакцию ошибкой deadlock detected. Лечение — стабильный порядок локов (всегда меньший id первым), короткие транзакции, не лочить родителя и детей в разном порядке из разных кусков кода.
Пример. Заказ 10 и заказ 20. Сервис A лочит 10, потом 20. Сервис B лочит 20, потом 10. Оба ждут друг друга. Postgres убивает одну транзакцию. Клиент должен уметь повторить запрос.
Table lock от DDL, LOCK TABLE или миграции с ACCESS EXCLUSIVE блокирует всех. Advisory locks — ручные, их легко взять и забыть снять, особенно при panic без defer. Их не видно в «обычной» модели строки, пока не посмотрите pg_locks.
Типичная ошибка
Путать lock и MVCC snapshot. Repeatable Read без FOR UPDATE не запрещает соседу сделать UPDATE: вы просто не увидите его результат в своём снимке, а сосед строку изменит. FOR UPDATE на Read Committed не защищает от фантома: двух INSERT «единственного» слота, которых ещё нет в таблице.
На собеседовании
SKIP LOCKED — паттерн воркера, его стоит назвать вслух. Лок живёт до конца транзакции, поэтому BEGIN + HTTP + COMMIT — путь и к deadlock, и к bloat. При deadlock Postgres убивает одну транзакцию, клиент должен быть готов к ошибке и ретраю.