Chapter 13 · Transactions, WATCH, Optimistic Locking, and Atomicity

Atomic Single Commands vs Transactions vs Functions/Scripts: Choose the Simplest Correct Primitive

Choose the smallest Redis correctness primitive that satisfies the invariant, from one atomic command through MULTI/EXEC and WATCH to server-side scripts/functions.

Advanced170–220 minutesTransactions and concurrencyRedis Open Source 8.10.1Free/local-firstLast reviewed: September 6, 2026

Learning outcomes

AtlasMart has accumulated several coordination needs: increment a counter, initialize multiple keys only if absent, update one String only if its exact prior value still matches, reserve inventory based on a read, and apply multi-key logic. Using MULTI/EXEC for all of them adds round trips and failure modes without adding correctness. This lesson builds a decision framework.

01

Prefer one atomic Redis command when it already expresses the invariant.

02

Distinguish unconditional MULTI/EXEC grouping from conditional WATCH-based concurrency control.

03

Use Redis 8.4+ conditional SET/DELEX only when their exact String comparison semantics fit.

04

Identify when scripts/functions reduce race windows but increase server-side blocking/versioning responsibility.

05

Keep Cluster slot locality, ACLs, persistence, external side effects, and observability in the primitive choice.

Exact lab baseline

All Chapter 13 mandatory labs reuse the disposable Chapter 01 environment: Redis Open Source 8.10.1 from Docker Official Image redis:8.10.1, container atlasmart-redis-ch01, standalone topology, host publication 127.0.0.1:6379, TLS disabled only because traffic stays on loopback, default ACL user disabled, named users academy-admin and atlasmart-app, logical database 0, AOF with appendfsync everysec plus RDB snapshots, persistent /data, and no explicit maxmemory limit or eviction policy. Transaction exercises use academy-admin because the Chapter 01 application ACL was intentionally not broadened with the separate @transaction category; production applications should grant only the commands and key patterns they need. Fixtures stay under atlasmart:ch13:*.

1. Start with the invariant, not with the feature name

The correct question is “what must be indivisible?” If the invariant is “increment this integer,” INCR already supplies one atomic server operation. If it is “create all these keys only if all are absent,” MSETNX may express it directly. If the invariant depends on a prior read, WATCH or a conditional command may be needed. Server-side logic is justified only when the simpler primitives cannot express the rule safely.

Invariant Prefer first Why
Increment one integer INCR / HINCRBY Single atomic command; no client read/modify/write race
Create multiple strings only if none exist MSETNX Atomic all-or-none condition in one command
Update one String only if exact old value matches (8.4+) SET IFEQ / IFNE Single-command compare-and-set
Unconditional group of commands MULTI/EXEC No interleaving across queued group
Read-compute-write with conflict detection WATCH + MULTI/EXEC Optimistic concurrency
Small bounded multi-command logic best evaluated server-side Lua/Redis Function One atomic server-side invocation; Chapter 14

2. Wrong approach: GET + SET for a counter

A client-side read/modify/write sequence creates a race and adds a round trip. Redis already has atomic counter commands.

redis-cli · compare race-prone concept with atomic command
# Wrong conceptual sequence under concurrency: GET counter; compute +1 client-side; SET counter newvalueSET atlasmart:ch13:l4:counter 0INCR atlasmart:ch13:l4:counterINCR atlasmart:ch13:l4:counterGET atlasmart:ch13:l4:counter# Expected: "2"; each INCR is the atomic operation.

3. MSETNX is stronger and simpler than a hand-built create transaction

MSETNX writes all supplied String keys only when none of them already exists. If that exact invariant fits, it avoids a WATCH/MULTI loop. It still has Cluster slot constraints when used in Redis Open Source Cluster.

redis-cli · atomic all-or-none initialization
UNLINK atlasmart:ch13:l4:init:a atlasmart:ch13:l4:init:bMSETNX atlasmart:ch13:l4:init:a A atlasmart:ch13:l4:init:b B# Expected: 1MSETNX atlasmart:ch13:l4:init:a A2 atlasmart:ch13:l4:init:b B2# Expected: 0; neither value changes because at least one key exists.MGET atlasmart:ch13:l4:init:a atlasmart:ch13:l4:init:b

4. Redis 8.4+ single-key compare-and-set can replace some WATCH loops

On the pinned Redis 8.10.1 server, SET IFEQ and related value/digest conditions are available. They are version-sensitive and String-specific. Use them when equality against one key is the whole condition; do not pretend they implement arbitrary multi-key transactions.

redis-cli · conditional String update
SET atlasmart:ch13:l4:state pendingSET atlasmart:ch13:l4:state reserved IFEQ pending# Expected: OKSET atlasmart:ch13:l4:state shipped IFEQ pending# Expected: nil/null because current value is reserved.GET atlasmart:ch13:l4:state

5. MULTI/EXEC is for unconditional grouping, not stale-read detection

If the application already knows the commands and simply needs them to run together without interleaving, MULTI/EXEC is appropriate. Reading a value before MULTI and then writing based on it is not protected; WATCH must cover the read-to-EXEC window or the condition must move into a single/server-side command.

