Skip to content
Navigation

Type at least two characters. Search covers page titles, headings, tags and database names.

↑ ↓ to navigateEnter to openEsc to close0 pages
Redis and Valkeyintermediate

Redis Transactions

What MULTI/EXEC guarantees, what it does not, and how WATCH provides optimistic concurrency.

2 min readIntermediateUpdated Edit this page

MULTI starts a command queue; EXEC runs the queued commands as one isolated unit. No other client's commands interleave.

MULTI
INCR shop:orders:count
LPUSH shop:orders:recent 4711
EXEC

What it guarantees

  • Isolation. The queued commands execute consecutively with nothing in between.
  • All-or-nothing execution. Either every command runs, or none does — if EXEC is never reached, or if a queued command was rejected at queue time.

What it does not guarantee

Errors detected at queue time (unknown command, wrong arity) abort the whole transaction at EXEC. Errors detected at execution time do not.

WATCH: optimistic concurrency

WATCH makes EXEC fail if a watched key changed since the watch began. This is the compare-and-set primitive Redis offers.

def transfer_credit(r, src, dst, amount):
    while True:
        try:
            with r.pipeline() as pipe:
                pipe.watch(src)
                balance = int(pipe.get(src) or 0)
                if balance < amount:
                    pipe.unwatch()
                    return False
                pipe.multi()
                pipe.decrby(src, amount)
                pipe.incrby(dst, amount)
                pipe.execute()          # raises WatchError if src changed
                return True
        except redis.WatchError:
            continue                    # someone else won; retry

The retry loop is mandatory. A WatchError is the protocol working, not an exceptional condition, and code that treats it as fatal will lose writes under contention.

When a Lua script is better

A Lua script executes atomically on the server, so it replaces the watch-retry cycle with a single round trip and no contention loop. Prefer it when the logic is small and self-contained:

EVAL "if tonumber(redis.call('GET', KEYS[1])) >= tonumber(ARGV[1]) then
        redis.call('DECRBY', KEYS[1], ARGV[1])
        redis.call('INCRBY', KEYS[2], ARGV[1])
        return 1
      else
        return 0
      end" 2 shop:credit:1001 shop:credit:1002 50

See Lua Scripts for the constraints that come with this.

In Redis Cluster

All keys in a transaction must map to the same hash slot, which in practice means using a hash tag:

{shop:user:1001}:credit
{shop:user:1001}:history

A transaction spanning slots is rejected. There is no cross-slot transaction in Redis Cluster.