Python Redis WATCH optimistic lock mock

A MockRedis class that simulates WATCH/MULTI/EXEC transactions with optimistic locking to detect concurrent modifications before committing.

Medium Python 3.9+ Aug 9, 2026 Caching & Redis 12 views 0 copies

Python code

66 lines
Python 3.9+
import time
import threading


class MockRedis:
    def __init__(self):
        self.data = {}
        self.watched = {}
        self.lock = threading.Lock()

    def get(self, key):
        return self.data.get(key)

    def set(self, key, value):
        self.data[key] = value

    def watch(self, *keys):
        with self.lock:
            for key in keys:
                self.watched[key] = self.data.get(key)

    def multi(self):
        return MockTransaction(self)

    def execute(self, transaction):
        with self.lock:
            for key, expected in self.watched.items():
                if self.data.get(key) != expected:
                    return None
            for key, value in transaction.operations:
                self.data[key] = value
            self.watched.clear()
            return transaction.operations


class MockTransaction:
    def __init__(self, redis):
        self.redis = redis
        self.operations = []

    def set(self, key, value):
        self.operations.append((key, value))


def transfer(redis, from_key, to_key, amount):
    redis.watch(from_key)
    balance = redis.get(from_key) or 0
    if balance < amount:
        redis.watched.clear()
        return False
    tx = redis.multi()
    tx.set(from_key, balance - amount)
    tx.set(to_key, (redis.get(to_key) or 0) + amount)
    return redis.execute(tx) is not None


if __name__ == "__main__":
    r = MockRedis()
    r.set("alice", 100)
    r.set("bob", 50)

    r.watch("alice")
    r.set("alice", 80)  # simulate concurrent modification
    result = transfer(r, "alice", "bob", 30)
    print(f"Transfer success: {result}")
    print(f"alice={r.get('alice')}, bob={r.get('bob')}")

Output

stdout
Transfer success: False
alice=80, bob=50

How it works

MockRedis.watch records the current value of each watched key. execute re-checks those keys under a lock and aborts the transaction (returns None) if any differs — that's optimistic locking. The transfer helper reads the balance, builds a transaction with multi(), and commits only if the watched key was unchanged. This mirrors Redis's WATCH semantics where a failed EXEC indicates a concurrent write.

Common mistakes

  • Clearing watched keys on abort — Redis clears them automatically after EXEC regardless of outcome.
  • Forgetting to check the `or 0` fallback when a key doesn't exist yet.
  • Not using a lock around the compare-and-swap, allowing race conditions in the mock.
  • Returning `True` on abort instead of `None` or `False`.

Variations

  1. Use `redis-py`'s real client with `pipeline(transaction=True)` and `watch()` for production code.
  2. Add support for `unwatch()` to clear watched keys without executing.

Real-world use cases

  • Preventing lost updates when multiple workers decrement a shared inventory counter.
  • Building a distributed rate limiter that checks and updates a counter atomically.
  • Implementing compare-and-swap semantics for a feature flag or config value in a cache.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Caching & Redis

Related tutorials and quizzes for this topic.