0%
Redis Deep Dive: Encodings, Memory, and Failover
The Server Model
Data Structures Inside
Memory Management
Persistence
Replication and Cluster
Transactions, and What Atomic Actually Means Here
Redis has transactions, and they are not the transactions most engineers are thinking of. Understanding precisely what MULTI and EXEC promise, and what they do not, prevents a whole family of bugs.
What MULTI does
MULTI puts the connection into a queuing state. Every subsequent command is not executed but appended to a list held for that connection, with the server replying QUEUED to each one.
Nothing has happened to the data at this point, and nothing will until EXEC arrives. The queue lives on the server, attached to the connection, which means it survives between round trips but dies if the connection dies. That last point is worth holding onto: an abandoned MULTI costs nothing beyond a little memory and disappears when the client disconnects, so there is no transaction to time out or roll back.
What EXEC does
EXEC executes every queued command in order, as a single unit, with no other client's command interleaved. It returns an array of replies, one per queued command, in the order they were queued.
The single-threaded command execution covered in the first lessons is what makes this cheap. There is no lock to acquire and no coordination to perform, because the server already executes one command at a time; a transaction is simply a group of commands the server agrees to run consecutively. That is why EXEC has essentially no overhead beyond running the commands themselves.
So atomic means no interleaving
The guarantee is isolation in the narrow sense that no other client sees a partial state and no other client's commands run in the middle. It is not the guarantee that the group succeeds or fails as a whole.
That distinction is the entire lesson in one sentence. In a relational database, atomic means all or nothing, with a rollback available if something fails partway. In Redis, atomic means together and uninterrupted, with no rollback at all. Both are called atomicity, both appear in the documentation, and conflating them produces code that assumes a safety net that does not exist.
There is no rollback
If a queued command fails at execution time, for instance an INCR on a key holding a string that is not a number, that command returns an error and every other command in the transaction still executes. The transaction is not aborted and nothing is undone.
EXEC returns the array with an error in that position, and the client is expected to notice. Most client libraries surface this correctly and plenty of application code never checks, which is how a transaction that half worked becomes a silent data inconsistency. The commands before the failure ran, the commands after it ran, and the data is in a state the author did not intend.
Why there is no rollback
The Redis documentation is explicit about this and gives two reasons. A command fails only when it is used wrongly, which is a programming error that should be caught in development, and supporting rollback would add complexity to the fast path for no gain in normal operation.
Whether one agrees or not, the reasoning is coherent: Redis has no undo log because it has no log of before-images, and adding one would cost memory and time on every write to protect against a class of bug that correct code does not produce. The design trades a safety net for speed, consistently with every other choice in the system.
Two kinds of error, at two different times
A command that is syntactically invalid or unknown is rejected at queue time, with an error instead of QUEUED, and the server flags the transaction so that EXEC refuses and returns an error without running anything. A command that is well formed but wrong for its key's type fails at execution time, inside EXEC, and the rest still runs.
So there is a form of all-or-nothing, and it covers only the errors detectable before execution. This is the part that most surprises people: a typo in a command name aborts the whole transaction cleanly, while an INCR against a hash does not. The two cases need entirely different handling in client code.
DISCARD
DISCARD throws away the queue and returns the connection to its normal state, also clearing any keys being watched. It is the way to abandon a transaction deliberately, for instance when application logic decides partway through that the operation should not proceed.
It is also the correct thing to do in an error handler, because a connection left in a queuing state will append whatever is sent next to the pending transaction. A client library that returns a connection to a pool without issuing DISCARD after an exception produces one of the stranger bugs in this area: an unrelated later command becomes part of a transaction nobody intended.
You cannot read a value and use it inside a transaction
Every queued command returns QUEUED, not a value, so a GET inside a transaction gives the client nothing until EXEC returns. There is no way to branch on a read result inside the transaction, because the client is not running anything during the queuing phase.
This is the structural limitation that makes Redis transactions much less useful than they first appear. Anything of the form "read this, decide, then write" cannot be expressed as a transaction at all. The two answers are optimistic locking with WATCH, which the next section covers, and a server-side script, which is the subject of the next lesson.
Transactions and pipelining are different things
A pipeline sends several commands without waiting for each reply, which is a network optimisation and provides no atomicity: other clients' commands can and do run between pipelined commands. A transaction provides atomicity. Many client libraries expose both through a similar interface and some conflate them.
That conflation is worth checking in whatever library a team uses. A method called pipeline that does not send MULTI gives no isolation at all, and a method called transaction that is really a pipeline gives none either. The only reliable check is capturing the actual protocol traffic, with MONITOR on a test instance, and confirming that MULTI and EXEC appear.
How transactions are replicated and persisted
A transaction is propagated to replicas and written to the AOF wrapped in MULTI and EXEC, so a replica applies the whole group together and a recovering server replays it as a unit. A partially written transaction at the tail of an AOF is detected and the partial group is discarded.
So the atomicity does extend to replication and to recovery, which is a genuine guarantee and a useful one. A replica never shows a partial transaction, and a server recovering from an AOF never applies half of one. Those two properties are often what people actually need when they reach for a transaction.
What to take from MULTI and EXEC
Three things. Atomic here means together and uninterrupted, never all-or-nothing, and there is no rollback of any kind. Errors detectable at queue time abort the transaction, while errors detectable only at execution time do not. And no command's result is available until EXEC returns, so read-then-decide cannot be a transaction.
Redis uses the word atomic to mean that the group runs consecutively with nothing interleaved, not that it succeeds or fails as a whole. A queued INCR against a key holding a hash fails inside EXEC and every other command in the transaction still runs, with no rollback and no abort. Code written on the assumption of all-or-nothing semantics has a silent inconsistency waiting in it.