Python Redis WATCH optimistic lock mock
A MockRedis class that simulates WATCH/MULTI/EXEC transactions with optimistic locking to detect concurrent modifications before committing.
Python code
66 linesimport 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
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
- Use `redis-py`'s real client with `pipeline(transaction=True)` and `watch()` for production code.
- 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
More from Caching & Redis
- Cache Asides in Python with a Read-Through Loader easy
- Cache Data in Redis with Python easy
- Cache Penetration Null Object Mock in Python medium
- Cache Stampede Prevention with SingleFlight in Python medium
- Cache Warming with Python: Preload Hot Keys easy
- Coalescing duplicate in-flight requests: one shared result for concurrent callers hard
Keep learning
Related tutorials and quizzes for this topic.