redis-cli · unconditional grouped write
MULTISET atlasmart:ch13:l4:order:1:status reservedHINCRBY atlasmart:ch13:l4:metrics reservations 1EXEC# Appropriate only if no pre-read condition is required.

6. WATCH is a client/server coordination protocol, not a lock

WATCH is useful when a client must inspect current state before deciding what to queue. Its cost includes extra round trips and possible retries. Under sustained contention, moving a small deterministic condition into a server-side script/function can reduce retries, but that shifts complexity into blocking time, declared key access, deployment/versioning, ACLs, and Cluster locality.

7. Scripts/functions are atomic but not magical

Redis executes a Lua script or Function atomically with respect to other commands on the server. Long or unbounded server-side logic can therefore block other work and damage tail latency. Scripts/functions also cannot make a Redis write and an external HTTP call one distributed atomic commit. Chapter 14 will make these boundaries observable.

Primitive Round trips Contention behavior Operational burden
Single command 1 No client retry for that command’s atomic rule Lowest
MULTI/EXEC Multiple + EXEC No conditional retry unless WATCH used Moderate
WATCH transaction Read + queue + EXEC, possibly repeated Retries on conflict Client retry/metrics logic
Script/Function Usually 1 invocation No WATCH conflict loop for internal logic Server-side blocking, code/version/ACL/slot discipline

8. Pipelining is orthogonal to the correctness choice

You can pipeline commands to reduce round trips without making them a transaction, or a client can implement a transactional pipeline that wraps MULTI/EXEC. Never infer atomicity solely from the word “pipeline.” Record whether the client’s pipeline is transactional and how errors/retries are surfaced.

9. Cluster slot locality belongs in the design review

Redis Open Source Cluster requires transaction and Lua-script keys to reside in one hash slot. A clever local transaction that spans arbitrary keys may therefore fail after migration to Cluster. Use hash tags only for truly coupled state and benchmark hot-slot effects; otherwise redesign the invariant across independent partitions.

redis-cli · co-located naming concept
SET atlasmart:ch13:{cart:44}:version 7SET atlasmart:ch13:{cart:44}:status open# In Cluster, the shared cart:44 hash tag co-locates these keys.

10. Security and least privilege

A function/script permission does not automatically imply every command/key it touches is safe for the application, and transaction commands have their own ACL category. Evaluate the command set, key patterns, channel permissions where relevant, and application-level tenant authorization separately. Chapter 22 will harden this systematically.

11. Reproducible decision lab

Implement three AtlasMart operations with the smallest correct primitive: a counter increment with INCR, two-key initialization with MSETNX, and one-string state transition with SET IFEQ. Then compare their request count and failure surface with an equivalent WATCH/MULTI design.

redis-cli · evidence bundle
GET atlasmart:ch13:l4:counterMGET atlasmart:ch13:l4:init:a atlasmart:ch13:l4:init:bGET atlasmart:ch13:l4:stateHGETALL atlasmart:ch13:l4:metrics# Record actual replies and command count; do not invent latency numbers.

12. Failure injection: deliberately over-engineer one operation

Reimplement the single counter increment as WATCH → GET → MULTI → SET → EXEC and compare the number of client/server steps with INCR. The WATCH version is not “more transactional”; it is simply more complex for an invariant Redis already implements atomically. Repair by returning to INCR.

13. Cleanup

redis-cli · bounded Lesson 4 cleanup
UNLINK atlasmart:ch13:l4:counter atlasmart:ch13:l4:init:a atlasmart:ch13:l4:init:b atlasmart:ch13:l4:state atlasmart:ch13:l4:order:1:status atlasmart:ch13:l4:metrics atlasmart:ch13:{cart:44}:version atlasmart:ch13:{cart:44}:status

14. Production judgment

Prefer the primitive with the smallest correctness surface that still expresses the invariant. Fewer round trips and less client/server coordination generally mean fewer ambiguity and retry paths, but only if semantics match. Version-gate Redis 8.4+ conditional SET options, keep Cluster slot locality explicit, and benchmark any server-side script/function under realistic payloads. Transaction atomicity never substitutes for persistence, failover testing, tenant authorization, or external-system reconciliation.

Check your understanding

  1. Why is INCR usually better than GET/compute/SET for a counter?
  2. When is MSETNX a good fit?
  3. What is the scope of SET IFEQ?
  4. When is WATCH justified?
  5. Why not put every invariant in a Lua script/function?
Review the answers

INCR is already one atomic server command and avoids the client-side race/extra round trip.

When all supplied String keys should be created only if none of them exists.

A version-sensitive single-String equality compare-and-set; it is not arbitrary multi-key logic.

When the client must read current Redis state and conditionally write based on it, with bounded conflict retries.

Server-side logic is atomic but can block and adds deployment, versioning, ACL, and Cluster-slot responsibilities.

15. Summary and next step

The simplest correct primitive wins: single atomic command first, MULTI/EXEC for unconditional grouping, WATCH for optimistic read-compute-write, and scripts/functions when small bounded logic genuinely belongs server-side. Lesson 5 stress-tests a realistic WATCH workflow so the decision can be driven by conflict and latency evidence rather than taste.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.