redis/redis
Transactions
Active contributors: antirez, Oran Agra, debing.sun.
Purpose
Redis supports an optimistic-concurrency transaction model: a client queues commands inside a MULTI/EXEC block, optionally watches keys for changes, and the server executes the queued commands atomically when EXEC is sent. The implementation is src/multi.c.
This is not a SQL-style transaction. There is no rollback on logical error, no isolation level above what the single-threaded executor already provides, and no spanning across multiple databases.
Source layout
| File | Role |
|---|---|
src/multi.c |
MULTI, EXEC, DISCARD, WATCH, UNWATCH. The transaction state on client.mstate. |
src/server.c |
processCommand recognises queued commands and routes them. |
src/db.c |
signalModifiedKey interacts with watched keys. |
Commands
| Command | Effect |
|---|---|
MULTI |
Mark the connection as in transaction mode. Subsequent commands are queued instead of executed; the server replies +QUEUED to each. |
EXEC |
Execute the queued commands atomically. Returns an array of replies, one per queued command. |
DISCARD |
Drop the queue. |
WATCH key [...] |
Mark keys for optimistic concurrency. If any are modified before EXEC, the transaction aborts with a nil reply. Must be called before MULTI. |
UNWATCH |
Forget all watched keys. |
How EXEC works
- The client sends
MULTI.client.flagsgetsCLIENT_MULTI. - Subsequent commands are validated (parsed, ACL-checked, cluster-slot-checked, key positions extracted) but not run. They go into
client.mstate.commands. The server replies+QUEUEDper command. - If any queued command is invalid (unknown command, wrong arity, etc.), the transaction is marked dirty (
CLIENT_DIRTY_EXEC).EXECwill fail withEXECABORT. - The client sends
EXEC. The server iterates the queue, callingc->cmd->proc(c)for each command in order. It writes each reply into the multi-bulk array. - The atomicity guarantee comes from the single-threaded executor: between
EXECstarting and finishing, no other client runs commands.
Notably, a runtime error (e.g. INCR on a string) does not abort the rest of the transaction. The error reply is included in the array; subsequent commands still run.
WATCH and optimistic concurrency
WATCH key1 key2 ... registers each key with the client. If, before the client issues EXEC, any of those keys is modified by anyone (including by the watching client itself outside MULTI), the transaction is aborted at EXEC time with a nil reply. The client then re-reads, recomputes, and retries.
Implementation:
WATCHadds the key toclient.watched_keysand todb->watched_keys(adict<key, list<client*>>).- Any mutating command calls
signalModifiedKey(c, db, key), which iteratesdb->watched_keys[key]and setsCLIENT_DIRTY_CASon each subscriber. EXECchecksCLIENT_DIRTY_CASand aborts if set.
UNWATCH clears client.watched_keys and unlinks the client from db->watched_keys.
WATCH is also implicitly cleared when the client disconnects, when MULTI starts, when EXEC runs, when DISCARD runs.
Examples
Basic atomic increment:
MULTI
INCR counter
INCR counter
EXEC→ [1, 2].
Optimistic conditional update:
WATCH balance
val = GET balance
if val < amount: UNWATCH; reject
MULTI
DECRBY balance amount
EXECIf anyone changed balance between WATCH and EXEC, EXEC returns nil and the client retries.
Caveats
- Cluster. All watched and queued keys must hash to the same slot. Otherwise the validation step rejects the command with
-CROSSSLOT. - Scripts vs transactions. A Lua script (
EVAL) is implicitly atomic — there is no need to wrap a script inMULTI. In fact, scripts cannot startMULTI(they are executed inside an already-atomic context). WAITand replication.WAIT n timeoutblocks the client until at leastnreplicas have confirmed they received the prior writes. It is not a transaction primitive but is often paired withEXECto gate on replication.SUBSCRIBEmode. A client in subscriber mode cannot start a transaction.- Modules. Modules can opt their commands out of
MULTIwithCMD_NO_MULTI. This is rare; most module commands work in transactions.
Replication of transactions
The whole EXEC block is propagated as a single MULTI ... EXEC block on the replication channel. Replicas execute the contents atomically.
For AOF: same. MULTI and EXEC are appended around the queued commands so the replay path runs them together.
WAIT and WAITAOF
Adjacent to transactions:
WAIT n timeout— block until at leastnreplicas have acknowledged the offset where this command was issued, or the timeout elapses. Returns the actual count.WAITAOF local n timeout— the AOF version: wait untillocal(0/1) and at leastnreplicas have fsync'd the AOF up to this offset.
Both are implemented in src/replication.c rather than src/multi.c and use the blocked-client infrastructure (src/blocked.c) plus the per-replica offset tracker.
Where to start modifying
- Add a transaction-aware optimisation — search for
CLIENT_MULTIandCLIENT_DIRTY_*insrc/server.c. - Tweak watched-key tracking —
signalModifiedKeyandtouchWatchedKeyinsrc/multi.c. - Restrict a command in transactions — set the
CMD_NO_MULTIflag in its JSON spec.
Related pages
- Pub/Sub & tracking — keyspace notifications fire even within transactions.
- Scripting — Lua/
FCALLis the alternative atomic primitive. - Replication —
WAITandWAITAOF.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.