Atomic Operations & Distributed Locks

4.Nobody Sells the Last Item Twice

M

In this chapter

We'll fix a real race condition — two customers, one last unit — with DECR, a rate limiter, a distributed lock, and idempotent deduplication, all runnable in the Playground.

10–12 min

The Problem in Real Life

One unit of the fast charger is left. Two customers, on two different phones, both click "Buy Now" within the same second. Both requests check the stock count. Both see 1. Both proceed to check out.

Mike watches it happen in the order log and asks the question this whole chapter is really about.

M

They both saw 1. Which one of them is actually getting it?

Mike

Read-Then-Write vs. One Atomic Step

Read-then-write leaves a gap

Checking a value, then acting on it as a separate step, leaves room for a second request to read the same starting value.

DECR closes the gap

Reading and decreasing happen as one atomic step — two simultaneous DECRs never both see the same number.

SET...NX is an atomic lock

Acquiring a lock is the same single step as checking it's free — no separate check-then-claim race.

SADD's return value proves idempotency

1 means new, 0 means duplicate — the exact same request processed twice only ever counts once.

Atomic Operations & Distributed Locks

"Check the count, then decide, then update it" is three separate steps — and between step one and step three, another request can slip in and read the exact same starting number. Both customers' requests really did see 1, because both checked before either one had finished writing. This is a race condition, and it isn't specific to Redis — the fix is.

Two customers' requests can both run the GET before either one runs the SET — both see "1", both proceed. Nothing about this code is wrong, individually. The gap between the read and the write is the actual bug.

The Risky Way — Read, Then Decide, Then Write
GET stock:lst-fast-charger-20w
# sees "1" — looks safe to sell one
SET stock:lst-fast-charger-20w "0"

DECR reads and decreases a number in a single, indivisible step — there's no gap in the middle for a second request to sneak into. If two requests both call DECR on a stock count of 1, one of them gets back 0 (they got the last unit) and the other gets back -1 (nothing left) — never both seeing the same starting number.

The Atomic Fix — DECR in One Step
SET stock:lst-fast-charger-20w "1"
DECR stock:lst-fast-charger-20w

A counter plus a TTL is the classic Redis rate limit: count requests in a window, block once the count passes a threshold. The EXPIRE only needs to run on the first request of a new window — checking whether INCR's own result was 1 is how a real implementation knows this is a fresh window and not one already ticking down.

Rate Limiting — Counting Within a Window
INCR ratelimit:u-501:checkout
# if this INCR's result was 1, this is the first request in the window:
EXPIRE ratelimit:u-501:checkout 60

Setting the expiry on every request, instead of only the first, would silently keep pushing the window back forever — a real, common mistake in a naive rate limiter.

NX means "only set if this key doesn't already exist." The first SET succeeds and IS the lock being acquired — one atomic step, not a separate "check if it's free" read followed by a "claim it" write, which would have the exact same race-condition gap as the stock counter above. The second SET, from a different worker, fails outright — it never got the lock, and never got to touch the checkout at all. EX 30 guarantees the lock releases itself after 30 seconds even if the worker holding it crashes.

Distributed Lock — One Winner, Atomically
SET lock:checkout-lst-fast-charger-20w "worker-A" NX EX 30
SET lock:checkout-lst-fast-charger-20w "worker-B" NX EX 30

SADD's own return value already answers "was this new?" — 1 means the value was actually added (first time seeing this request); 0 means it was already there (a duplicate, safe to skip). A network retry sending the exact same request ID twice gets processed exactly once, not twice — the same idempotency guarantee a well-designed API endpoint gives, built here from one Set and one command.

Deduplication — Have We Already Processed This?
SADD processed:requests "req-checkout-9f21"
SADD processed:requests "req-checkout-9f21"

Every one of these — DECR, the rate-limit counter, the lock, the dedup check — is the same underlying idea applied to a different problem: do the check and the action as one atomic step, so there's never a gap for a second request to land in. Redis earns the name atomic operations here specifically because a single command like DECR or SET ... NX can never be interrupted halfway through by another client's command.

Key Takeaway

A race condition isn't bad code — it's a gap between checking and acting that a second request can slip into. Redis's atomic operations close that gap by making the check and the action one single, uninterruptible step.

Why This Matters

This is the concrete, hands-on version of a lesson this course has been building toward since Act 1's CAP theorem chapter — a system under real concurrent load needs more than "it usually works." DECR, SET...NX, and SADD's return value are three small, genuinely different tools solving the same underlying problem: making sure two requests racing each other can never both win.

GreenMart's flash sale can no longer sell the same last unit twice. The next problem isn't about one request racing another — it's about telling every open tab and connected device about a change the instant it happens, all at once.

Next