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