select and lock random row from postgresql from Java application

Mayuran :

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?

MondKin :

There is a race condition with your first select and the first update:

  • Imagine 2 different threads execute step 1 at the same time, and both happen to access the same row. At that point is_locked = false so it's possible for both to select the same row.
  • In step 2, both threads will try to update that row. Thread 1 will succeed immediately, thread 2 will get blocked until thread 1's transaction is committed.
  • Once thread 1 commits, thread 2 will again set is_locked = true to the same row and will process the same account again.
  • This is not desirable, you not only have a thread that had to wait, but you processed the same account twice.

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.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=352276&siteId=1