Distributed locking with Postgres advisory locks
rclayton.silvrback.com
rclayton.silvrback.com
It's possible to write software that relies on READ COMMITTED and behaves correctly.
I wrote up more details on https://sudhir.io/understanding-connections-pools
I would use Hashicorp Consul for this these days.
Well, I think it just blocks until the lock is released. I tested locally and checked the docs[0] to confirm. In any event, this is a cool mechanism to be aware of. If you ever need pg_advisory_xact_lock and you find yourself wanting to unlock it mid-transaction and are scratching your head just introduce a nested transaction :)
[0] https://www.postgresql.org/docs/12/functions-admin.html
> pg_advisory_lock locks an application-defined resource, which can be identified either by a single 64-bit key value or two 32-bit key values (note that these two key spaces do not overlap). If another session already holds a lock on the same resource identifier, this function will wait until the resource becomes available. The lock is exclusive. Multiple lock requests stack, so that if the same resource is locked three times it must then be unlocked three times to be released for other sessions' use.
normal Postgres locks have a https://postgresqlco.nf/doc/en/param/lock_timeout/ which is max time a client will wait to acquire lock before returning an error instead
SELECT FOR UPDATE queries also accept NOWAIT keyword to return error immediately if locked
I would guess the former applies to advisory locks too. I think NOWAIT only applies to FOR UPDATE / FOR SHARE queries
But it looks like there is try_advisory_lock method https://www.postgresql.org/docs/current/functions-admin.html... that does the equivalent
https://www.postgresql.org/docs/14/functions-admin.html#FUNC...
That said, I'm curious if the example given is just a simplified example, because to me this absolutely seems like a case where I wouldn't use a custom locking solution, but a case where I would use basic DB transaction guarantees to ensure that I didn't insert a row without the balance being sufficient, e.g.:
INSERT INTO ledger (tx_type, credit_holder_id, credit_delta)
SELECT 'use', 'foo', -10
FROM (SELECT sum(credit_delta) as balance
FROM ledger WHERE credit_holder_id = 'foo') foo_balance
WHERE foo_balance.balance > 10;
I'd need to double check the transaction isolation level that you'd need in PG, but generally PG is really good about efficiently supporting repeatable read isolation level.Simpler cases are supported by e.g. SELECT FOR UPDATE
SELECT ... FOR UPDATE
Should work here. Lock the table when the balance is confirmed, then append to the ledger, then commit the transaction. All following transactions then need to wait for the FOR UPDATE lock to be released before they can read from the ledger.They have 1000s of concurrent transactions on that table for different credit holders. Locking the whole table every time means everyone is stalled, even though you only want transactions for a single credit holder to be serialised, not every single mutation of that table.
select *
from transactions
where
account_id=$user_account_id
for update;
Wouldn't all rows for the particular user be locked ? Then you can check the balance, append, commit and release the lock BEGIN;
SELECT * FROM accounts WHERE id = $account_id FOR UPDATE;
-- select/insert/update/delete transactions
COMMIT;
This essentially serializes all reads/writes to data that belongs to the account. Not sure if this performs better or worse than using an advisory lock.For instance, you may want to do some sanity checks, to see if the account is in a valid status, i.e "ACTIVE" and not "BLOCKED". You'll also probably want to create a corresponding credit entry to match the newly created debit. I think you are better off optimistically creating a new transaction with a "STARTING" status and then settling it asynchronous (but quickly), rolling it back to an invalid state if something goes wrong, or to "COMPLETED" if everything clears.
if you didn't want to do this, i would also assume you could do this within a transaction w/o the lock using a serializable transaction. though no idea how indexes interplay with postgres and serializibility.
However, in some cases you want to enact a specific locking order on some objects that interact, and in particular if locks aren't reentrant, I'd be cautious and at least use a cryptographic hash here to reduce likelihood if collisions.
Since the lock is centralised, the use case could be either redesigned to also be centralised, or could be switched to use a different distributed algorithm.