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.
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.
Prefer one atomic Redis command when it already expresses the invariant.
Distinguish unconditional MULTI/EXEC grouping from conditional WATCH-based concurrency control.
Use Redis 8.4+ conditional SET/DELEX only when their exact String comparison semantics fit.
Identify when scripts/functions reduce race windows but increase server-side blocking/versioning responsibility.
Keep Cluster slot locality, ACLs, persistence, external side effects, and observability in the primitive choice.
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.
# 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.
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.
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.
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.
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.
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
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
- Why is INCR usually better than GET/compute/SET for a counter?
- When is MSETNX a good fit?
- What is the scope of SET IFEQ?
- When is WATCH justified?
- 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
- Redis Open Source 8.10 release notes
- Redis 8.10 command reference
- Redis transactions
- MULTI
- EXEC
- DISCARD
- WATCH
- UNWATCH
- Redis multi-key operations
- Redis pipelining
- redis-py pipelines and transactions
- redis-py documentation
- SET
- DELEX
- MSETNX
- INCR
- HINCRBY
- EVAL
- FCALL
- Redis ACLs
- Redis persistence
- Redis replication
- Redis licenses