SELECT FOR UPDATE
не защищает от тупиковых ситуаций. Он просто блокирует строки. Блокировки устанавливаются в процессе, в порядке, указанном ORDER BY
, или в произвольном порядке при отсутствии ORDER BY
. Лучшая защита от взаимоблокировок - блокировать строки в последовательном порядке во всей транзакции - и делать то же самое во всех параллельных транзакциях. Или, как указано в в руководстве, :
Лучшая защита от взаимоблокировок, как правило, состоит в том, чтобы избегать их, будучи уверенными в том, что все приложения, использующие базу данных, устанавливают блокировки на нескольких объектах в согласованном порядке. порядок.
Иначе это может произойти ( row1 , row2 , ... строки пронумерованы в соответствии с виртуальным последовательным порядком):
T1: SELECT FOR UPDATE ... -- lock row2, row3
T2: SELECT FOR UPDATE ... -- lock row4, wait for T1 to release row2
T1: INSERT ... ON CONFLICT ... -- wait for T2 to release lock on row4
--> deadlock
Добавление ORDER BY
к вашему SELECT... FOR UPDATE
может уже избежать ваших тупиков. (Это позволило бы избежать описанного выше.) Или это произойдет, и вам придется сделать больше:
T1: SELECT FOR UPDATE ... -- lock row2, row3
T2: SELECT FOR UPDATE ... -- lock row1, wait for T1 to release row2
T1: INSERT ... ON CONFLICT ... -- wait for T2 to release lock on row1
--> deadlock
Все внутри транзакции должно происходить в последовательном порядке, чтобы быть абсолютно уверенным.
Также , ваш UPDATE
не соответствует SELECT FOR UPDATE
. component_id
<> hw_component_id
. Опечатка? Кроме того, f.archived_at IS NULL
не гарантирует, что более поздний SET archived_at = NOW()
влияет только на эти строки. Вам нужно будет добавить WHERE f.archived_at IS NULL
к UPDATE
быть в очереди. (В любом случае кажется хорошей идеей?)
Я предполагаю, что это может быть вызвано оператором ON CONFLICT DO UPDATE
, который может обновлять строки, не заблокированные предыдущим SELECT FOR UPDATE
.
Пока UPSERT (ON CONFLICT DO UPDATE
) придерживается последовательного порядка, это не будет проблемой. Но это может быть сложно или невозможно обеспечить.
Может ли оператор SELECT ... FOR UPDATE
заблокировать несколько строк, а затем дождаться разблокировки других строк в условии?
Да, как объяснялось выше, блокировки приобретаются в процессе. Возможно, ему придется остановиться и подождать на полпути.
NOWAIT
Если все это по-прежнему не может разрешить ваши взаимоблокировки, медленный и надежный метод - использовать Serializable Isolation Уровень . Затем вы должны быть готовы к ошибкам сериализации и в этом случае повторить транзакцию. В целом значительно дороже.
Или может быть достаточно добавить NOWAIT
:
SELECT FROM files
WHERE component_id = $1
AND archived_at IS NULL
ORDER BY id -- whatever you use for consistent, deterministic order
FOR UPDATE NOWAIT;
Руководство:
С NOWAIT
, оператор сообщает об ошибке, а не ожидает, если выбранная строка не может быть заблокирована немедленно.
Вы можете даже пропустить предложение ORDER BY
с помощью NOWAIT
, если не можете установить sh в любом случае согласованный заказ с UPSERT.
Затем вы должны отловить эту ошибку и повторить транзакцию. Подобно обнаружению сбоев сериализации, но намного дешевле - и менее надежно. Например, несколько транзакций все еще могут блокироваться только с помощью их UPSERT. Но это становится все менее и менее вероятным.