Database
NoSQL
Redis
Transactions & Signals

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 EXEC is 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

  1. Client: Sends MULTI.

  2. Server: Switches connection state to multi: true. Returns +OK.

  3. Client: Sends SET key1 value1.

  4. Server: Parser sees multi: true, appends command to the Connection Queue. Returns +QUEUED.

  5. Client: Sends EXEC.

  6. Server:

  • Iterates through the queue.
  • Executes each command sequentially on the data store.
  • Collects results in an Array.
  1. 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/EXEC across different shards (unless using Hash Tags).

7. Redis vs. Our Implementation: What we Simplified

  • WATCH/CAS: Redis uses "Optimistic Locking" with the WATCH command. If a key changes during the transaction, EXEC fails. We simplified this by only implementing basic MULTI/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-concurrency WATCH users might experience frequent "retry loops".

9. Edge Cases & Failure Scenarios

  • Memory Exhaustion (Queue Bloat): If a client sends 1 million commands in a MULTI block without calling EXEC, 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 HSET on 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 EXEC even runs.

  • WATCH Tracking: Redis maintains a watched_keys dictionary 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.