The Problem
Redis transactions using MULTI/EXEC fail unexpectedly. Commands queued in a transaction abort, or EXEC returns null indicating the transaction was discarded. You see errors like:
(error) EXECABORT Transaction discarded because of previous errorsor EXEC returns (nil) when using WATCH, meaning the transaction was aborted due to key changes.
Understanding Redis Transactions
Redis transactions work differently from traditional database transactions:
- 1.MULTI - Start transaction, queue subsequent commands
- 2.Commands queued - Not executed immediately
- 3.EXEC - Execute all queued commands atomically
- 4.DISCARD - Cancel transaction without executing
Key differences from SQL transactions: - No rollback on command failure within EXEC - All commands execute even if one fails - Atomicity means no other commands execute between queued commands
Diagnosis Commands
Check Transaction Status
# No direct command to check transaction state
# Use CLIENT LIST to see clients in transaction
redis-cli CLIENT LIST | grep -E 'flags=.*M'The M flag indicates a client with an active MULTI transaction.
Debug Transaction Issues
redis-cli DEBUG SLEEP 1 # To see transaction state brieflyMonitor Commands
redis-cli MONITORWatch MULTI, queued commands, and EXEC/DISCARD in real-time.
Common Transaction Abort Scenarios
Scenario 1: Command Syntax Error Before EXEC
Problem: A command with syntax error is queued. EXEC aborts the entire transaction.
redis-cli
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 value1
QUEUED
127.0.0.1:6379> SET key2 # Missing value - syntax error
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errorsSolution: Validate commands before queuing or handle errors:
```bash # Use redis-cli --eval for script-based transactions # Or validate command syntax in application before sending
redis-cli MULTI OK redis-cli SET key1 value1 QUEUED redis-cli SET key2 value2 QUEUED redis-cli EXEC 1) OK 2) OK ```
Scenario 2: WATCH Key Modified by Another Client
Problem: Optimistic locking with WATCH fails when another client modifies the watched key.
```bash # Client 1 redis-cli WATCH counter OK redis-cli GET counter "10" redis-cli MULTI OK redis-cli SET counter 11 QUEUED
# Client 2 (while Client 1 is queuing) redis-cli SET counter 20 OK
# Client 1 redis-cli EXEC (nil) # Transaction aborted - counter was modified ```
Solution: Retry the transaction with updated value:
```python import redis
r = redis.Redis()
def increment_counter(key, max_retries=3): for attempt in range(max_retries): try: # Watch the key r.watch(key)
# Get current value current = r.get(key) if current is None: current = 0 else: current = int(current)
# Start transaction with r.pipeline() as pipe: pipe.multi() pipe.set(key, current + 1) pipe.execute() return current + 1
except redis.WatchError: # Key was modified, retry continue except Exception as e: r.unwatch() raise
raise Exception("Transaction failed after max retries") ```
Scenario 3: Type Mismatch in Transaction
Problem: Commands fail due to wrong key types, but EXEC still runs all commands.
redis-cli SET mylist "string_value" # Create string key
OK
redis-cli MULTI
OK
redis-cli LPUSH mylist element1 # Will fail - wrong type
QUEUED
redis-cli SET other_key value
QUEUED
redis-cli EXEC
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) OKNote: Unlike syntax errors, runtime errors don't abort the transaction. The SET still executes.
Solution: Check key types before transaction:
```bash redis-cli TYPE mylist # If returns "string", don't use list commands
# Or use TYPE check in transaction: redis-cli MULTI OK redis-cli TYPE mylist QUEUED redis-cli LPUSH mylist element1 QUEUED redis-cli EXEC # Check TYPE result before proceeding ```
Scenario 4: Transaction Timeout
Problem: Long transactions block other operations.
Diagnosis:
redis-cli CLIENT LIST | grep -E 'flags=.*M'
redis-cli CONFIG GET timeoutSolution: Keep transactions short:
```bash # Don't do slow operations in transaction redis-cli MULTI OK redis-cli KEYS * # DON'T - blocks and is slow QUEUED redis-cli EXEC
# Instead, use short atomic operations redis-cli MULTI OK redis-cli SET key1 value1 QUEUED redis-cli SET key2 value2 QUEUED redis-cli INCR counter QUEUED redis-cli EXEC ```
Transaction Pattern Best Practices
Pattern 1: Optimistic Locking with Retry
```python import redis import time
r = redis.Redis()
def safe_transfer(from_key, to_key, amount, max_retries=10): """ Safely transfer amount between keys using WATCH. """ for attempt in range(max_retries): try: r.watch(from_key, to_key)
from_balance = r.get(from_key) if from_balance is None: from_balance = 0 else: from_balance = int(from_balance)
if from_balance < amount: r.unwatch() raise ValueError("Insufficient balance")
to_balance = r.get(to_key) if to_balance is None: to_balance = 0 else: to_balance = int(to_balance)
with r.pipeline() as pipe: pipe.multi() pipe.set(from_key, from_balance - amount) pipe.set(to_key, to_balance + amount) pipe.execute() return True
except redis.WatchError: # Retry with backoff time.sleep(0.1 * attempt) continue
return False ```
Pattern 2: Atomic Multi-Key Operations
```bash # Without transaction - race condition possible redis-cli SET user:1:balance 100 redis-cli SET user:2:balance 200 redis-cli SET user:1:balance 50 # Could happen between above redis-cli SET user:2:balance 250
# With transaction - atomic redis-cli MULTI OK redis-cli SET user:1:balance 100 QUEUED redis-cli SET user:2:balance 200 QUEUED redis-cli EXEC 1) OK 2) OK ```
Pattern 3: Conditional Transactions
```lua -- Lua script for conditional operation (alternative to WATCH) local key = KEYS[1] local expected = ARGV[1] local new_value = ARGV[2]
local current = redis.call('GET', key) if current == expected then redis.call('SET', key, new_value) return 1 else return 0 end ```
redis-cli --eval conditional_set.lua key1 , expected_value new_valuePattern 4: Pipelining vs Transactions
```python # Pipeline without transaction - faster, no atomicity pipe = r.pipeline() pipe.set('key1', 'value1') pipe.set('key2', 'value2') pipe.set('key3', 'value3') results = pipe.execute()
# Pipeline with transaction - atomic, slower pipe = r.pipeline() pipe.multi() # Start transaction pipe.set('key1', 'value1') pipe.set('key2', 'value2') pipe.set('key3', 'value3') results = pipe.execute() ```
Debugging Transaction Issues
Step 1: Monitor in Real-Time
redis-cli MONITORSend transactions and observe: - MULTI command - Queued commands - EXEC or DISCARD - Results
Step 2: Check Client State
# Find clients with open transactions
redis-cli CLIENT LIST | awk '/flags=.*M/ {print}'Step 3: Kill Stuck Transactions
```bash # Find stuck client ID redis-cli CLIENT LIST | grep 'flags=.*M'
# Kill the client redis-cli CLIENT KILL ID <client_id> ```
Step 4: Validate Command Syntax
# Test command before using in transaction
redis-cli SET key value # If this works, will work in transaction
redis-cli LPUSH key value # Test type compatibilityCommon Pitfalls
- 1.Not handling WatchError - Application crashes on aborted transaction
- 2.Using KEYS in transaction - Extremely slow, blocks server
- 3.Not checking TYPE - Wrong type errors persist through EXEC
- 4.No retry logic - Single WATCH failure aborts entire operation
- 5.Too many commands - Long transactions increase collision chance
Verification
Test transaction behavior:
```bash # Test simple transaction redis-cli MULTI OK redis-cli SET test_key test_value QUEUED redis-cli INCR test_counter QUEUED redis-cli EXEC # Should return: 1) OK 2) (integer) 1
# Test WATCH abort redis-cli WATCH test_key OK redis-cli MULTI OK redis-cli SET test_key new_value QUEUED # From another terminal: redis-cli SET test_key modified redis-cli EXEC (nil) # Transaction aborted ```
Monitoring Transactions
```bash #!/bin/bash # Monitor for clients stuck in transactions
while true; do MULTI_CLIENTS=$(redis-cli CLIENT LIST | grep -c 'flags=.*M')
if [ "$MULTI_CLIENTS" -gt 5 ]; then echo "$(date): WARNING: $MULTI_CLIENTS clients in transaction" redis-cli CLIENT LIST | grep 'flags=.*M' fi
sleep 5 done ```