Базы данных
Блокировки и дедлоки
2012 год. Knight Capital Group. Новый торговый алгоритм запустили на 8 серверах, но на 7-м осталась старая версия. За 45 минут система провела миллионы некорректных сделок. Потеряли USD 440 миллионов. Одна из причин - конкурентные обновления без правильной блокировки привели к inconsistent state. Блокировки - это не академика. Это USD 440M за 45 минут.
- Stripe использует advisory locks поверх PostgreSQL для идемпотентных платёжных операций: одна транзакция на один idempotency key в любой момент времени
- GitHub хранит pull requests в PostgreSQL, advisory locks предотвращают race condition при concurrent merge - без distributed lock manager
- Shopify обнаружила deadlock pattern в Black Friday 2019: одновременные discount применения конфликтовали. Решение: ORDER BY id для детерминированного порядка блокировок
Типы блокировок: shared vs exclusive
Блокировки - механизм координации concurrent-транзакций. Без блокировок: две транзакции читают счёт (USD 1000), обе вычитают USD 500, обе записывают USD 500. Итог: USD 500 вместо USD 0. Это Lost Update - один из классических concurrency аномалий.
Shared Lock (S-lock, читающая): несколько транзакций могут держать S-lock на одной записи одновременно. Никто не изменяет, все читают - конфликта нет. Exclusive Lock (X-lock, пишущая): одна транзакция держит X-lock, никакие другие не могут ни читать (S), ни писать (X). Матрица совместимости:
Intent locks - оптимизация для иерархического locking. Без intent locks: чтобы поставить X-lock на строку, нужно сканировать все блокировки таблицы. С intent locks: IX-lock на таблице сигнализирует 'внутри есть X-locks', проверка O(1). PostgreSQL реализует это через lwlocks (lightweight locks) внутри shared memory.
Two-Phase Locking (2PL) - классический протокол: фаза расширения (acquire locks), фаза сжатия (release locks). После первого release нельзя acquire новые. Строгий 2PL: все locks до конца транзакции. Это гарантирует serializability, но максимизирует contention.
Могут ли две транзакции держать S-lock на одной строке одновременно?
Row locks и table locks: гранулярность
PostgreSQL поддерживает восемь режимов table lock. Самые важные: ACCESS SHARE (SELECT), ROW EXCLUSIVE (INSERT/UPDATE/DELETE), SHARE UPDATE EXCLUSIVE (VACUUM, CREATE INDEX CONCURRENTLY), ACCESS EXCLUSIVE (ALTER TABLE, DROP TABLE). ALTER TABLE блокирует всё - даже чтение.
SKIP LOCKED - стандартный паттерн для concurrent job queues. Без него: N воркеров SELECT FOR UPDATE одной строки -> N-1 ждут пока первый обработает. С SKIP LOCKED: каждый воркер берёт следующую доступную задачу. Это то, как BullMQ с PostgreSQL backend реализует очереди без Redis.
Row-level locking в InnoDB (MySQL): locks хранятся в lock table в памяти, а не в строке. Gap locks - блокировка диапазона, которого нет (prevents phantom reads при REPEATABLE READ). Next-key locks = row lock + gap lock. Это источник неожиданных deadlocks в MySQL.
Lock escalation - автоматическое повышение гранулярности. SQL Server: если транзакция держит >5000 row locks, SQL Server может escalate до table lock. Это снижает overhead lock manager, но увеличивает contention. PostgreSQL не делает lock escalation автоматически.
Для чего используется SKIP LOCKED в SELECT FOR UPDATE?
Advisory Locks: locks для бизнес-логики
Advisory Locks - блокировки, управляемые приложением, а не автоматически БД. Используются для координации за пределами транзакций: distributed mutex, ensure-only-one-instance, сериализация по бизнес-ключу.
hashtext() конвертирует строковый ключ в integer для advisory lock. Проблема: коллизии. hashtext('user:1') может совпасть с hashtext('order:3827'). Для production: использовать bigint, составной из двух int32 (намекает на пространство имён). Функция `(class, object)` версия: `pg_advisory_lock(1, 42)` - первый int namespace.
Advisory locks в PostgreSQL хранятся в shared memory lock table. Лимит: max_locks_per_transaction * (max_connections + max_prepared_transactions). По умолчанию ~6400 одновременных locks. При высоком параллелизме может исчерпаться. Мониторинг: pg_locks WHERE locktype = 'advisory'.
Чем advisory locks отличаются от row locks?
Deadlock: порочный круг ожидания
Deadlock: транзакция A держит lock на row 1, ждёт row 2. Транзакция B держит lock на row 2, ждёт row 1. Оба ждут вечно. PostgreSQL обнаруживает deadlock через wait-for graph: строит граф 'кто кого ждёт', ищет цикл. По умолчанию проверяет каждые deadlock_timeout = 1 секунду.
deadlock_timeout в PostgreSQL = 1 секунда по умолчанию. За это время транзакция сначала ждёт lock_timeout (бесконечно по умолчанию). Только после deadlock_timeout запускается детектор. Для OLTP: уменьши deadlock_timeout до 100ms, установи lock_timeout = 5s. Для batch jobs: deadlock_timeout можно оставить 1s.
Какой паттерн лучше всего предотвращает deadlock?
Мониторинг блокировок в production
Lock wait в production - тихий убийца производительности. Запрос висит, connection занят, connection pool исчерпывается, приложение деградирует. Без мониторинга lock waits обнаруживаются когда p99 latency уже в 10x.
Prometheus + postgres_exporter экспортирует pg_locks count в метрики. Alert: `pg_locks{mode='ExclusiveLock',granted='false'} > 10` - больше 10 ожидающих X-lock. Это сигнал проблемы до того, как пользователи почувствуют. Grafana дашборд с lock waits, deadlock rate, avg lock wait time - стандарт production мониторинга.
auto_explain с log_min_duration_statement: логирует slow queries. Но lock wait не виден в EXPLAIN - сама query быстрая, просто ждёт. Нужен pg_stat_activity + track_activity_query_size увеличенный до 4096. CloudWatch Insights или Datadog для анализа lock patterns в исторических логах.
Высокий уровень изоляции (SERIALIZABLE) автоматически предотвращает deadlocks
SERIALIZABLE может увеличить частоту deadlocks, так как больше конфликтов приводит к serialization failures
При SERIALIZABLE PostgreSQL использует Serializable Snapshot Isolation (SSI) с predicate locks. Когда SSI обнаруживает потенциальный конфликт - одна из транзакций откатывается с serialization failure. Это семантически схоже с deadlock но происходит чаще. READ COMMITTED + явные SELECT FOR UPDATE + правильный порядок - часто лучший выбор для OLTP
Какая система таблица PostgreSQL показывает текущие ожидающие блокировки?
Связанные темы
Блокировки связаны с транзакционностью, изоляцией и distributed coordination:
- Транзакции и ACID — Isolation уровни реализуются через locks
- MVCC — MVCC уменьшает lock contention для reads
- Распределённые транзакции — 2PC требует distributed lock management
Ключевые идеи
- S-locks совместимы, X-lock требует эксклюзии. Intent locks для иерархического locking
- SELECT FOR UPDATE SKIP LOCKED - паттерн для concurrent job queue без внешнего брокера
- Advisory locks для бизнес-координации вне транзакций: idempotency, distributed mutex
- Deadlock: всегда блокировать в одном порядке. Мониторинг: pg_locks + pg_stat_activity
Вопросы для размышления
- Когда advisory lock лучше row lock для бизнес-сериализации, и когда наоборот?
- Как влияет deadlock_timeout на производительность и почему его не стоит делать слишком маленьким?
- Почему SKIP LOCKED лучше FOR UPDATE для job queue, даже если это означает возможный голод некоторых задач?
Связанные уроки
- db-13-transactions — Транзакции - контекст для блокировок
- db-14-mvcc — MVCC уменьшает потребность в блокировках
- db-16-distributed-tx — Распределённые блокировки - следующий уровень
- db-03-acid — ACID изоляция реализуется через блокировки
- os-04-scheduling
- os-05-sync