Transactions & Signals: Atomic Guarantees
In a database, consistency is king. If you need to deduct 10 units from Alice and add 10 to Bob, you cannot afford for only one of those to happen. This is where Transactions come in. To wrap up our server, we'll also implement Graceful Shutdowns to ensure our data is safe when the service stops.
1. Intuition: The "Sealed Envelope"
Why this exists in Redis
Since Redis is single-threaded, it's already "atomic" at the command level. But between two different commands, another client could sneak in and change the state. A transaction ensures that a block of commands runs without any interference.
Real-world problem it solves: Preventing race conditions when multiple steps depend on each other (e.g., banking transfers, inventory updates).
The Analogy (The Letter Box)
A transaction is like putting several letters into a single envelope before dropping it into the mail. The mailman (Redis) can't process them one by one; they must open the envelope and process everything inside all at once.
2. Internal Architecture
Components Involved
- Command Queue: A per-client list that stores commands until
EXECis called. - Connection State: A flag (
isMulti) that tells the parser whether to execute immediately or queue.
How Redis designs this feature
Redis transactions are Isolated. Once EXEC is called, no other client's command can be interleaved. However, Redis does not support Rollbacks.
Trade-offs: Performance vs. ACID
- Pros: Extremely fast. Atomic at the command block level.
- Cons: No Rollbacks. If command #2 fails, #3 still runs. This avoids the overhead of "Undo Logs" and snapshots, which would slow Redis down.
3. End-to-End Flow (VERY IMPORTANT)
Client → TCP → Event Loop → Command Queue → Parser → Execution → Data Store → Response
-
Client: Sends
MULTI. -
Server: Switches connection state to
multi: true. Returns+OK. -
Client: Sends
SET key1 value1. -
Server: Parser sees
multi: true, appends command to the Connection Queue. Returns+QUEUED. -
Client: Sends
EXEC. -
Server:
- Iterates through the queue.
- Executes each command sequentially on the data store.
- Collects results in an Array.
- Response: Returns a RESP Array of results.
4. Internals Breakdown
Optimistic Locking (WATCH)
Redis also uses WATCH, which implements Optimistic Locking. If a "watched" key changes before you call EXEC, the entire transaction fails. This is how Redis prevents "Lost Updates" without using heavy locks.
OS Signals
When you run docker stop, the OS sends a SIGTERM. A well-behaved database must catch this, flush all AOF buffers, and close sockets safely.
5. Node.js Reimplementation (Hands-on)
Step 1: The Connection State
class Connection {
constructor(socket) {
this.socket = socket;
this.isMulti = false;
this.queue = [];
}
}Step 2: The Command Pipeline (Queuing Logic)
function handleCommand(conn, command) {
if (command[0].toUpperCase() === 'MULTI') {
conn.isMulti = true;
return '+OK\r\n';
}
if (command[0].toUpperCase() === 'EXEC') {
const results = conn.queue.map(cmd => execute(cmd));
conn.isMulti = false;
conn.queue = [];
return formatAsRESPArray(results);
}
if (conn.isMulti) {
conn.queue.push(command);
return '+QUEUED\r\n';
}
return execute(command);
}Step 3: Graceful Shutdown
process.on('SIGTERM', async () => {
console.log('Shutting down...');
server.close(); // Stop new connections
// Final persistence flush
await flushAOFToDisk();
console.log('Safe exit.');
process.exit(0);
});6. Performance, High Concurrency & Backpressure
High Concurrency Behavior
Redis transactions are "Lock-Free". Because the entire transaction runs on a single thread, it is natively atomic. This allows for massively high concurrency without the overhead of row-level or table-level locks.
Bottlenecks & Scaling Limitations
- Bottleneck: Execution Time. If your transaction has 10,000 commands, every other client is blocked for the duration of that entire block.
- Scaling: Transactions are limited to a single node. In a Redis Cluster, you cannot run
MULTI/EXECacross different shards (unless using Hash Tags).
7. Redis vs. Our Implementation: What we Simplified
- WATCH/CAS: Redis uses "Optimistic Locking" with the
WATCHcommand. If a key changes during the transaction,EXECfails. We simplified this by only implementing basicMULTI/EXEC. - Memory Pipelining: Redis can parse multiple transactions in one network read. Our Node implementation processes them sequentially.
8. Why Redis is Optimized
Redis is optimized for Command Isolation. By buffering commands in memory and only executing them once the "Seal" (EXEC) is broken, it ensures that your multi-step operation is either entirely separated or entirely atomized.
- WATCH Race Condition: If a key is modified by another client 1 microsecond before
EXEC, the transaction fails. High-concurrencyWATCHusers might experience frequent "retry loops".
9. Edge Cases & Failure Scenarios
-
Memory Exhaustion (Queue Bloat): If a client sends 1 million commands in a
MULTIblock without callingEXEC, the server's RAM could explode because the queue grows unchecked. Redis does not limit the transaction queue size by default. -
Partial Execution Failure: If a command fails execution (e.g., calling
HSETon a string), the transaction still continues. There is no rollback. This can leave your application in an inconsistent state if you don't handle errors per result. -
Error Detection: Real Redis detects "syntax errors" (wrong number of arguments) during the queuing phase and will fail the whole transaction before
EXECeven runs. -
WATCH Tracking: Redis maintains a
watched_keysdictionary in the server state to track which clients are watching which keys.
8. Summary & Key Takeaways
- MULTI/EXEC provide total isolation.
- No Rollbacks keeps Redis fast.
- Signal Handling is the difference between a toy and a production system.
Next Step: We've built the engine. Now let's explore the Core Data Structures that make Redis the Swiss Army Knife of databases.