Redis · lesson 5 of 14
Beginner
4Intermediate
7- 5 MULTI/EXEC Transactions and the WATCH-Based Optimistic Lock
- 6 Pub/Sub: Channels, Patterns, and the Message That Never Arrives
- 7 RDB vs AOF: What Survives a Hard Kill and What Doesn't
- 8 Replication: A Read-Only Replica, and What WAIT Actually Guarantees
- 9 ACL: a User Restricted to One Key Pattern and One Command
- 12 maxmemory and Eviction: What Redis Throws Away When It's Full
- 13 Cache-Aside with Redis: Stale Reads, Races and Stampedes
Advanced
3Also part of Redis
3MULTI/EXEC Transactions and the WATCH-Based Optimistic Lock
Queue commands with MULTI, run them atomically with EXEC — then watch a real transaction abort mid-flight when another client changes a WATCHed key first.
By SkillByExample · Published
MULTI and EXEC make several commands run as one atomic unit — and WATCH is what happens
when a transaction’s logic depends on a value that might not be there anymore by the time it
runs.
MULTI and EXEC: queue, then run atomically
printf "MULTI\nSET balance 100\nINCRBY balance 50\nEXEC\n" | docker exec -i redis-lesson redis-cli
OK
QUEUED
QUEUED
OK
150
MULTI starts queuing — each command after it returns QUEUED instead of actually running.
EXEC runs the whole queue back-to-back, and the results (OK from SET, 150 from
INCRBY) come back together, in order. No other client’s command can interleave between the
two queued commands once EXEC starts.
The bug: a transaction that aborts mid-flight
WATCH ties a transaction to a key’s state. Start one that reads a balance, then pauses
before committing an update — and have another client change the balance in that window:
docker exec redis-lesson redis-cli SET balance 100
# "Session A" watches balance, then pauses before committing
(printf "WATCH balance\n"; sleep 2; printf "MULTI\nINCRBY balance 10\nEXEC\n") \
| docker exec -i redis-lesson redis-cli &
sleep 1
# "Session B" changes the balance while session A is paused
docker exec redis-lesson redis-cli SET balance 999
wait
OK
OK
QUEUED
Read this closely: WATCH balance returns OK, MULTI returns OK, INCRBY balance 10
returns QUEUED — and then EXEC returns nothing at all. Session A’s transaction never
ran. INCRBY was never applied, even though it was successfully queued.
docker exec redis-lesson redis-cli GET balance
999
The balance is 999 — session B’s write, untouched by session A’s +10. If session A’s
transaction had actually run, the balance would be 1009. WATCH noticed the key changed
between the watch and the EXEC, and refused to run the transaction at all rather than run it
against a balance that might already be stale.
The same transaction, uncontested
docker exec redis-lesson redis-cli SET balance 100
printf "WATCH balance\nMULTI\nINCRBY balance 10\nEXEC\n" | docker exec -i redis-lesson redis-cli
docker exec redis-lesson redis-cli GET balance
OK
OK
OK
QUEUED
110
110
No other client touched balance in between this time, so EXEC actually runs and returns
110 — the increment applied cleanly. Same code, same commands; the only difference is
whether something else got there first. Production code using this pattern checks whether
EXEC came back empty and, if so, re-reads the value and retries the whole sequence.
Practice
1. Queue multiple unrelated commands in one transaction and confirm they all ran.
printf "MULTI\nSET a 1\nSET b 2\nSET c 3\nEXEC\n" | docker exec -i redis-lesson redis-cli
docker exec redis-lesson redis-cli MGET a b c
OK
QUEUED
QUEUED
QUEUED
OK
OK
OK
1
2
3
Three independent SETs, no shared state between them — a plain MULTI/EXEC batch with no
need for WATCH, since nothing here depends on a value read beforehand.
2. Use DISCARD to cancel a queued transaction before it runs.
printf "MULTI\nSET temp 1\nDISCARD\n" | docker exec -i redis-lesson redis-cli
docker exec redis-lesson redis-cli EXISTS temp
OK
QUEUED
OK
0
DISCARD throws away everything queued since MULTI without running any of it — temp was
never actually set, confirmed by EXISTS returning 0.
3. Watch a key that never changes, and confirm the transaction runs normally.
docker exec redis-lesson redis-cli SET stable "unchanged"
printf "WATCH stable\nMULTI\nGET stable\nEXEC\n" | docker exec -i redis-lesson redis-cli
OK
OK
OK
QUEUED
unchanged
WATCH only aborts the transaction if the watched key actually changes — watching a key that
stays untouched costs nothing and the transaction commits normally, exactly like not watching
at all.
Next: Pub/Sub — channels, pattern subscriptions, and sharded Pub/Sub.