Reputation: 708
I have a situation where I have a table with around 500 accounts. These accounts are used for doing some financial transaction submissions. When a transaction starts, I need to get an account from this table(any random account would work) and then lock that row so no other row will have access to this account until the operation is finished. Then do submit the transaction to the external system and then unlock the account.
I have implemented this using the following algorithm. All queries are executed in the transactional context here.
//Get Random account
1. select * from gas_accounts where is_locked = false order by random() limit 1
//Lock that account using the primary key
2. update gas_accounts set is_locked = true, last_updated_at = current_timestamp where public_key = :publicKey
//unlock the account
3. update gas_accounts set is_locked = false, last_updated_at = current_timestamp where public_key = :publicKey
When running with 50 concurrent threads, the above function takes sometime around 50 seconds to return. Is there any better way to do this?
Upvotes: 1
Views: 925
Reputation: 23015
There is a race condition with your first select and the first update:
is_locked = false
so it's possible for both to select the same row.is_locked = true
to the same row and will process the same account again. Instead, Postgres has a clause called FOR UPDATE SKIP LOCKED
, which will allow you to do step 1 like this (steps 2 and 3 remain unchanged):
SELECT * FROM gas_accounts WHERE is_locked = false LIMIT 1 FOR UPDATE SKIP LOCKED
Notice you don't even need the order by random()
as this will select the next available row that is not locked (either by your is_locked
column nor by Postgres' own locking), so you may consider that's a random row.
What this clause do is to give each transaction one row that is not currently locked by any other transaction, so the race condition disappears completely. Just ensure you are executing the SELECT and the first UPDATE in the same transaction.
Additional note:
Also, notice that if the 3 statements happen in the same transaction, you don't even need to have a is_locked
column. Just by using FOR UPDATE SKIP LOCKED
the row will remain locked and invisible to other transactions that also use that clause.
Upvotes: 1