builds / redis / stage-4SHEET 4 / 6 · REV ASIGN IN
ASSEMBLY DIAGRAM — YOUR REDISSCALE: LEARNING
clientthe outside worldPORT JACK✓ builtRESP CODEC✓ builtMEMORY BANK✓ builtINTERLOCK⚙ buildingCLOCK UNITstage 5FLIGHT RECORDERstage 6
BUILTUNDER CONSTRUCTIONNOT YET IMAGINED INTO EXISTENCE
STAGE 4 · THE CONCURRENCY INTERLOCK

Many Hands, One Ledger

Real servers don't take turns.

What you're wiring up

This stage flips the accept loop to go handle(conn) — one goroutine per client — and immediately breaks the Stage 3 machine: two clients writing the same map is a data race, and Go's race detector will literally name the two lines responsible. The fix is the interlock: a sync.RWMutex so readers don't queue behind readers.

The proof that locking is about atomicity, not just crash avoidance, is INCR: read-modify-write must be one indivisible act, or two clients incrementing 1000 times each land short of 2000. This is DDIA's race conditions made personal.

Assembly steps

[ 01 ]
Spawn a goroutine per connection. Re-run the Stage 3 checks with -race — watch it burn.
hint

The race detector's stack trace names the exact two lines touching the map. Reading it is the skill.

[ 02 ]
Guard the store with sync.RWMutex: RLock for GET/EXISTS, Lock for SET/DEL. Critical sections tiny — never hold a lock around a socket write.
hint

defer mu.Unlock() inside small store methods keeps lock scope honest; holding a lock while writing to a slow client is how real outages happen.

[ 03 ]
Implement INCR/DECR: missing key counts as 0; non-numeric → -ERR value is not an integer. Read-modify-write under ONE lock hold.
hint

Get-then-Set as two locked calls loses updates — the hammer test will catch it. One method, one lock hold.

[ 04 ]
Per-connection state (bufio.Reader, reply buffer) must be created inside the handler goroutine.
hint

Shared parser state is the sneaky second race.

Go deeper (after it passes)

Pairs with Clean Code M10 — Clean Concurrency and DDIA M7 — Transactions Without Tears.