Skip to content
Muhammet Şafak
tr
Asked by: Polat Answered:

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.

  1. Best: an atomic conditional UPDATE. Write UPDATE 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 the balance >= 40 condition. If zero rows are affected, the operation failed on insufficient funds.
  2. Alternative: SELECT ... FOR UPDATE. If your logic between read and write is complex, lock the balance row inside a transaction with FOR UPDATE. The second worker that reaches that row waits for the first to commit; they serialize, no clash. That’s pessimistic locking.
  3. Optimistic locking works under low contention too. Add a version column to the row; on update include WHERE version = ? and retry if it doesn’t match. Cheap when conflicts are rare, but it creates a retry storm under heavy contention.
  4. 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

Tags: #data#queue#redis
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind