Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Базы данных · Уровень: Senior ## TL;DR - **Deadlock** — циклическое ожидание блокировок: транзакция A держит ресурс 1 и ждёт ресурс 2, транзакция B держит ресурс 2 и ждёт ресурс 1. Никто не может продвинуться. - PostgreSQL **автоматически обнаруживает** deadlock через `deadlock detector`, который запускается, если транзакция ждёт блокировку дольше `deadlock_timeout` (по умолчанию **1 секунда**). - Детектор выбирает одну транзакцию-**жертву** (victim), откатывает её и возвращает ошибку **SQLSTATE `40P01`** (`deadlock_detected`). Остальные продолжают работу. - Главная причина — **разный порядок захвата блокировок** в разных транзакциях. Главная профилактика — **единый порядок** (например, всегда блокировать строки `ORDER BY id`). - Инструменты: единый порядок блокировок, короткие транзакции, `SELECT ... FOR UPDATE` с `ORDER BY`, `lock_timeout`, `NOWAIT`, `SKIP LOCKED`. - Диагностика: `pg_locks` (кто что держит и кто чего ждёт), `pg_stat_activity` (что выполняет бэкенд, `wait_event_type = 'Lock'`), а также сообщение в логе PostgreSQL с деталями цикла. - В приложении на Go deadlock — **ожидаемая ситуация при конкуренции**; правильная реакция — детектировать `40P01` и **повторить транзакцию** (retry с backoff), а не падать. ## Теория ### Что такое deadlock и почему он возникает Deadlock (взаимоблокировка) — ситуация, когда две или более транзакций образуют **цикл ожидания**: каждая держит блокировку, которая нужна другой, и ни одна не может завершиться. Это фундаментальное свойство любой системы с блокировками, а не баг PostgreSQL. Классический сценарий — **разный порядок захвата**: ``` T1: UPDATE accounts SET ... WHERE id = 1; -- захватил ROW EXCLUSIVE на строке 1 T2: UPDATE accounts SET ... WHERE id = 2; -- захватил блокировку на строке 2 T1: UPDATE accounts SET ... WHERE id = 2; -- ждёт строку 2 (её держит T2) T2: UPDATE accounts SET ... WHERE id = 1; -- ждёт строку 1 (её держит T1) -> цикл ``` Образовался цикл `T1 -> T2 -> T1`. Один из них будет убит детектором. Важно: deadlock — это **не таймаут**. Это именно обнаруженный цикл. Бесконечное ожидание одной блокировки без цикла (lock contention) — это не deadlock, и детектор его не тронет (его убивает только `lock_timeout`, если задан). ### Типичные причины | Причина | Описание | |---|---| | Разный порядок захвата строк | Самая частая. Две транзакции трогают одни и те же строки в обратном порядке. | | Эскалация SHARE -> EXCLUSIVE | Две транзакции читают строку с `FOR SHARE`/`FOR KEY SHARE`, затем обе пытаются обновить. Каждая ждёт, пока другая отпустит share-lock. | | Внешние ключи (FK) | `INSERT`/`UPDATE` дочерней строки берёт `FOR KEY SHARE` на родителе. При конкурентных вставках в обратном порядке может возникнуть deadlock. | | Индексы / unique constraints | Конкурентные `INSERT` с одинаковым уникальным ключом ждут друг друга на B-tree странице. | | Триггеры / каскады | Скрытые блокировки внутри триггеров меняют фактический порядок захвата. | | Долгие транзакции | Чем дольше держится блокировка, тем выше шанс пересечения с другой транзакцией. | ### Обнаружение deadlock PostgreSQL не проверяет циклы постоянно — это дорого. Вместо этого: 1. Транзакция запрашивает блокировку, которая занята, и встаёт в очередь ожидания. 2. Заводится таймер на `deadlock_timeout` (по умолчанию `1s`). 3. Если за это время блокировка не получена, **запускается deadlock detector** — он строит граф ожидания (wait-for graph) и ищет цикл. 4. Если цикла нет — транзакция продолжает ждать (обычное contention). 5. Если цикл есть — выбирается жертва, её транзакция откатывается с ошибкой `40P01`. ```sql -- Текущее значение SHOW deadlock_timeout; -- 1s по умолчанию -- Не делайте его слишком маленьким: на загруженной БД детектор -- будет запускаться слишком часто и жечь CPU. 1s — разумный дефолт. ``` **Логика выбора жертвы.** PostgreSQL не гарантирует строгих правил, но на практике жертвой становится транзакция, **отмена которой разрывает цикл** (та, чей запрос как раз обнаружил deadlock — то есть «последняя» в цикле инициировавшая проверку). Полагаться на конкретную транзакцию как на жертву нельзя — приложение должно быть готово к откату любой из них. Сообщение в логе PostgreSQL даёт полную картину: ``` ERROR: deadlock detected DETAIL: Process 1234 waits for ShareLock on transaction 5678; blocked by process 5679. Process 5679 waits for ShareLock on transaction 5680; blocked by process 1234. Process 1234: UPDATE accounts SET balance = balance - 100 WHERE id = 2; Process 5679: UPDATE accounts SET balance = balance - 100 WHERE id = 1; HINT: See server log for query details. CONTEXT: while updating tuple (0,5) in relation "accounts" ``` Это лучший источник для разбора: видны оба процесса, оба запроса и конкретные tuple. ### Блокировки на уровне таблицы (table-level lock modes) PostgreSQL имеет 8 режимов блокировок таблицы. Конфликты определяются матрицей совместимости. Большинство команд берут их автоматически. | Режим | Кто берёт (типично) | Конфликтует с | |---|---|---| | `ACCESS SHARE` | `SELECT` | только с `ACCESS EXCLUSIVE` | | `ROW SHARE` | `SELECT ... FOR UPDATE/SHARE` | `EXCLUSIVE`, `ACCESS EXCLUSIVE` | | `ROW EXCLUSIVE` | `INSERT`, `UPDATE`, `DELETE` | `SHARE`, `SHARE ROW EXCLUSIVE`, `EXCLUSIVE`, `ACCESS EXCLUSIVE` | | `SHARE UPDATE EXCLUSIVE` | `VACUUM` (не FULL), `ANALYZE`, `CREATE INDEX CONCURRENTLY` | сам с собой и более сильными | | `SHARE` | `CREATE INDEX` (не CONCURRENTLY) | `ROW EXCLUSIVE` и сильнее | | `SHARE ROW EXCLUSIVE` | `CREATE TRIGGER`, некоторые `ALTER TABLE` | почти со всеми | | `EXCLUSIVE` | `REFRESH MATERIALIZED VIEW CONCURRENTLY` | всё, кроме `ACCESS SHARE` | | `ACCESS EXCLUSIVE` | `DROP TABLE`, `TRUNCATE`, `VACUUM FULL`, большинство `ALTER TABLE`, `LOCK TABLE` по умолчанию | со **всеми**, включая `SELECT` | Ключевые наблюдения для senior: - Обычные `SELECT` и `INSERT/UPDATE/DELETE` **не конфликтуют между собой** на уровне таблицы (`ACCESS SHARE` и `ROW EXCLUSIVE` совместимы). Конкуренция за данные идёт на уровне строк. - `ACCESS EXCLUSIVE` блокирует абсолютно всё, включая чтения. Поэтому миграции (`ALTER TABLE`) на проде опасны — длинная миграция держит таблицу, и за ней выстраивается очередь даже из `SELECT`. Отсюда требование `lock_timeout` для миграций. ```sql -- Явная блокировка таблицы (редко нужна; режим по умолчанию — ACCESS EXCLUSIVE) LOCK TABLE accounts IN SHARE ROW EXCLUSIVE MODE; ``` ### Блокировки на уровне строк (row-level locks) Запрашиваются через `SELECT ... FOR ...`. Матрица их конфликтов: | Режим | Назначение | Конфликтует с | |---|---|---| | `FOR KEY SHARE` | слабейшая; берётся FK при ссылке на строку | только с `FOR UPDATE` | | `FOR SHARE` | разделяемая блокировка строки | `FOR UPDATE`, `FOR NO KEY UPDATE` | | `FOR NO KEY UPDATE` | как `FOR UPDATE`, но не трогает ключевые столбцы; берётся обычным `UPDATE` без изменения PK/unique | `FOR UPDATE`, `FOR NO KEY UPDATE`, `FOR SHARE` | | `FOR UPDATE` | сильнейшая; полная блокировка строки на изменение/удаление | со **всеми** четырьмя | Совместимость (X — конфликт): | | KEY SHARE | SHARE | NO KEY UPDATE | UPDATE | |---|---|---|---|---| | **KEY SHARE** | | | | X | | **SHARE** | | | X | X | | **NO KEY UPDATE** | | X | X | X | | **UPDATE** | X | X | X | X | Почему это важно: разделение `FOR UPDATE` и `FOR NO KEY UPDATE` (появилось в 9.3) уменьшает конфликты с FK. Раньше любой `UPDATE` блокировал вставки в дочерние таблицы. Теперь обычный `UPDATE` берёт `FOR NO KEY UPDATE`, а FK-проверка — `FOR KEY SHARE`, и они **совместимы** — меньше deadlock'ов на FK. Важная семантика блокировок строк: даже при обычном `UPDATE` row-lock держится **до конца транзакции** и виден в `pg_locks` как `tuple` lock + xid-блокировка через `pg_xact`. ### SELECT ... FOR UPDATE подробно `SELECT ... FOR UPDATE` блокирует **выбранные строки** так, будто их собираются обновить. Другие транзакции, пытающиеся `UPDATE`/`DELETE`/`SELECT FOR UPDATE` эти же строки, будут ждать до конца текущей транзакции (или коммита/роллбэка). ```sql BEGIN; -- Заблокировали строку; другие FOR UPDATE/UPDATE по этой строке ждут SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- ... читаем, считаем, валидируем ... UPDATE accounts SET balance = balance - 100 WHERE id = 1; COMMIT; ``` Что блокирует `FOR UPDATE`: - Блокирует **строки**, а не таблицу (на уровне таблицы берёт лишь `ROW SHARE`). - Блокирует только строки, реально вернувшиеся запросом (после применения `WHERE`, `JOIN`). - Не блокирует чистые `SELECT` (читатели без `FOR ...` видят старую версию через MVCC). **`NOWAIT`** — не ждать, если строка уже заблокирована: сразу ошибка `55P03` (`lock_not_available`). ```sql SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT; -- ERROR: could not obtain lock on row in relation "accounts" (SQLSTATE 55P03) ``` **`SKIP LOCKED`** — пропустить уже заблокированные строки. Идеально для **очередей задач**: каждый воркер забирает свободную задачу, не конкурируя за одну и ту же. ```sql -- Паттерн очереди: атомарно взять одну свободную задачу WITH job AS ( SELECT id FROM jobs WHERE status = 'pending' ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1 ) UPDATE jobs SET status = 'processing', picked_at = now() FROM job WHERE jobs.id = job.id RETURNING jobs.*; ``` `SKIP LOCKED` практически исключает deadlock в очередях, т.к. воркеры не ждут друг друга. Платой является то, что результат **не детерминирован** (не видишь пропущенные строки), но для очередей это и нужно. ### Advisory locks (рекомендательные блокировки) Это блокировки «по соглашению» — PostgreSQL не привязывает их к строкам/таблицам, смысл задаёт приложение. Полезны для распределённой координации (например, гарантировать, что только один процесс выполняет джоб). ```sql -- На уровне сессии (нужно явно отпускать) SELECT pg_advisory_lock(42); -- блокирует, ждёт SELECT pg_try_advisory_lock(42); -- не ждёт, возвращает bool SELECT pg_advisory_unlock(42); -- На уровне транзакции (отпускается автоматически на COMMIT/ROLLBACK) SELECT pg_advisory_xact_lock(42); SELECT pg_try_advisory_xact_lock(42); ``` Особенности: - Ключ — это `bigint` или пара `(int, int)`. Часто используют `hashtext('my-key')`. - Session-level advisory lock **не освобождается на коммите** — легко получить утечку при использовании пула соединений (соединение вернулось в пул с висящей блокировкой). На пулах предпочитайте `pg_advisory_xact_lock`. - Advisory locks **тоже участвуют в deadlock detector** — два процесса, берущие два advisory-ключа в разном порядке, получат `40P01`. ### Профилактика deadlock **1. Единый порядок захвата.** Самое мощное средство. Если все транзакции блокируют строки в одном порядке (например, по возрастанию `id`), цикл невозможен. ```sql -- Перевод между счетами: всегда блокируем по возрастанию id BEGIN; SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id -- критично: фиксирует порядок захвата FOR UPDATE; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT; ``` `ORDER BY` в `SELECT ... FOR UPDATE` гарантирует порядок блокировки строк, и любой другой перевод (даже 2 -> 1) возьмёт те же строки в том же порядке. **2. Короткие транзакции.** Не держите блокировки во время сетевых вызовов, ожиданий пользователя, тяжёлых вычислений. Сначала посчитайте, потом — короткая транзакция на запись. **3. `lock_timeout`.** Ограничивает, сколько транзакция ждёт *любую* блокировку. Не предотвращает deadlock, но не даёт зависнуть надолго и превращает зависание в управляемую ошибку (`55P03`). ```sql SET lock_timeout = '2s'; -- на сессию/транзакцию -- особенно важно для миграций ALTER TABLE ``` **4. `NOWAIT` / `SKIP LOCKED`** там, где можно не ждать (очереди, опциональные блокировки). **5. Снижать уровень изоляции там, где не нужен `SERIALIZABLE`.** На `SERIALIZABLE` чаще ловятся `40001` (serialization_failure) — формально не deadlock, но обрабатывается тем же retry-механизмом. **6. Retry в приложении.** Deadlock при конкуренции неизбежен; правильный паттерн — поймать `40P01` и повторить транзакцию. ### Диагностика: pg_locks и pg_stat_activity `pg_locks` показывает все текущие блокировки и ожидания (`granted = false` означает «ждёт»). ```sql -- Кто кого блокирует (классический запрос «blocking tree») SELECT blocked.pid AS blocked_pid, blocked_act.query AS blocked_query, blocking.pid AS blocking_pid, blocking_act.query AS blocking_query, blocked_act.wait_event_type, blocked_act.wait_event FROM pg_locks blocked JOIN pg_stat_activity blocked_act ON blocked.pid = blocked_act.pid JOIN pg_locks blocking ON blocking.locktype = blocked.locktype AND blocking.database IS NOT DISTINCT FROM blocked.database AND blocking.relation IS NOT DISTINCT FROM blocked.relation AND blocking.transactionid IS NOT DISTINCT FROM blocked.transactionid AND blocking.pid <> blocked.pid AND blocking.granted JOIN pg_stat_activity blocking_act ON blocking.pid = blocking_act.pid WHERE NOT blocked.granted; ``` Начиная с PG 9.6 есть удобная функция: ```sql -- Возвращает PID'ы, которые блокируют данный PID SELECT pg_blocking_pids(1234); ``` `pg_stat_activity` показывает, чем заняты бэкенды: ```sql SELECT pid, state, wait_event_type, wait_event, now() - xact_start AS xact_age, query FROM pg_stat_activity WHERE wait_event_type = 'Lock' -- висят на блокировке OR state = 'idle in transaction' -- опасное состояние: держит блокировки, ничего не делает ORDER BY xact_age DESC; ``` `idle in transaction` — частый виновник: транзакция открыта, держит блокировки, но приложение не шлёт ни запроса, ни коммита. Ограничивайте `idle_in_transaction_session_timeout`. ### Полный пример deadlock на двух транзакциях Воспроизведение в двух сессиях `psql`: ```sql -- Подготовка CREATE TABLE accounts (id int PRIMARY KEY, balance numeric); INSERT INTO accounts VALUES (1, 1000), (2, 1000); ``` ```sql -- Сессия A -- Сессия B BEGIN; BEGIN; UPDATE accounts UPDATE accounts SET balance = balance - 100 SET balance = balance - 100 WHERE id = 1; WHERE id = 2; -- A держит строку 1 -- B держит строку 2 UPDATE accounts -- (B ещё не выполнял вторую команду) SET balance = balance + 100 WHERE id = 2; -- A ЖДЁТ строку 2 (её держит B) UPDATE accounts SET balance = balance + 100 WHERE id = 1; -- B ждёт строку 1 -> ЦИКЛ -- через ~deadlock_timeout детектор находит цикл и убивает одну из транзакций: -- ERROR: deadlock detected (SQLSTATE 40P01) ``` Та сессия, что стала жертвой, получит `40P01` и должна повторить весь блок. Другая успешно завершит и закоммитит. **Исправление** — единый порядок: ```sql -- ОБЕ сессии блокируют сначала меньший id, потом больший SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE; -- дальше любые UPDATE безопасны — цикла быть не может ``` ### Обработка deadlock в Go Ключевой приём — распознать `40P01` и сделать retry с экспоненциальным backoff и джиттером. С `pgx` (через `pgconn.PgError`): ```go package db import ( "context" "errors" "math/rand" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" ) // isRetryable: deadlock (40P01) и serialization_failure (40001) — безопасно повторять. func isRetryable(err error) bool { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { return pgErr.Code == "40P01" || pgErr.Code == "40001" } return false } // WithRetry выполняет fn в транзакции, повторяя при deadlock/serialization failure. func WithRetry(ctx context.Context, pool *pgx.Conn, fn func(pgx.Tx) error) error { const maxAttempts = 5 var lastErr error for attempt := 0; attempt < maxAttempts; attempt++ { tx, err := pool.Begin(ctx) if err != nil { return err } err = fn(tx) if err != nil { _ = tx.Rollback(ctx) // важно: откатить перед повтором if isRetryable(err) { lastErr = err // backoff с джиттером, чтобы транзакции «разъехались» по времени backoff := time.Duration(1< hi { lo, hi = hi, lo } tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) if err != nil { return err } defer tx.Rollback() //nolint: safe no-op after commit // Единый порядок захвата -> deadlock невозможен if _, err = tx.ExecContext(ctx, `SELECT 1 FROM accounts WHERE id IN ($1, $2) ORDER BY id FOR UPDATE`, lo, hi); err != nil { return err } if _, err = tx.ExecContext(ctx, `UPDATE accounts SET balance = balance - $1 WHERE id = $2`, amount, from); err != nil { return err } if _, err = tx.ExecContext(ctx, `UPDATE accounts SET balance = balance + $1 WHERE id = $2`, amount, to); err != nil { return err } return tx.Commit() } ``` ## Подводные камни / gotchas - **Deadlock — не баг, а штатная ситуация.** Если в системе есть конкуренция за одни ресурсы, retry обязателен. Падение приложения на `40P01` — ошибка проектирования. - **`SELECT FOR UPDATE` без `ORDER BY` сам по себе НЕ спасает от deadlock.** Порядок блокировки строк не определён без явного `ORDER BY` (особенно при `IN (...)` или join). Нужен именно детерминированный порядок. - **`deadlock_timeout != lock_timeout`.** Первый — задержка перед запуском детектора (защита от цикла), второй — максимум ожидания любой блокировки (защита от долгого зависания). Это разные настройки с разными целями. - **Жертву нельзя предсказать.** Не пишите логику в расчёте на то, что убьют «другую» транзакцию. - **Deadlock на FK незаметен.** Конкурентные `INSERT` в дочернюю таблицу берут `FOR KEY SHARE` на родителе; при определённых паттернах это даёт deadlock, хотя в коде нет явных `FOR UPDATE`. - **Уникальные индексы создают deadlock при конкурентных вставках.** Два `INSERT ... ON CONFLICT` с пересекающимися ключами в разном порядке могут зайти в цикл. - **Session-level advisory locks текут на пулах соединений.** Соединение возвращается в пул с непогашенной блокировкой. Используйте `pg_advisory_xact_lock` или явно отпускайте. - **`idle in transaction` держит блокировки.** Долго открытая транзакция = бомба замедленного действия для блокировок и для VACUUM (раздувание). Ставьте `idle_in_transaction_session_timeout`. - **`SKIP LOCKED` молча пропускает строки.** Не используйте его там, где нужна полнота выборки — только для очередей/распределения работы. - **Retry без backoff усугубляет проблему.** Повтор сразу после deadlock приводит к повторному столкновению тех же транзакций. Нужен джиттер. - **На `SERIALIZABLE` deadlock может прийти как `40001`, в том числе на `COMMIT`.** Retry-логика должна ловить и его, и проверять ошибку коммита тоже. - **`ALTER TABLE` берёт `ACCESS EXCLUSIVE` и блокирует даже `SELECT`.** Длинная миграция выстраивает очередь и сама может попасть в deadlock с рабочими запросами. Всегда `SET lock_timeout` для DDL. ## Вопросы на собеседовании **В:** Что такое deadlock в PostgreSQL и чем он отличается от обычного ожидания блокировки (lock contention)? **О:** Deadlock — это циклическое ожидание: набор транзакций, где каждая держит блокировку, нужную другой, образуя цикл, который не разрешится сам. Обычное contention — это ожидание одной блокировки без цикла; оно разрешится, как только держатель закоммитит. PostgreSQL автоматически обнаруживает только циклы (через deadlock detector) и убивает одну транзакцию с `40P01`. Простое ожидание не убивается (если не задан `lock_timeout`). **В:** Как PostgreSQL обнаруживает deadlock и кто становится жертвой? **О:** Когда транзакция ждёт блокировку дольше `deadlock_timeout` (по умолчанию 1с), запускается deadlock detector. Он строит wait-for граф и ищет цикл. Если цикл найден — выбирается жертва (обычно та транзакция, чья проверка обнаружила цикл, то есть отмена которой разрывает его), она откатывается с `SQLSTATE 40P01`. Какая именно транзакция станет жертвой — не гарантируется, приложение должно быть готово к откату любой. **В:** Чем `deadlock_timeout` отличается от `lock_timeout`? **О:** `deadlock_timeout` — задержка перед запуском детектора циклов; он не отменяет транзакцию по таймауту, а лишь решает, когда проверять граф. `lock_timeout` — максимальное время ожидания любой блокировки, по истечении которого транзакция падает с `55P03` (`lock_not_available`). Первый защищает от вечного цикла, второй — от долгого ожидания вообще. **В:** Как предотвратить deadlock при переводе денег между двумя счетами? **О:** Захватывать строки в едином, детерминированном порядке — например `SELECT ... WHERE id IN (a,b) ORDER BY id FOR UPDATE`. Тогда любой перевод блокирует строки в одном порядке (по возрастанию id), и цикл невозможен. Дополнительно: короткие транзакции, retry на `40P01`, нормализация порядка id в коде. **В:** Что именно блокирует `SELECT ... FOR UPDATE` и зачем нужны `NOWAIT` и `SKIP LOCKED`? **О:** Блокирует выбранные запросом строки (а не таблицу — на таблице берёт лишь `ROW SHARE`), так что конкурентные `UPDATE`/`DELETE`/`FOR UPDATE` по этим строкам ждут до конца транзакции. Чистые `SELECT` не блокируются (MVCC). `NOWAIT` — не ждать, сразу ошибка `55P03`, если строка занята. `SKIP LOCKED` — пропустить занятые строки; используется для очередей задач, где воркеры разбирают разные строки без конкуренции. **В:** В чём разница между `FOR UPDATE` и `FOR NO KEY UPDATE`? **О:** `FOR NO KEY UPDATE` — более слабая блокировка, которую берёт обычный `UPDATE`, не меняющий ключевые столбцы (PK/unique). Она совместима с `FOR KEY SHARE`, который берут проверки внешних ключей. Это позволяет конкурентно обновлять родительскую строку и вставлять дочерние без блокировки друг друга, что снижает количество deadlock'ов на FK. `FOR UPDATE` — сильнейшая, конфликтует со всеми режимами row-level блокировок. **В:** Как продиагностировать, кто кого блокирует прямо сейчас? **О:** Через `pg_locks` (`granted = false` = ждёт) в join с `pg_stat_activity`, чтобы увидеть запросы обеих сторон; либо проще — `pg_blocking_pids(pid)` (PG 9.6+). В `pg_stat_activity` смотрю `wait_event_type = 'Lock'` и состояние `idle in transaction`. Для разбора уже случившегося deadlock читаю лог PostgreSQL — там есть оба процесса, оба запроса и tuple. **В:** Как правильно обрабатывать deadlock в Go-приложении? **О:** Ловить ошибку с `SQLSTATE 40P01` (через `pgconn.PgError` в pgx или `pq.Error` в lib/pq), откатывать транзакцию и повторять её целиком с экспоненциальным backoff и джиттером (чтобы конкурирующие транзакции «разъехались» во времени). Стоит также ретраить `40001` (serialization_failure) и проверять ошибку на `Commit()`, так как на `SERIALIZABLE` ошибка может прийти именно там. Число попыток ограничено. **В:** Что такое advisory locks и какие с ними есть подводные камни? **О:** Это рекомендательные блокировки по произвольному ключу (`bigint`), смысл которых задаёт приложение — удобно для распределённой координации (один процесс на джоб). Бывают session-level (нужно явно отпускать) и xact-level (отпускаются на коммите). Главный подвох — session-level блокировки текут при работе через пул соединений, поэтому на пулах используют `pg_advisory_xact_lock`. Они тоже участвуют в deadlock detector. ## На что копают на senior+ - **Знание матрицы конфликтов блокировок** на уровне таблиц и строк наизусть хотя бы качественно: что `SELECT` и `UPDATE` не конфликтуют на таблице, что `ACCESS EXCLUSIVE` блокирует всё, как `FOR KEY SHARE`/`FOR NO KEY UPDATE` снижают FK-конфликты. - **Понимание, что deadlock неустраним полностью** в конкурентной системе и архитектурно правильный ответ — идемпотентность + retry, а не «убрать все deadlock'и». - **Тонкости retry:** джиттер, ограничение попыток, ретрай и `40P01`, и `40001`, проверка ошибки на `Commit`, идемпотентность операции (важно, если в транзакции были сайд-эффекты вне БД). - **Скрытые источники блокировок:** FK, уникальные индексы, триггеры, каскады, `ON CONFLICT`. Кандидат должен уметь объяснить deadlock там, где в коде нет явного `FOR UPDATE`. - **Влияние уровня изоляции:** на `REPEATABLE READ`/`SERIALIZABLE` появляются serialization failures, меняется поведение `FOR UPDATE` (на RR строка перечитывается с проверкой, можно получить ошибку «could not serialize»). - **Операционные риски:** длинные транзакции и `idle in transaction` как причина и блокировок, и раздувания таблиц (блокируют VACUUM); `lock_timeout` и `idle_in_transaction_session_timeout` как защитные настройки; опасность `ALTER TABLE` под нагрузкой. - **Очереди на SQL:** умение спроектировать надёжную очередь на `FOR UPDATE SKIP LOCKED` (CTE + `UPDATE ... RETURNING`), понимание её ограничений против выделенных брокеров (Kafka/RabbitMQ). - **Диагностика на бою:** уверенное использование `pg_locks`, `pg_blocking_pids`, `pg_stat_activity`, чтение `DETAIL` из лога deadlock, настройка `log_lock_waits`. - **Распределённые блокировки:** advisory locks как примитив для leader election / single-runner, их семантика на пулах, сравнение с внешними координаторами (etcd, Redis, Zookeeper).