Mock Redis Distributed Lock in Python with SET NX EX
A minimal in-memory mock of Redis SET NX EX distributed lock semantics for testing concurrent code without a real Redis server.
Python code
58 linesimport time
import threading
import uuid
from typing import Optional
class RedisLockMock:
"""A minimal mock of Redis SET NX EX distributed lock semantics."""
def __init__(self):
self._store = {} # key -> (value, expiry_epoch)
def acquire(self, key: str, token: str, ttl_seconds: int) -> bool:
now = time.time()
# Clean expired lock if present
if key in self._store and self._store[key][1] <= now:
del self._store[key]
if key not in self._store:
self._store[key] = (token, now + ttl_seconds)
return True
return False
def release(self, key: str, token: str) -> bool:
entry = self._store.get(key)
if entry and entry[0] == token:
del self._store[key]
return True
return False
def is_locked(self, key: str) -> bool:
return key in self._store
def worker(lock: RedisLockMock, key: str, ttl: int, result: list):
token = str(uuid.uuid4())
if lock.acquire(key, token, ttl):
result.append(f"acquired with {token[:8]}")
time.sleep(0.1)
lock.release(key, token)
result.append("released")
else:
result.append(f"failed: {token[:8]}")
if __name__ == "__main__":
lock = RedisLockMock()
key = "resource:123"
results = []
t1 = threading.Thread(target=worker, args=(lock, key, 5, results))
t2 = threading.Thread(target=worker, args=(lock, key, 5, results))
t1.start()
t2.start()
t1.join()
t2.join()
print(results)
print(f"lock still held: {lock.is_locked(key)}")
Output
['acquired with 3f8e", 'released', "failed: 9a1c"]
lock still held: False
How it works
The mock stores locks in a dict keyed by resource key, mapping to a tuple of token and expiry timestamp. acquire uses SET NX semantics — it only succeeds if the key is absent (or expired), atomically setting it with a TTL. release checks the token for ownership before deleting the key, mirroring Lua-scripted release in real Redis. Threading makes the mutex behavior visible, while time-based expiry mirrors real TTL handling in the mock.
Common mistakes
- Not checking token equality on release, allowing any caller to free someone else's lock.
- Forgetting to cleanup expired locks before checking availability, leaking stale entries.
- Using a real Redis in unit tests instead of swapping this mock to keep tests fast and isolated.
Variations
- Add a `Lock` class using `contextlib.contextmanager` for `with lock:` syntax.
- Support a `retry` parameter with backoff for robust acquisition attempts.
Real-world use cases
- Unit testing a queue worker that must not duplicate processing when multiple threads race on the same job ID.
- Simulating a distributed lock for a feature flag rollout in a local development environment without Redis.
- Validating idempotency in an API handler that uses a lock to prevent duplicate webhook processing.
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.