简体   繁体   中英

PostgreSQL concurrent check if row exists in table

Suppose I have simple logic:
If user had no balance accrual earlier (which is recorded in accruals table), we must give him 100$ to balance:

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;

The problem of this transaction is it's not concurrent.
Running this transaction in parrallel results getting user_id=1 with balance=200 and 2 accruals recorded.

How do I test concurrency ?
1. I run in session 1: START TRANSACTION; LOCK TABLE accruals; START TRANSACTION; LOCK TABLE accruals;
2. In session 2 and session 3 I run this transaction
3. In session 1: ROLLBACK

The question is: How do I make this 100% concurrent and make sure user will have 100$ only once.
The only way I see is to lock the table ( label A in code sample)

But do I have another way ?

The simplest way is probably to use the serializable isolation level (by changing default_transaction_isolation). Then one of the processes should get something like "ERROR: could not serialize access due to concurrent update"

If you want to keep the isolation level at 'read committed', then you can just count accruals at the end and throw an error:

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;

This works because one process will block the other on the UPDATE (assuming non-zero number of rows get updated), and by the time one process commits to release the blocked process, its inserted row will be visible to the other one.

Formally there is then no need for the first check, but if you don't want a lot of churn due to rolled back INSERT and UPDATE, you might want to keep it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM