Что делать при возникновении deadlock при синхронизации переводов между счетами
Что проверяют
Понимаете ли вы, почему две одновременные операции способны заблокировать друг друга, и умеете ли назвать порядок действий: как обнаружить, как снять сейчас и как перестроить код, чтобы не повторилось. Перевод между счетами это канонический пример, где блокировки берутся парами, поэтому ждут разговора и про потоки JVM, и про транзакции базы.
Как отвечать
Начните с того, что взаимная блокировка бывает двух уровней и лечится по-разному:
- внутри JVM: два потока держат мониторы и ждут чужой. Ищется через
jcmd <pid> Thread.printилиjstack, в выводе есть блок «Found one Java-level deadlock». Программно то же даётThreadMXBean.findDeadlockedThreads(); - в базе: две транзакции взяли строки счетов в разном порядке. Здесь СУБД сама разрывает цикл и откатывает одну транзакцию (Postgres отдаёт SQLSTATE 40P01), приложению остаётся распознать ошибку и повторить операцию.
Главное средство лечения одно: детерминированный порядок захвата. Счета блокируются всегда в одном порядке, например по возрастанию идентификатора:
Account first = a.id() < b.id() ? a : b;
Account second = (first == a) ? b : a;
first.lock().lock();
second.lock().lock();Если стабильного ключа нет, берут tryLock с таймаутом и случайной паузой перед повтором. Скажите и то, что деньги обычно живут в базе: надёжнее держать перевод одной транзакцией с SELECT ... FOR UPDATE в отсортированном порядке, а повтор делать идемпотентным по ключу операции.
Где спотыкаются
Предлагают «убить зависший поток». Безопасного способа снять поток в Java нет: Thread.stop небезопасен и убран, а interrupt не выводит из ожидания монитора. Реально помогает только перезапуск процесса, и это аварийная мера.
Второе: tryLock без таймаута в цикле. Взаимная блокировка превращается в livelock: потоки бесконечно берут и отпускают блокировки, процессор занят, прогресса нет. Нужны таймаут, ограничение попыток и разброс паузы.
Третье: синхронизация внутри JVM там, где приложение работает в нескольких экземплярах. synchronized защищает один процесс, соседний экземпляр про него не знает, и двойное списание проходит мимо. Общий ресурс защищается базой или внешней блокировкой.