Базы данных

Блокировки и дедлоки

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
Блокировки и дедлоки

0

1

Войти