Reputation: 27283
Assume we have a table that describes the contents of a user's basket of fruit. This is essentially just a mapping of user -> list of fruit.
Periodically, we query a remote data source and get a new list for a particular user. Then, we try to wholly replace all of that user's current rows with a new set of rows. For example:
Before:
user fruit freshness
-------------------------------
...
47 'apple' 0.1
47 'pear' 9.5
47 'pear' 2.8
...
After:
user fruit freshness
-------------------------------
...
47 'apple' 93.9034
47 'banana' 0
...
Given a new set of rows, is there a way to do the replacement that is atomic in postgres?
The following does NOT work:
BEGIN
DELETE FROM basket WHERE user=47
INSERT INTO basket ...
COMMIT
If we have multiple transactions going on simultaneously, postgres will happily order commands like this (with no locking issues):
BEGIN
BEGIN
DELETE
DELETE
INSERT
INSERT <---- data inserted twice here
COMMIT
COMMIT
This will result in twice as many rows as there should be.
A lot of answers to similar questions claim that the first DELETE will lock the rows in question and the second transaction will have to wait, but this isn't true (for whatever reason). Both transactions happily step on each other's toes here.
Notes:
user
-> fruit[]
. I could just stuff this all in a single jsonb column, but then I can't run joins on the rows. And joining is nice :(Upvotes: 3
Views: 2161
Reputation: 121474
You can lock the user_id
that you are going to modify. If you have a users
table then select the corresponding row for update:
BEGIN;
SELECT user_id FROM users WHERE user_id = 47 FOR UPDATE;
DELETE FROM basket WHERE user_id = 47;
INSERT INTO basket ...
COMMIT;
Alternatively you can use advisory locks, e.g.:
BEGIN;
SELECT pg_advisory_lock('basket'::regclass::int, 47);
DELETE FROM basket WHERE user_id = 47;
INSERT INTO basket ...
SELECT pg_advisory_unlock('basket'::regclass::int, 47);
COMMIT;
Read about Explicit Locking in the documentation.
Upvotes: 5