Что такое дедлок
Дедлок — это ситуация, в которой несколько транзакций блокируют друг друга так, что ни одна из них не может продолжить выполнение. Причина всегда одна и та же: возникает цикл ожиданий, когда каждая транзакция ждёт ресурс, который уже удерживается другой транзакцией — замкнутый круг.
Важно понимать: дедлоки — это нормальная часть работы любой СУБД с блокировками. Это не баг и не признак того, что система работает неправильно. Они неизбежны в условиях конкурентного доступа к данным. Задача PostgreSQL — не предотвратить их любой ценой, а быстро обнаружить и корректно разрешить.
Простейший пример: транзакция_1 заблокировала одну строку и пытается получить доступ к другой, а транзакция_2 уже держит вторую строку и, в свою очередь, хочет получить первую. Обе ждут друг друга — и ни одна не может продвинуться дальше.
Почему PostgreSQL не предотвращает дедлоки заранее
Для того чтобы предсказать возникновение дедлока, системе нужно предсказывать поведение транзакций: какие ресурсы они будут запрашивать дальше и в каком порядке. Это почти невозможно. Транзакции могут быть сложными, порядок операций — зависеть от данных и условий выполнения, а сами зависимости — формироваться динамически. Попытка проанализировать всё это заранее приведёт к значительному падению производительности.
PostgreSQL использует более практичный подход: он допускает возникновение дедлоков, но с помощью специальных проверок оперативно их обнаруживает и устраняет. Это позволяет сохранить баланс между производительностью и надёжностью.
Проверка дедлока
Проверка не выполняется постоянно. Она запускается только тогда, когда транзакция начинает ждать блокировку дольше
заданного времени (параметр deadlock_timeout в конфиге). По умолчанию он равен одной секунде. Это
позволяет PostgreSQL улучшить производительность и не проверять каждое ожидание: большинство блокировок
удерживаются недолго и освобождаются сами.
Для большинства систем значение по умолчанию подойдёт лучше всего. Менять его стоит только тщательно обдумав — слишком маленькое значение увеличит нагрузку на сервер, а слишком большое — приведёт к тому, что реальные дедлоки будут обнаружены с задержкой.
Как PostgreSQL обнаруживает дедлок
В основе механизма лежит граф ожиданий (wait-for graph). Это абстрактная модель, в которой вершинами являются транзакции, а рёбра — это отношение "ждёт" (Если упростить, то Транзакция_1 ждёт пока Транзакция_2 освободит блокировку = Т1 -> Т2).
База данных строит граф из транзакций, которые ожидают освобождения блокировок. Если в нём появляется цикл, например: T1 -> T2 -> T3 -> T1, то это означает, что каждая транзакция в цепочке ждёт другую, и ни одна из них не сможет продолжить выполнение самостоятельно. Именно в поиске таких циклов и заключается работа механизма обнаружения дедлоков.
Если при обходе графа цикл найден — дедлок подтверждён. Если нет — система считает, что это просто длительное
ожидание, и продолжает работу (дальше упадёт только по таймауту для ожидания блокировки
lock_timeout или statement_timeout, если они установлены).
Разрешение дедлоков
Если цикл найден, система должна разорвать его. Для этого выбирается одна транзакция, которая будет принудительно
завершена. Для уменьшения потерь, PostgreSQL выбирает не просто первую попавшуюся транзакцию, а ту, которая выполнила
меньше работы и удерживает меньше ресурсов (т.е. ту, чей откат будет "дешевле"). Выбранная транзакция прерывается,
откатывается (ROLLBACK), и все её блокировки освобождаются. Остальные транзакции, участвующие в цикле,
получают возможность продолжить выполнение.
ERROR: deadlock detected
DETAIL: Process xxx waits for xxx blocked by process xxx
Эта ошибка не означает, что система сломалась, наоборот, проблема решена, но текущую транзакцию необходимо повторить.
--Транзакция 1
BEGIN;
UPDATE goods SET stock_count = stock_count - 1 WHERE id = 1;
UPDATE goods SET stock_count = stock_count - 3 WHERE id = 2;
--Транзакция 2
BEGIN;
UPDATE goods SET stock_count = stock_count + 5 WHERE id = 2;
UPDATE goods SET stock_count = stock_count + 10 WHERE id = 1;
-- каждая транзакция держит одну строку и ждёт другую. PostgreSQL фиксирует цикл в графе ожиданий и завершает
-- одну из транзакций с ошибкой. Вторая транзакция будет выполнена
PostgreSQL не защищает от дедлоков — он делает их безопасными. Полностью избавиться от них в нагруженных системах невозможно. Но если ставить блокировки в одинаковом порядке и не делать длинных транзакций, то дедлоков будет минимальное количество. Также хорошим решением будет научить приложение отлавливать ошибку deadlock, и повторять отменённую транзакцию.
Построить граф ожиданий, обнаружить цикл и откатить одну из транзакций — три ключевых этапа обнаружения дедлоков. Такой подход позволяет системе сохранять высокую производительность и при этом гарантировать, что она не зависнет в состоянии бесконечного ожидания.