Open-Source Wikis

/

Redis

/

Features

/

Transactions

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

  1. The client sends MULTI. client.flags gets CLIENT_MULTI.
  2. 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 +QUEUED per command.
  3. If any queued command is invalid (unknown command, wrong arity, etc.), the transaction is marked dirty (CLIENT_DIRTY_EXEC). EXEC will fail with EXECABORT.
  4. The client sends EXEC. The server iterates the queue, calling c->cmd->proc(c) for each command in order. It writes each reply into the multi-bulk array.
  5. The atomicity guarantee comes from the single-threaded executor: between EXEC starting 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:

  • WATCH adds the key to client.watched_keys and to db->watched_keys (a dict<key, list<client*>>).
  • Any mutating command calls signalModifiedKey(c, db, key), which iterates db->watched_keys[key] and sets CLIENT_DIRTY_CAS on each subscriber.
  • EXEC checks CLIENT_DIRTY_CAS and 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
EXEC

If 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 in MULTI. In fact, scripts cannot start MULTI (they are executed inside an already-atomic context).
  • WAIT and replication. WAIT n timeout blocks the client until at least n replicas have confirmed they received the prior writes. It is not a transaction primitive but is often paired with EXEC to gate on replication.
  • SUBSCRIBE mode. A client in subscriber mode cannot start a transaction.
  • Modules. Modules can opt their commands out of MULTI with CMD_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 least n replicas 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 until local (0/1) and at least n replicas 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_MULTI and CLIENT_DIRTY_* in src/server.c.
  • Tweak watched-key trackingsignalModifiedKey and touchWatchedKey in src/multi.c.
  • Restrict a command in transactions — set the CMD_NO_MULTI flag in its JSON spec.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Transactions – Redis wiki | Factory