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.

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

Python code

58 lines
Python 3.9+
import 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

stdout
['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

  1. Add a `Lock` class using `contextlib.contextmanager` for `with lock:` syntax.
  2. 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

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.