Простейшим способом, вероятно, является использование сериализуемого уровня изоляции (путем изменения 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, вы можете хочу сохранить его.