Redis path · Using it well · Topic 5 of 11

Atomic steps: MULTI/EXEC, WATCH and Lua

Not started

Every Redis command is atomic on its own, but most real operations need several: move money between two balances, take an item from stock only if some is left, release a lock only if you still own it. Done as separate commands from the client, other clients' commands can interleave. Redis offers three tools: MULTI/EXEC to run a batch without interruption, WATCH to abort the batch if something changed, and Lua scripts for logic that must read and decide on the server.

Why it matters: Read-modify-write from a client races with other clients; Redis gives you queued transactions, optimistic checks and server-side scripts to close the gap.

  1. 1MULTI/EXEC runs a batch without interruption

    Commands are queued, then executed one after another with no other client's command in between.

  2. 2No rollback

    If one queued command fails at run time, the others still run; Redis does not undo them.

  3. 3WATCH is optimistic locking

    If a watched key changes before EXEC, the transaction is not executed and EXEC returns nil, so the client retries.

  4. 4Lua scripts can decide

    A script runs atomically on the server and can read, branch and write, which MULTI cannot.

1

How it works

The explanation, with examples you can run.

MULTI and EXEC

MULTI starts queuing; each command replies QUEUED; EXEC runs them all in order, with no other client's command in between.

> SET balance:1 100OK> MULTIOK> DECRBY balance:1 30QUEUED> INCRBY balance:2 30QUEUED> EXEC1) (integer) 702) (integer) 30

There is no rollback. A command that fails when it runs fails alone, and the rest still apply (the OK and QUEUED replies are left out here):

> MULTI> SET k v> LPUSH k x> INCR counter> EXEC1) OK2) (error) WRONGTYPE Operation against a key holding the wrong kind of value3) (integer) 1

(Errors Redis can detect while queuing, such as a misspelt command, make EXEC discard the whole transaction.) The Redis docs explain this as a choice for simplicity and speed; runtime errors are usually programming errors that tests should catch. DISCARD abandons a queued transaction.

The bigger limit: queued commands have no results yet, so you cannot read a value and decide what to write inside MULTI.

WATCH: check-and-set

WATCH marks keys; if any of them changes before your EXEC, the transaction is not run and EXEC returns nil. Reading a value, then writing based on it, becomes safe with a retry loop:

# Client A                              # Client B> WATCH stock:sku1> GET stock:sku1"5"                                        > DECR stock:sku1                                        (integer) 4> MULTI> DECR stock:sku1QUEUED> EXEC(nil)                                   # A's transaction did not run; re-read and retry

This is optimistic: no one waits, but under heavy contention on one key many attempts fail and retry.

Lua scripts

A script runs on the server, atomically: while it runs, no other command executes. It can read, branch and write, so it handles cases MULTI cannot:

> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:invoice:7 3f9a1c(integer) 1

Pass every key the script touches in KEYS (not built from strings inside the script), so that Redis Cluster can route it and check that the keys share a slot. Load scripts once with SCRIPT LOAD and call them by hash with EVALSHA, or, since Redis 7, use functions (FUNCTION LOAD, FCALL), which are stored with the data and replicated.

Atomic also means blocking. A slow script stops every other client. After the busy threshold (5 seconds by default) Redis starts answering other clients with a BUSY error, and SCRIPT KILL can stop a script that has not yet written anything; one that has written can only be stopped by shutting the server down without saving. Keep scripts short and bounded.

Which to use

  • Several independent writes that should all happen together: MULTI/EXEC (or a pipeline wrapped in it).
  • Read, then write based on what you read, with little contention: WATCH with a retry loop.
  • Conditional logic, or heavy contention on one key: a Lua script or function.
2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.Inside MULTI you queue SET k v, LPUSH k x, INCR counter. LPUSH fails with WRONGTYPE. After EXEC, what is the state?
  2. 2.Client A runs WATCH stock, reads 5, then MULTI, DECR stock, EXEC. Between A's read and EXEC, client B decrements stock. What does A's EXEC return?
  3. 3.You need: if the lock key still holds my token, delete it. Why is GET followed by DEL from the client not safe?

0 of 3 right so far. Get all three to finish this topic.