Блокировка в Postgres для комбинации UPDATE / INSERT

11

У меня есть две таблицы. Одним из них является таблица журнала; другой содержит, по сути, коды купонов, которые можно использовать только один раз.

Пользователь должен иметь возможность использовать купон, который вставит строку в таблицу журнала и пометит купон как использованный (обновив usedстолбец до true).

Естественно, здесь есть очевидная проблема состояния / безопасности гонки.

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

Есть ли лучший способ сделать это в Postgres? В частности, я обеспокоен тем, что блокировка глобальная, но не обязательна - мне действительно нужно только убедиться, что никто другой не пытается ввести этот конкретный код, так что, возможно, сработает какая-то блокировка на уровне строк?

Роб Миллер
источник

Ответы:

15

Я слышал о проблемах параллелизма в MySQL раньше. Не так в Postgres.

Достаточно встроенных блокировок на уровне строк в уровне READ COMMITTEDизоляции транзакций по умолчанию .

Я предлагаю один оператор с CTE, изменяющим данные (чего у MySQL также нет), потому что удобно передавать значения из одной таблицы в другую напрямую (если вам это нужно). Если вам ничего не нужно из couponтаблицы, вы также можете использовать транзакцию с отдельным оператором UPDATEи INSERTоператорами.

WITH upd AS (
   UPDATE coupon
   SET    used = true
   WHERE  coupon_id = 123
   AND    NOT used
   RETURNING coupon_id, other_column
   )
INSERT INTO log (coupon_id, other_column)
SELECT coupon_id, other_column FROM upd;

Редко случается , что более чем одна транзакция пытается выкупить один и тот же купон. У них есть уникальный номер, не так ли? Более чем одна попытка транзакции в один и тот же момент времени должна быть намного реже. (Может быть, ошибка приложения или кто-то пытается воспроизвести систему?)

Как бы то ни было, UPDATEединственное, что успешно выполняется только для одной транзакции, несмотря ни на что. Объект UPDATEполучает блокировку на уровне строк в каждой целевой строке перед обновлением. Если параллельная транзакция пытается получить доступ к UPDATEтой же строке, она увидит блокировку в строке и будет ждать завершения ( ROLLBACKили COMMIT) блокирующей транзакции , а затем станет первой в очереди блокировки:

  • Если совершено, перепроверьте условие. Если это все еще NOT used, заблокируйте строку и продолжайте. Иначе UPDATEтеперь не находит подходящей строки и ничего не делает, не возвращая строки, поэтому INSERTтакже ничего не делает.

  • В случае отката заблокируйте строку и продолжайте.

Там нет никакого потенциала для состояния гонки .

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

Это INSERTбеззаботно. Если по какой-то ошибке coupon_idуже есть в logтаблице (и у вас есть ограничение UNIQUE или PK log.coupon_id), вся транзакция будет откатана после уникального нарушения. Укажет недопустимое состояние в вашей БД. Если приведенное выше утверждение является единственным способом записи в logтаблицу, это никогда не должно происходить.

Эрвин Брандштеттер
источник
Действительно, очень редко случается так, что несколько транзакций пытаются выкупить один и тот же код, но ваши подозрения верны в том, что это будет происходить исключительно, когда кто-то пытается играть в систему. Большое спасибо за это - CTE были для меня большой выгодой при переезде в Postgres, но я не понимал, что неявная блокировка будет достаточно для этого.
Роб Миллер