PostgreSQL одновременная проверка, существует ли строка в таблице - PullRequest
0 голосов
/ 17 марта 2020

Предположим, у меня есть простой лог c:
Если у пользователя ранее не было начисления баланса (что записано в таблице accruals), мы должны дать ему 100 $ на баланс:

START TRANSACTION;
DO LANGUAGE plpgsql $$
DECLARE _accrual accruals;
BEGIN
  --LOCK TABLE accruals; -- label A
  SELECT * INTO _accrual from accruals WHERE user_id = 1;
  IF _accrual.accrual_id IS NOT NULL THEN
    RAISE SQLSTATE '22023';
  END IF;
  UPDATE users SET balance = balance + 100 WHERE user_id = 1;
  INSERT INTO accruals (user_id, amount) VALUES (1, 100);
END
$$;
COMMIT;

Проблема этой транзакции в том, что она не параллельная.
Выполнение этой транзакции в параллельных результатах с получением user_id=1 с balance=200 и записью 2 начислений.

Как проверить параллельность?
1. Я запускаю в сеансе 1: START TRANSACTION; LOCK TABLE accruals;
2. В сеансе 2 и сеансе 3 я запускаю эту транзакцию
3. В сеансе 1: ROLLBACK

Вопрос заключается в следующем: как мне сделать это 100% одновременное и убедитесь, что у пользователя будет 100 $ только один раз.
Единственный способ, которым я вижу, - заблокировать таблицу (label A в примере кода)

Но есть ли у меня другой способ?

1 Ответ

1 голос
/ 17 марта 2020

Простейшим способом, вероятно, является использование сериализуемого уровня изоляции (путем изменения default_transaction_isolation). Тогда один из процессов должен получить что-то вроде «ОШИБКА: не удалось сериализовать доступ из-за одновременного обновления»

Если вы хотите сохранить уровень изоляции на уровне «подтверждено чтение», то вы можете просто подсчитать начисления в конце и выдает ошибку:

START TRANSACTION;
DO LANGUAGE plpgsql $$
DECLARE _accrual accruals;
        _count int;
BEGIN
  SELECT * INTO _accrual from accruals WHERE user_id = 1;
  IF _accrual.accrual_id IS NOT NULL THEN
    RAISE SQLSTATE '22023';
  END IF;
  UPDATE users SET balance = balance + 100 WHERE user_id = 1;
  INSERT INTO accruals (user_id, amount) VALUES (1, 100);
  select count(*) into _count from accruals where user_id=1;
  IF _count >1 THEN
    RAISE SQLSTATE '22023';
  END IF;
END
$$;
COMMIT;

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

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

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