Course outline

Redis / Intermediate: application correctness and coordination / Lesson 9

MULTI, EXEC, WATCH and error semantics

Use Redis transaction isolation and optimistic checks without importing SQL rollback assumptions. Inspect queue-time, execution-time and uncertain-commit outcomes separately.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain queued commands and complete EXEC reply inspection.
  • Design bounded WATCH retries and clean connection state.

Redis transactions serialize a queued command group

MULTI begins command queuing and EXEC runs the queued group without serving another clientโ€™s request in its middle. QUEUED means scheduled, not successfully executed. DISCARD clears the unexecuted queue; it is not a rollback after EXEC. Redis does not provide general SQL-style transaction rollback. Runtime errors can appear among successful replies, while some queue-time errors prevent execution of the group. The application must inspect the actual returned array and business postcondition. Isolation of command execution is different from durability, distributed system atomicity and automatic validation of every invariant.

Read every reply when one command has the wrong type

Assume both synthetic keys start absent. The first SET creates a string value one. Inside MULTI, INCR on that string succeeds, HSET on the same string fails with WRONGTYPE, and INCR on the second absent key succeeds. EXEC does not undo the successful increments because a later command had a runtime type error. The expected final values are two and one. This intentionally invalid example is explanatory and never executed; it shows why a transaction block must not be advertised as automatic all-or-nothing error rollback.

redis Non-runnable intentional runtime-error illustration
SET lab:tx:a 1
MULTI
INCR lab:tx:a
HSET lab:tx:a field value
INCR lab:tx:b
EXEC
GET lab:tx:a
GET lab:tx:b
Expected output
EXEC logical replies: [2, WRONGTYPE, 1]
Final values: a=2, b=1

EXEC replies include 2, WRONGTYPE and 1; the successful changes remain. Final GET values are 2 and 1 under the stated fixture.

WATCH protects a read-dependent attempt conditionally

WATCH tracks specified keys before EXEC; if a watched key changes, the conditional transaction can abort and the application must reconsider its read-dependent decision. Modern Redis also treats relevant expiration or eviction as a modification; behavior before 6.0.9 differed for expiration. WATCH is connection-scoped and needs a dedicated client flow rather than unrelated requests sharing state casually. A retry must re-read and recompute, with a finite attempt budget. A null/aborted EXEC outcome is not the same as a successful EXEC array containing a runtime error.

Model an optimistic version check

The offline version model rejects a stale expected version and accepts a matching one. It is not an implementation of Redis WATCH, because real keys, connection state and server modification tracking are absent. Its purpose is to expose the retry decision: rejection must not lead to an unconditional overwrite based on old data. If the operation also has an external effect, retry eligibility must preserve business identity. A successful server-side conditional update does not tell whether a separate notification or payment was delivered once.

python Offline optimistic-attempt decision
def may_commit(expected_version, current_version):
    return expected_version == current_version

assert not may_commit(2, 3)
assert may_commit(3, 3)

The stale attempt is rejected and the fresh attempt accepted only in this declared model. No real Redis transaction or lock is created.

Clean up state and reconcile uncertain EXEC

A client exception must not return a connection still in MULTI or WATCH state to an unrelated pool borrower. Use documented library transaction helpers and cleanup paths. If the connection fails after EXEC was sent but before its reply arrives, the outcome may be uncertain; repeating non-idempotent work blindly can duplicate effects. Cluster transactions require eligible same-slot keys, and managed services can restrict supported flows. Review queue-time errors, runtime errors, watch aborts and lost replies as separate cases. Tests should prove the required business invariant under each outcome, not merely that a code block uses MULTI.

Common errors and fixes

Successful commands are assumed rolled back after WRONGTYPE

Why it happens
Redis runtime errors were interpreted with SQL rollback semantics.
How to fix it
Inspect the EXEC array and reconcile successful effects explicitly.
Prevent it next time
Test runtime errors after an earlier queued write.

An aborted WATCH attempt overwrites fresh data

Why it happens
The retry reused stale read results without rechecking.
How to fix it
Re-read, recompute and retry only within a bounded policy.
Prevent it next time
Separate optimistic rejection from successful commit handling.

A pooled connection retains transaction state

Why it happens
Exception cleanup omitted queued or watched state.
How to fix it
Use the clientโ€™s documented reset/discard/unwatch lifecycle.
Prevent it next time
Test connection reuse after every error branch.

Check your understanding

1. EXEC returns two successful replies and one runtime type error. Is it correct to tell the caller that nothing happened?

Show worked solution

No. Successful commands can have applied and remain applied. Interpret each reply and the business postcondition, preserving event identity before any recovery or retry. Redis transaction execution is isolated from other clients, but that property is not general rollback when an individual command fails at runtime.

2. WATCH aborts because another writer changed a key. What must a valid retry do?

Show worked solution

Read the current values again, recompute the intended decision, and attempt the supported transaction flow within a bounded retry budget. Reusing the old value and removing the check defeats optimistic concurrency. Also ensure the connection is not shared with unrelated requests while its state belongs to the attempt.

Key takeaways

  • Redis transaction isolation does not imply SQL-style rollback.
  • Classify EXEC outcomes and clean connection state before reuse.

Official references