Блог 7even

про ruby, rails, sinatra, git и всё на свете

Сказ о Deadlock-ах

Каждый разработчик, работавший над нагруженным проектом, сталкивался с дедлоками - это ситуация, которая возникает в БД, когда две транзакции блокируют друг друга, и в результате одна из них сбрасывается (во всяком случае такое поведение реализовано в PostgreSQL). Недавно пришло время и мне столкнуться с такой ситуацией.

Бэкграунд

Есть rails-приложение, построенное по принципу RIA, в котором фронт-энд логически разделен с бэк-эндом. Фронт-энду нужно знать, что происходит на серверсайде, и поэтому раз в секунду приходит запрос на некий урл, где определенный экшен определенного контроллера производит какие-то действия и рендерит ответ в формате JSON.

И среди действий этого контроллера есть обновление времени последнего доступа. Реализовано оно одним UPDATE-запросом примерно следующего вида:

1
UPDATE items SET access_time = NOW() WHERE id IN (34256, 34978, 34147)

На первый взгляд человека, незнакомого с дедлоками, тут нет ничего потенциально проблематичного. Но, тем не менее, в логе продакшен-сервера время от времени попадаются записи вида:

1
2
3
4
5
6
7
postgres[22432]: [30-1] ERROR:  deadlock detected
postgres[22432]: [30-2] DETAIL:  Process 22432 waits for ShareLock on transaction 189302415; blocked by process 22443.
postgres[22432]: [30-3]  Process 22443 waits for ShareLock on transaction 189302416; blocked by process 22432.
postgres[22432]: [30-4]  Process 22432: UPDATE "items" SET "access_time" = '2011-07-08 08:49:03.429301' WHERE "items"."id" IN (691, 690, 692, 689, 686, 688, 687)
postgres[22432]: [30-5]  Process 22443: UPDATE "items" SET "access_time" = '2011-07-08 08:49:03.414084' WHERE "items"."id" IN (686, 687, 688, 689, 691, 690)
postgres[22432]: [30-6] HINT:  See server log for query details.
postgres[22432]: [30-7] STATEMENT:  UPDATE "items" SET "access_time" = '2011-07-08 08:49:03.429301' WHERE "items"."id" IN (691, 690, 692, 689, 686, 688, 687)

Присмотревшись повнимательнее, можно увидеть, что оба запроса меняют записи с одними и теми же id, в одной и той же таблице - но в разном порядке.

Первый запрос меняет записи 691, 690, 692 и 689; второй в то же время обновляет 686, 687 и 688. Далее происходит следующее: первый запрос пытается обновить запись 686, но на нее уже установлен ShareLock вторым запросом; а второй запрос пытается изменить запись 689, запертую первым запросом. Потом оба запроса ожидают определенное время (которое устанавливается в настройках постгреса, и по умолчанию равно 1 секунде), один запрос отваливается (вместе со своим ShareLock), а второй продолжает выполнение до победного конца.

Вариант решения

Так как проблема проявлялась на продакшен-сервере, и была довольно критичной, нужно было найти решение в максимально сжатые сроки.

В итоге некоторого обсуждения было решено просто отсортировать id обновляемых записей в обоих запросах. Таким образом, если два запроса одновременно будут обновлять записи, даже если список id будет совпадать, один запрос начнет выполнение раньше другого, и не будет заблокирован; а второй запрос выполнится после снятия ShareLock после завершения первого запроса.

Таким образом, изменения в коде минимальны:

1
2
3
4
# было
Item.where(id: item_ids).update_all(access_time: Time.now) unless item_ids.empty?
# стало
Item.where(id: item_ids.sort).update_all(access_time: Time.now) unless item_ids.empty?

З.Ы. на прошлых выходных установка/настройка нового сервера не позволила мне продолжить серию статей “Краткое введение в Ruby” - она обязательно будет продолжена.

Комментарии