Redis Transactions (MULTI)
Learn how to use Redis MULTI to execute a batch of commands atomically and consistently, with rollback on syntax errors and practical exercises.
Focus: Redis transactions MULTI
Imagine you're processing a payment: you need to deduct funds from one account and credit another. If the first command succeeds but the second fails, you've just lost money. Without atomicity, your data corrupts silently. Redis transactions with MULTI solve this by batching commands into an all-or-nothing sequence, ensuring consistency even under concurrent load.
The problem this lesson solves
Redis is fast, but its single-threaded nature doesn't guarantee atomicity across multiple operations. Consider a stock inventory system:
- Check stock level (
GET) - Decrement stock (
DECR) - Record order in a log (
LPUSH)
If step 2 executes while another process reads stale data between steps 1 and 2, you'll oversell. Redis transactions wrap these three commands so they execute as a single isolated unit. No other client can interleave commands inside the transaction.
Without transactions, race conditions corrupt leaderboards, inventory, and financial balances. MULTI is the simplest way to enforce atomicity without migrating to Lua scripting.
Pro tip: Use transactions whenever you need a short-lived batch of commands to execute without interference. For long-running scripts, consider a Lua script instead (see 'Compare options').
Core concept / mental model
Think of MULTI as opening a baker's queue. You place orders (commands) one by one, but the baker doesn't start baking until you say EXEC. If you change your mind, DISCARD throws away the entire order. The key properties:
- Atomic: All commands run or none do. If Redis crashes before
EXEC, nothing runs. - Isolated: No other client sees intermediate states while the transaction is queued.
- No rollback: If a command fails after
EXEC(e.g., runtime error), the other commands still execute. Only syntax errors block the entire transaction.
Terminology
| Term | Meaning |
|---|---|
MULTI |
Begins a transaction block |
EXEC |
Executes all queued commands atomically |
DISCARD |
Cancels the transaction, discards queue |
WATCH |
Optimistic locking — abort if watched keys change (advanced) |
How it works step by step
- Start a transaction: Client sends
MULTI. Redis switches to 'transactional mode'. - Queue commands: Each subsequent command (
SET,LPUSH, etc.) is queued, not executed. Redis replies withQUEUED. - Commit or discard: Send
EXECto run all commands atomically, orDISCARDto cancel. - No syntactic errors: If any queued command has a syntax error (e.g., wrong number of arguments),
EXECreturns an error and no commands run. - Runtime errors allowed: If a queued command fails at runtime (e.g.,
INCRon a string), the other commands still run. Redis does not roll back.
This design means transactions are lightweight but limited: they don't provide the ACID rollback of a relational database. They guarantee atomic execution, not atomic failure.
Mental model: Imagine you write cooking steps on sticky notes.
MULTIcollects them.EXECcooks them all simultaneously. If one dish burns, the others still cook.
Hands-on walkthrough
Example 1: Basic atomic batch
Transfer balance between two user accounts atomically.
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
r.set('user:1000:balance', 100)
r.set('user:2000:balance', 50)
pipe = r.pipeline(transaction=True) # uses MULTI/EXEC internally
pipe.decrby('user:1000:balance', 30)
pipe.incrby('user:2000:balance', 30)
results = pipe.execute()
print(results) # [amount_after_decrement, amount_after_increment]
print(r.get('user:1000:balance')) # b'70'
print(r.get('user:2000:balance')) # b'80'
Expected output:
[30, 130]
b'70'
b'80'
The pipeline object with transaction=True wraps commands in MULTI...EXEC automatically.
Example 2: Manual MULTI with DISCARD
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 value1
QUEUED
127.0.0.1:6379> GET key1
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> EXISTS key1
(integer) 0
Notice GET is also queued. The transaction discarded, so key1 was never set.
Example 3: Error handling — syntax vs. runtime
Syntax error blocks the whole transaction:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key2 value2
QUEUED
127.0.0.1:6379> SETEX key3 # missing arguments
(error) ERR wrong number of arguments for 'setex' command
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> EXISTS key2
(integer) 0
Runtime error does NOT block others:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key4 "hello"
QUEUED
127.0.0.1:6379> INCR key4 # can't increment a string
QUEUED
127.0.0.1:6379> SET key5 "world"
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
127.0.0.1:6379> GET key4
"hello"
127.0.0.1:6379> GET key5
"world"
Key4 and Key5 are set even though INCR failed.
Example 4: Python with WATCH for optimistic locking
import redis
r = redis.Redis()
r.set('seat:42:booked', 0)
def book_seat(seat_key):
with r.pipeline() as pipe:
while True:
try:
pipe.watch(seat_key) # watch for changes
current = int(pipe.get(seat_key) or 0)
if current == 1:
print("Seat already booked")
return False
pipe.multi()
pipe.set(seat_key, 1)
pipe.execute()
print("Booked successfully")
return True
except redis.WatchError:
continue
book_seat('seat:42:booked')
If another process modifies the key between WATCH and EXEC, the transaction aborts and retries.
Compare options / when to choose what
| Approach | When to use | Pros | Cons |
|---|---|---|---|
MULTI / EXEC |
Short atomic batches (under 100 commands) | Simple, no script overhead | No rollback; no conditions |
WATCH + MULTI |
Optimistic locking (e.g., reservation systems) | Detects conflicts | Requires retry logic |
Lua scripting (EVAL) |
Complex logic or conditional branching | Full rollback on error; can branch | Steeper learning curve; harder to debug |
| Redis Streams | Event sourcing or ordered message queues | Built-in consumer groups | Overkill for atomic operations |
Guideline: Use MULTI for straightforward batch operations. Switch to Lua if you need to check conditions inside the transaction. Use WATCH only when you must prevent concurrent modifications.
Troubleshooting & edge cases
Common mistakes
-
Assuming rollback on runtime errors. Remember: Redis queues commands, then executes them sequentially. If one fails (e.g., wrong type), later commands still run. Use Lua if you need full rollback.
-
Forgetting to call EXEC. If you forget
EXEC, the transaction stays open. Redis will eventually close it but this wastes resources. Always pairMULTIwithEXECorDISCARD. -
Using MULTI inside a loop incorrectly. Queuing thousands of commands in one transaction can block Redis. Keep transactions small (under 100 commands).
-
Assuming WATCH protects all keys.
WATCHonly watches keys you explicitly call. If you modify an unwatched key inside the transaction, it won't trigger an abort.
Edge case: Transaction with no commands
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> EXEC
(empty array)
This returns an empty reply but doesn't error. It's harmless but pointless.
Edge case: MULTI inside MULTI
If you call MULTI while already in a transaction, Redis returns an error. The transaction stays active. Always check for successful QUEUED replies.
What you learned & what's next
You now understand how to use Redis transactions (MULTI, EXEC, DISCARD) to execute atomic batches, handle syntax vs. runtime errors, and apply optimistic locking with WATCH. You've seen practical examples in Python and CLI.
Key takeaways:
- MULTI opens a transaction, EXEC runs it atomically.
- Syntax errors abort the whole transaction; runtime errors do not.
- Use WATCH for conditional atomicity.
- Transactions are great for short batch operations, not complex branching.
Next lesson: Redis Optimistic Locking with WATCH — deep dive into concurrency control for high-write systems.
Pro tip: In production, always set reasonable timeouts for transactions to prevent hanging clients. Use
CLIENT LISTto monitor open transactions.
Practice recap
Try writing a Python script that simulates a hotel booking system. Use MULTI/EXEC to atomically decrement available rooms and record a booking. Then add WATCH to prevent double-booking under concurrent requests. Test by running two clients simultaneously.
Common mistakes
- Assuming runtime errors cause rollback — they don't; all other commands still run.
- Forgetting to call EXEC, leaving the transaction open and wasting server resources.
- Using MULTI inside a loop to queue hundreds of commands, blocking Redis for seconds.
- Thinking WATCH protects all keys automatically — you must explicitly WATCH each key you care about.
Variations
- Use Lua scripting (EVAL) for complex logic or conditional rollback inside an atomic block.
- The WATCH command adds optimistic locking to retry transactions when watched keys change.
- For very simple atomic increments, use INCR or other single commands that are already atomic.
Real-world use cases
- E-commerce: atomically decrement stock and record order to avoid overselling under high concurrency.
- Gaming: atomic leaderboard update (increment score and push to sorted set) without race conditions.
- Banking: transfer funds between accounts in one atomic operation with full isolation.
Key takeaways
- MULTI opens a transaction; EXEC runs all queued commands atomically.
- Syntax errors before EXEC abort the entire transaction; runtime errors during EXEC do not.
- Use DISCARD to cancel a transaction cleanly without running any queued commands.
- WATCH provides optimistic locking — retry by catching WatchError in Python if keys change.
- Keep transactions short (under 100 commands) to avoid blocking Redis.
- For rollback or conditional logic, prefer Lua scripting over MULTI.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.