Race conditions and locks
Not climbed yetModel answer
A race condition is a bug where the result depends on the timing of two things running at once.
count++looks like one step but is three: readcount, add 1, write it back. Two threads both read 5, both compute 6, both write 6. Two increments, one counted. Run it a million times on two threads and the total comes out short, by a different amount each run.Races are hard to find because they only show up under load or on certain timings, and adding logging can make them disappear.
Model answer
A lock (mutex,
synchronized) makes the read-add-write a critical section that one thread enters at a time.An atomic operation does the read-modify-write as one indivisible CPU instruction (compare-and-swap):
AtomicInteger.incrementAndGet()in Java,atomic.AddInt64in Go.For a single counter, choose the atomic: no blocking, no risk of forgetting to unlock, faster under contention. Use a lock when several values must change together (move money between two balances), which one atomic cannot cover.
Model answer
Each server has its own memory, so its lock only protects its own threads. Move the atomicity to the shared store:
- An atomic update in SQL:
UPDATE counters SET value = value + 1 WHERE id = ?. The database serialises it. Simplest, and usually enough. - Optimistic locking: read the row with its
version, writeWHERE version = ?, retry if nothing changed. Good when conflicts are rare. - Pessimistic locking:
SELECT ... FOR UPDATEinside a transaction when the logic between read and write is complex. - Redis
INCRwhen the counter is hot and does not need to live in the database.
For very hot counters (likes, views), keep several sharded counter rows and sum them on read, so writes do not all fight over one row.
- An atomic update in SQL:
Model answer
- The holder pauses past the expiry: a long GC pause or slow network makes the job run longer than 30 seconds, the lock expires, a second worker takes it, and now two run at once. Nothing on the first worker notices.
- Deleting someone else's lock: worker A finishes late and runs
DEL, removing the lock worker B now holds. Store a random token as the value and delete only if it still matches, in one Lua script. - Failover: the Redis primary acknowledges the lock and dies before replicating it; the new primary has no lock.
If correctness depends on the lock, use fencing tokens: the lock service hands out an increasing number, the worker sends it with every write, and the storage rejects writes with a number lower than one it has seen. Or let the database do the locking (a row lock, Postgres advisory locks), which is where the data lives anyway.
A Redis lock is fine for efficiency ("avoid doing the work twice"), not for correctness ("must never happen twice").
