Redis is single-threaded for command execution, which provides atomicity at the command level. However, when multiple commands are executed together, or when multiple clients perform operations simultaneously, race conditions may occur. Redis provides transactions and the WATCH
command to handle these cases safely.
1. Single-Threaded Execution in Redis
- Redis executes commands sequentially in a single main thread.
- No need for locks or synchronization for individual commands.
- Example: Transferring money between users without transactions can produce incorrect results due to concurrent operations.
Example Without Transactions
Two keys in Redis:
user:1:balance = 1 user:2:balance = 0
Objective: Transfer 1 unit from user:1
to user:2
.
If two clients execute commands concurrently:
- Client A reads
user:1:balance = 1
- Client B reads
user:1:balance = 1
- Client A decrements →
user:1:balance = 0
- Client B decrements →
user:1:balance = -1
- Client A increments →
user:2:balance = 1
- Client B increments →
user:2:balance = 2
Result: user:1:balance = -1
, user:2:balance = 2
→ incorrect.
2. Simulating Multiple Clients in Redis
To simulate concurrency:
- Open two terminals and connect both to Redis.
- Flush the database in both terminals:
FLUSHDB
- Initialize balances:
SET user:1:balance 1 SET user:2:balance 0
- Both terminals attempt to transfer money at the same time using
DECR
andINCR
commands.
Without transactions, you can observe the race condition, leading to negative balances or inconsistent results.
3. Using Transactions with MULTI
and EXEC
Redis transactions allow you to execute multiple commands atomically.
- Start transaction mode:
MULTI
- Queue commands:
DECR user:1:balance INCR user:2:balance
- Execute all commands as a single block:
EXEC
- All queued commands are executed together.
- Results are returned for each command.
- If all commands succeed, changes are committed.
- If a command fails, the entire transaction is rolled back.
Example With Two Terminals
Terminal 1:
MULTI DECR user:1:balance INCR user:2:balance
Terminal 2:
Simultaneously issues the same transaction.
When EXEC
is called:
- Terminal 1 executes successfully.
- Terminal 2 may fail if the relevant keys were modified by Terminal 1.
- Redis ensures atomicity and consistency.
4. Using WATCH
to Prevent Conflicts
WATCH
monitors keys for changes before executing a transaction.- If a watched key is modified by another client, the transaction fails automatically.
Example:
WATCH user:1:balance user:2:balance MULTI DECR user:1:balance INCR user:2:balance EXEC
- If another client modifies
user:1:balance
during this transaction,EXEC
returnsnil
, preventing inconsistent updates. - After
EXEC
, the watch is automatically removed. - If you want to abort a transaction manually, use:
DISCARD
This cancels all queued commands in the current transaction.
5. Key Points
- Redis executes commands sequentially in a single thread, making individual operations atomic.
- Multiple related commands require transactions (
MULTI
+EXEC
) to ensure correctness. - Use
WATCH
when multiple clients may modify the same keys simultaneously. - Transactions ensure all-or-nothing execution, crucial for operations like:
- Money transfers
- Inventory updates
- Booking systems
- DISCARD allows rolling back queued commands before execution if needed.
6. Summary
Using transactions and WATCH in Redis:
- Prevents race conditions.
- Ensures atomic execution of multiple commands.
- Automatically handles conflicts between concurrent clients.
- Removes the need for complex locking mechanisms in your application.