Объясните механизм взаимной блокировки, возникающей, когда две транзакции захватывают одни и те же строки в противоположном порядке: почему СУБД не может просто ждать бесконечно?
Взаимная блокировка, или deadlock, возникает, когда каждая транзакция удерживает ресурс, нужный другой транзакции, и сама ожидает ресурс, удерживаемый другой. Ожидание образует цикл, поэтому обычное продолжение невозможно. СУБД должна обнаружить такой цикл или прервать ожидание по тайм-ауту, выбрать одну транзакцию жертвой, откатить её и освободить блокировки.
Блокировки появились как механизм защиты согласованности данных при одновременном выполнении транзакций. Они позволяют одной транзакции временно удерживать изменяемые данные, чтобы другая не прочитала или не изменила их в несовместимом состоянии.
Такая защита создаёт естественный компромисс: чем дольше удерживаются блокировки, тем выше изоляция, но тем больше ожидание и риск конфликтов. Deadlock — не обязательно ошибка реализации СУБД; это возможное следствие корректного использования блокировок несколькими участниками.
Предположим, транзакция T1 заблокировала строку A и пытается получить строку B. Одновременно T2 уже заблокировала строку B и пытается получить строку A.
Ни одна транзакция не может продолжить работу, пока другая не отпустит блокировку. Но отпустить её можно обычно только после завершения транзакции, а завершить её невозможно из-за ожидания второй блокировки. Если ничего не предпринять, запросы будут ждать бесконечно, удерживая ресурсы и снижая пропускную способность системы.
Состояние можно представить как граф ожидания: вершина — транзакция, ребро от T1 к T2 означает, что T1 ждёт ресурс, удерживаемый T2. Цикл в таком графе означает deadlock.
После первых изменений T1 удерживает строку с идентификатором 1, а T2 — строку с идентификатором 2. Вторые изменения создают цикл ожидания. Конкретные типы блокировок, момент их получения и текст ошибки зависят от СУБД и изоляции, но логика конфликта остаётся той же.
СУБД может периодически анализировать граф ожидания и обнаруживать цикл. Затем она выбирает одну транзакцию жертвой — например, по стоимости отката, длительности или другим внутренним критериям — откатывает её и тем самым освобождает блокировки. Вторая транзакция получает возможность продолжить работу.
Другой вариант — тайм-аут ожидания. Он не доказывает наличие цикла: запрос просто прерывается после заданного времени. Поэтому приложение должно быть готово обработать как явную ошибку deadlock, так и ошибку тайм-аута.
Основная профилактика — единый порядок захвата ресурсов. Например, все операции, которым нужны строки 1 и 2, должны сначала блокировать строку с меньшим идентификатором. Тогда цикл из противоположного порядка обычно не возникает. Дополнительно помогают короткие транзакции, отсутствие внешних вызовов внутри транзакции и согласованный порядок обращения к таблицам.
Полностью исключить deadlock только на уровне приложения бывает трудно: блокировки могут возникать из-за разных индексов, триггеров, каскадных изменений или скрытых операций СУБД. Поэтому приложение всё равно должно корректно повторять всю транзакцию после отката жертвы. Повтор допустим лишь для операций, которые можно безопасно выполнить повторно; для внешних побочных эффектов нужна отдельная идемпотентность или схема надёжной публикации событий.
В сервисе переводов один обработчик сначала изменял счёт отправителя, затем счёт получателя. Другой путь, выполнявший возврат, изменял те же счета в обратном порядке. При редких параллельных операциях возникали deadlock, из-за которых пользователи получали ошибки, хотя данные не становились некорректными.
Рассматривались три варианта. Увеличение тайм-аута уменьшало число немедленных ошибок, но лишь дольше удерживало блокировки и не устраняло цикл. Переход на более слабую изоляцию не решал проблему конфликтующих изменений и мог ухудшить гарантии согласованности. Безусловный повтор отдельных запросов был опасен: частичный повтор мог привести к неверной бизнес-операции.
Выбрали единый порядок блокировки счетов по их идентификаторам, ограничили длительность транзакции и добавили повтор всей транзакции при ошибке deadlock с ограниченным числом попыток и задержкой. Внешнее уведомление отправлялось только после успешной фиксации изменения. В результате циклические ожидания исчезли для штатного сценария, а редкие конфликты от других операций стали безопасно обрабатываться повтором.
Ответ: При обычном ожидании существует направленная зависимость без цикла: транзакция ждёт другую, которая в конечном итоге может завершиться и освободить ресурс. При deadlock образуется цикл зависимостей, поэтому ни один участник не может продвинуться самостоятельно. Долгое ожидание ещё не доказывает deadlock: причиной может быть медленная транзакция, большой объём работы или длительная блокировка без цикла.
Ответ: Откат жертвы обычно отменяет всю её транзакцию, а не только запрос, вызвавший обнаружение deadlock. После этого нужно заново выполнить все действия транзакции, соблюдая её исходную бизнес-логику. Повтор одного запроса может оставить операцию неполной или применить её к состоянию, отличающемуся от того, для которого она рассчитывалась.
Ответ: Он устраняет конкретный класс циклов, возникающих из-за противоположного порядка захвата тех же ресурсов. Но deadlock может появиться через другие ресурсы: разные таблицы, индексы, триггеры, каскадные действия или дополнительные блокировки, получаемые СУБД. Поэтому единый порядок — важная профилактика, но не замена обработке ошибок deadlock и контролю длительности транзакций.