Тупик, связанный с SELECT FOR UPDATE - PullRequest
1 голос
/ 04 августа 2020

У меня транзакция с несколькими запросами. Сначала выбираются строки с блокировкой FOR UPDATE:

SELECT f.source_id FROM files AS f WHERE
    f.component_id = $1 AND
    f.archived_at IS NULL
FOR UPDATE

Далее идет запрос на обновление:

UPDATE files AS f SET archived_at = NOW()
WHERE
hw_component_id = $1 AND
f.source_id = ANY($2::text[])

И затем идет вставка:

INSERT INTO files AS f (
    source_id,
    ...
)
VALUES (..)
ON CONFLICT (component_id, source_id) DO UPDATE
SET archived_at = null,
is_valid = excluded.is_valid

У меня два экземпляра приложения, и иногда я вижу ошибки взаимоблокировки в PostgreSQL журнале:

ERROR:  deadlock detected
DETAIL:  Process 3992939 waits for ShareLock on transaction 230221362; blocked by process 4108096.
Process 4108096 waits for ShareLock on transaction 230221365; blocked by process 3992939.
Process 3992939: SELECT f.source_id FROM files AS f WHERE f.component_id = $1 AND f.archived_at IS NULL FOR UPDATE
Process 4108096: INSERT INTO files AS f (source_id, ...) VALUES (..) ON CONFLICT (component_id, source_id) DO UPDATE SET archived_at = null, is_valid = excluded.is_valid
CONTEXT:  while locking tuple (41116,185) in relation \"files\"

Я предполагаю, что это может быть вызвано оператором ON CONFLICT DO UPDATE, который может обновлять строки, которые не заблокированы предыдущим SELECT FOR UPDATE

Но я не могу понять, как запрос SELECT ... FOR UPDATE может вызвать тупик, если это первый запрос в транзакции. Перед ним нет запросов. Может ли оператор SELECT ... FOR UPDATE заблокировать несколько строк, а затем дождаться разблокировки других строк в состоянии?

1 Ответ

1 голос
/ 05 августа 2020

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. Но это становится все менее и менее вероятным.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...