Какие виды блокировок существуют в PostgreSQL и как их использовать для решения проблем с конкурентным доступом?
Что проверяют
Понимаете ли вы, какие блокировки PostgreSQL берёт сам, а какие вы запрашиваете явно, и умеете ли закрыть типичную гонку: два запроса одновременно списывают деньги с одного баланса. Следом спрашивают про deadlock и про то, как всё это выглядит в коде на Go.
Как отвечать
Начните с того, что благодаря MVCC обычное чтение не блокирует запись и наоборот. Блокировки нужны там, где MVCC не спасает:
- табличные: восемь режимов, от
ACCESS SHARE, который берёт любойSELECT, доACCESS EXCLUSIVEуDROP,TRUNCATEи большинстваALTER TABLE. Последний блокирует даже чтение; - строковые:
FOR UPDATE,FOR NO KEY UPDATE,FOR SHARE,FOR KEY SHARE. ОбычныйUPDATEбез изменения ключа сам берётFOR NO KEY UPDATE; - advisory:
pg_advisory_xact_lock(key)и сессионныйpg_advisory_lock. Блокировка на произвольное число, например чтобы фоновую задачу выполнял только один экземпляр сервиса.
Практические приёмы: SELECT ... FOR UPDATE перед изменением баланса, FOR UPDATE SKIP LOCKED для очереди задач, где воркеры разбирают строки, не дожидаясь друг друга, NOWAIT и lock_timeout, чтобы не висеть. Альтернатива без ожиданий: оптимистичная блокировка через колонку version и UPDATE ... WHERE id = $1 AND version = $2.
В Go это живёт внутри tx, err := pool.Begin(ctx) с defer tx.Rollback(ctx), строковая блокировка держится до Commit. Deadlock PostgreSQL обнаруживает сам и откатывает одну из транзакций с кодом 40P01: его ловят через pgconn.PgError и повторяют транзакцию. Диагностика: pg_locks, pg_stat_activity и pg_blocking_pids().
Где спотыкаются
Берут сессионный advisory lock через pgxpool без закреплённого соединения. Разблокировка уходит в другое соединение из пула и не срабатывает. За PgBouncer в режиме transaction сессионные блокировки ломаются так же, там нужен pg_advisory_xact_lock.
Запускают ALTER TABLE на живой таблице без lock_timeout. Запрос встаёт в очередь за долгой транзакцией, а все новые SELECT выстраиваются уже за ним.
Держат транзакцию открытой во время HTTP-вызова к внешнему сервису. Строки заблокированы секундами, и под нагрузкой пул соединений заканчивается.