Race condition on balance updates: Pessimistic Lock or Redlock?
Question
We have a queue job that deducts money from a user's balance. If the user triggers two operations in quick succession, two jobs for the same user end up in the queue; we run 10 parallel workers. Worker 1 and 2 read the old balance at the same time (100), both deduct 40 and write 60 (it should be 20). To prevent this race condition, should I use Pessimistic Locking or a Redis distributed lock (Redlock)?
Answer
Short answer: what you’re hitting is the classic lost-update problem. When money is involved, keep the invariant in the DB; the cleanest fix is an atomic conditional UPDATE, with SELECT ... FOR UPDATE as the second choice. Don’t reach for Redlock.
The core of your scenario: the read-modify-write cycle is split across parallel workers. Both read the old value, both write over it, and one update is lost. The fix is to make that cycle atomic.
- Best: an atomic conditional
UPDATE. WriteUPDATE accounts SET balance = balance - 40 WHERE id = ? AND balance >= 40. Read and write are one statement; no app-side lock needed. The DB manages the row lock itself, which both resolves the race and refuses to overdraw via thebalance >= 40condition. If zero rows are affected, the operation failed on insufficient funds. - Alternative:
SELECT ... FOR UPDATE. If your logic between read and write is complex, lock the balance row inside a transaction withFOR UPDATE. The second worker that reaches that row waits for the first to commit; they serialize, no clash. That’s pessimistic locking. - Optimistic locking works under low contention too. Add a
versioncolumn to the row; on update includeWHERE version = ?and retry if it doesn’t match. Cheap when conflicts are rare, but it creates a retry storm under heavy contention. - Use Redlock only for a non-DB resource. A Redis distributed lock (Redlock) makes sense when the thing you’re protecting isn’t a single DB row (cross-service/cross-resource coordination) — and it has edge cases around clock drift/timeouts. When a balance can already be protected by the DB’s transaction, don’t entrust it to a distributed lock.
Bottom line: use an atomic conditional UPDATE (or SELECT FOR UPDATE); save Redlock for non-DB resources. For money, keep the rule where the transaction guarantees it — in the DB; a distributed lock is too fragile a tool to protect an invariant the database itself could enforce.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.