Каждый разработчик, работавший над нагруженным проектом, сталкивался с дедлоками - это ситуация, которая возникает в БД, когда две транзакции блокируют друг друга, и в результате одна из них сбрасывается (во всяком случае такое поведение реализовано в PostgreSQL). Недавно пришло время и мне столкнуться с такой ситуацией.
Бэкграунд
Есть rails-приложение, построенное по принципу RIA, в котором фронт-энд логически разделен с бэк-эндом. Фронт-энду нужно знать, что происходит на серверсайде, и поэтому раз в секунду приходит запрос на некий урл, где определенный экшен определенного контроллера производит какие-то действия и рендерит ответ в формате JSON.
И среди действий этого контроллера есть обновление времени последнего доступа. Реализовано оно одним UPDATE
-запросом примерно следующего вида:
1
|
|
На первый взгляд человека, незнакомого с дедлоками, тут нет ничего потенциально проблематичного. Но, тем не менее, в логе продакшен-сервера время от времени попадаются записи вида:
1 2 3 4 5 6 7 |
|
Присмотревшись повнимательнее, можно увидеть, что оба запроса меняют записи с одними и теми же id, в одной и той же таблице - но в разном порядке.
Первый запрос меняет записи 691
, 690
, 692
и 689
; второй в то же время обновляет 686
, 687
и 688
. Далее происходит следующее: первый запрос пытается обновить запись 686
, но на нее уже установлен ShareLock вторым запросом; а второй запрос пытается изменить запись 689
, запертую первым запросом. Потом оба запроса ожидают определенное время (которое устанавливается в настройках постгреса, и по умолчанию равно 1 секунде), один запрос отваливается (вместе со своим ShareLock), а второй продолжает выполнение до победного конца.
Вариант решения
Так как проблема проявлялась на продакшен-сервере, и была довольно критичной, нужно было найти решение в максимально сжатые сроки.
В итоге некоторого обсуждения было решено просто отсортировать id обновляемых записей в обоих запросах. Таким образом, если два запроса одновременно будут обновлять записи, даже если список id будет совпадать, один запрос начнет выполнение раньше другого, и не будет заблокирован; а второй запрос выполнится после снятия ShareLock после завершения первого запроса.
Таким образом, изменения в коде минимальны:
1 2 3 4 |
|
З.Ы. на прошлых выходных установка/настройка нового сервера не позволила мне продолжить серию статей “Краткое введение в Ruby” - она обязательно будет продолжена.