Distributed Locks
Coordinate exclusive access to resources across multiple service instances.
Overview
-
Preventing duplicate processing of jobs/events
-
Coordinating singleton processes (cron, leaders)
-
Protecting critical sections across instances
-
Implementing leader election
-
Rate limiting at distributed level
Lock Types Comparison
Lock Type Durability Latency Use Case
Redis (single) Low ~1ms Fast, non-critical
Redlock (multi) High ~5ms Critical, HA required
PostgreSQL advisory High ~2ms Already using PG, ACID
Quick Reference
Redis Lock (Single Node)
from uuid_utils import uuid7 import redis.asyncio as redis
class RedisLock: """Redis lock with Lua scripts for atomicity."""
ACQUIRE = "if redis.call('set',KEYS[1],ARGV[1],'NX','PX',ARGV[2]) then return 1 end return 0"
RELEASE = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) end return 0"
def __init__(self, client: redis.Redis, name: str, ttl_ms: int = 30000):
self._client = client
self._name = f"lock:{name}"
self._owner = str(uuid7())
self._ttl = ttl_ms
async def acquire(self) -> bool:
return await self._client.eval(self.ACQUIRE, 1, self._name, self._owner, self._ttl) == 1
async def release(self) -> bool:
return await self._client.eval(self.RELEASE, 1, self._name, self._owner) == 1
async def __aenter__(self):
if not await self.acquire():
raise LockError(f"Failed to acquire {self._name}")
return self
async def __aexit__(self, *_):
await self.release()
See redis-locks.md for complete implementation with retry/extend.
PostgreSQL Advisory Lock
from sqlalchemy import text
async def with_advisory_lock(session, lock_id: int): """PostgreSQL advisory lock (session-level).""" await session.execute(text("SELECT pg_advisory_lock(:id)"), {"id": lock_id}) try: yield finally: await session.execute(text("SELECT pg_advisory_unlock(:id)"), {"id": lock_id})
See postgres-advisory-locks.md for transaction-level and monitoring.
Key Decisions
Decision Recommendation
Backend Redis for speed, PG if already using it
TTL 2-3x expected operation time
Retry Exponential backoff with jitter
Fencing Include owner ID for safety
Anti-Patterns (FORBIDDEN)
NEVER forget TTL (causes deadlocks)
await redis.set(f"lock:{name}", "1") # WRONG - no expiry!
NEVER release without owner check
await redis.delete(f"lock:{name}") # WRONG - might release others' lock
NEVER use single Redis for critical operations
lock = RedisLock(single_redis, "payment") # Use Redlock for HA
NEVER hold locks across await points without heartbeat
async with lock: await slow_external_api() # Lock may expire!
Related Skills
-
idempotency-patterns
-
Complement locks with idempotency
-
caching-strategies
-
Redis patterns
-
background-jobs
-
Job deduplication
References
-
Redis Locks - Lua scripts, retry, extend
-
Redlock Algorithm - Multi-node HA
-
PostgreSQL Advisory - Session/transaction
Capability Details
redis-locks
Keywords: Redis, Lua, SET NX, atomic, TTL Solves: Fast distributed locks, atomic acquire/release, auto-expiry
redlock
Keywords: Redlock, multi-node, quorum, HA, fault-tolerant Solves: High-availability locking, survive node failures
advisory-locks
Keywords: PostgreSQL, advisory, pg_advisory_lock, session, transaction Solves: Lock with existing PG, ACID integration, no extra infra
leader-election
Keywords: leader, election, singleton, coordinator Solves: Single active instance, coordinator pattern