Обновить, когда условия гонки Postgres (читать прочитано)

Я пытаюсь написать запрос, который обновляет строку в таблице "претензии" до активного состояния только в том случае, если пользователь не имеет более двух активных заявок, открытых уже. Поэтому для обеспечения целостности данных очень важно, чтобы пользователь никогда не открывал более двух активных заявок в любой момент времени.

Я запускаю этот запрос в параллельной среде, поэтому возможно, что два процесса выполняют этот запрос одновременно. Я также запускаю его под уровнем изоляции Read Committed по умолчанию.

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

В то же время производительность не так важна для этого запроса, как целостность данных.

update claim
set is_active = '1'
where claim.id = %s
and (2 > (select count(*)
          from claim as active_claim
          where active_claim.user_id = %s
          and active_claim.is_active = '1'))

Ответ 1

Да, это абсолютно возможно, что это может привести к более чем двум активным требованиям, поскольку параллельные транзакции не могут видеть изменения друг друга, поэтому два или более одновременных исполнения будут отображаться как две заявки, и оба приступают к обновлению своих целевых требований чтобы сделать их активными.

См. связанные: Операции с базами данных препятствуют условиям гонки.

Блокировка таблиц

Самый простой вариант - просто:

BEGIN;
LOCK TABLE claim IN EXCLUSIVE MODE;
UPDATE ...
COMMIT;

... но это довольно тяжелое решение.

Блокировка уровня строки на объекте пользователя

Предполагая, что у вас есть таблица user для владельца претензии, вы должны:

SELECT 1 FROM user WHERE user_id = whatever FOR UPDATE

в той же транзакции, перед запуском UPDATE. Таким образом, у вас будет эксклюзивная блокировка строки для пользователя, а другие операторы SELECT ... FOR UPDATE блокируют вашу блокировку. Эта блокировка также блокирует UPDATE и удаляет user; он не будет блокировать обычный SELECT пользователя без предложения FOR UPDATE или FOR SHARE.

См. явная блокировка в руководстве PostgreSQL.

SERIALIZABLE Изоляция

Альтернативой является использование изоляции SERIALIZABLE; PostgreSQL 9.2 и новее имеют обнаружение зависимостей транзакций, из-за которых все, кроме одной из конфликтующих транзакций, прерываются с ошибкой сериализации в приведенном выше примере. Поэтому ваше приложение должно помнить, что он пытался сделать, когда он запускает транзакцию, и может ловить ошибки, обнаруживать, что они являются сбоями в сериализации, и повторить попытку после сбоя в сериализации.

См. изоляция транзакций в руководстве PostgreSQL.

Контрольные блокировки

Иногда нет хорошего объекта-кандидата для блокировки строки, и по какой-либо причине или другой сериализуемой изоляции эта проблема не будет решена или не будет использоваться по другим причинам. Это не относится к вам, это просто общая информация.

В таких случаях вы можете использовать консультативные блокировки PostgreSQL для блокировки произвольных числовых значений; в этом случае вы бы, например, pg_advisory_xact_lock(active_claim.user_id). Явная глава блокировки содержит больше информации.