Skip to content
LowLevelDesign Mastery

Distributed Locks

Coordinating actions across distributed nodes

In distributed systems, multiple nodes often need to coordinate access to shared resources:

  • Database updates: Only one node should update a record at a time
  • Cache invalidation: Prevent multiple nodes from invalidating cache simultaneously
  • Scheduled tasks: Ensure only one node runs a scheduled job
  • Resource allocation: Coordinate access to limited resources

The Challenge: Traditional locks (like mutexes) only work within a single process. In distributed systems, we need locks that work across multiple nodes, handle network failures, and prevent deadlocks.

Distributed coordination problem: three nodes all want to update Record X, and a distributed lock lets only one hold it at a time

A distributed lock is a coordination mechanism that ensures only one process or node can hold a lock at a time across a distributed system. It provides mutual exclusion across network boundaries.

  1. Mutual Exclusion: Only one holder at a time
  2. Deadlock Free: Locks are eventually released (via timeout/lease)
  3. Fault Tolerant: Survives node failures
  4. High Availability: Lock service must be available
  5. Performance: Low latency, high throughput

Think of a distributed lock like a bathroom key at a restaurant:

  • Only one person can have the key at a time
  • If someone forgets to return the key, there’s a timeout mechanism (staff has a master key)
  • Multiple people can request the key, but only one gets it
  • The key must be returned for others to use it

Lease-based locks automatically expire after a timeout period. This prevents deadlocks from crashed nodes.

Lease-based lock lifecycle: acquire with a lease time, hold and renew before expiry, or the lock is released automatically on expiration

How It Works:

  1. Node acquires lock with a lease time (e.g., 10 seconds)
  2. Node must renew lock before lease expires
  3. If node crashes, lock automatically expires
  4. Other nodes can acquire lock after expiration

Benefits:

  • Prevents deadlocks from crashed nodes
  • Automatic cleanup
  • No manual lock release needed if node crashes

Redis provides a simple way to implement distributed locks using the SET command with NX (only if not exists) and EX (expiration) options.

Redis distributed lock: client sends SET lock_key value NX EX 10 and gets OK if acquired or nil if another client holds it

Key Points:

  • Use unique value (like UUID) to verify ownership
  • Set expiration to prevent deadlocks
  • Check return value: OK means lock acquired, nil means already held
💡 Tip: Click dropdown to switch between languages
"distributed_lock.py
import redis
import uuid
import time
import threading
from typing import Optional
class DistributedLock:
"""Distributed lock using Redis"""
def __init__(self, redis_client: redis.Redis, lock_key: str, lease_time: int = 10):
self.redis = redis_client
self.lock_key = lock_key
self.lease_time = lease_time
self.lock_value = str(uuid.uuid4()) # Unique value for ownership verification
self.renewal_thread: Optional[threading.Thread] = None
self.lock_held = False
def acquire(self, timeout: int = 5) -> bool:
"""Acquire lock with timeout"""
start_time = time.time()
while time.time() - start_time < timeout:
# Try to acquire lock: SET lock_key value NX EX lease_time
result = self.redis.set(
self.lock_key,
self.lock_value,
nx=True, # Only set if not exists
ex=self.lease_time # Expire after lease_time seconds
)
if result:
self.lock_held = True
# Start renewal thread
self._start_renewal()
return True
# Lock held by another, wait a bit
time.sleep(0.1)
return False # Timeout
def release(self) -> bool:
"""Release lock (only if we own it)"""
if not self.lock_held:
return False
# Lua script to atomically check and delete
# Only delete if value matches (we own the lock)
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
result = self.redis.eval(lua_script, 1, self.lock_key, self.lock_value)
if result:
self.lock_held = False
self._stop_renewal()
return True
return False # Lock expired or held by another
def _start_renewal(self):
"""Start thread to renew lock before expiration"""
def renew():
while self.lock_held:
time.sleep(self.lease_time / 2) # Renew at half lease time
if self.lock_held:
# Extend expiration
self.redis.expire(self.lock_key, self.lease_time)
self.renewal_thread = threading.Thread(target=renew, daemon=True)
self.renewal_thread.start()
def _stop_renewal(self):
"""Stop renewal thread"""
self.renewal_thread = None
def __enter__(self):
"""Context manager entry"""
if not self.acquire():
raise Exception("Failed to acquire lock")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit"""
self.release()
# Usage
redis_client = redis.Redis(host='localhost', port=6379)
# Using context manager
lock = DistributedLock(redis_client, "resource_lock", lease_time=10)
with lock:
# Critical section
print("Doing work with lock held")
time.sleep(5)
# Lock automatically released

Problem: Network partition can cause split-brain (multiple lock holders).

Solution: Use majority consensus (like Redlock algorithm) or accept that locks are best-effort.

Problem: Different nodes have different clocks, affecting lease expiration.

Solution: Use logical clocks or ensure clock synchronization (NTP).

Problem: If lock holder crashes, lock might never be released.

Solution: Use lease-based locks with automatic expiration.

Problem: Lock acquisition adds latency.

Solution: Use local locks when possible, minimize lock hold time, use optimistic locking when appropriate.

Ensure only one node updates a record at a time. Prevents race conditions and data corruption.

Ensure only one node runs a scheduled job. Prevents duplicate execution across multiple nodes.

Coordinate cache invalidation across nodes. Prevents multiple nodes from invalidating cache simultaneously.

Coordinate access to limited resources (like API rate limits, connection pools).

Advantages:

  • Provides mutual exclusion across nodes
  • Prevents race conditions
  • Coordinates distributed operations

Disadvantages:

  • Adds latency
  • Single point of failure (lock service)
  • Complex to implement correctly
  • Network partitions can cause issues

Mutual Exclusion

Ensures only one node holds lock at a time. Provides coordination across distributed systems.

Lease-Based

Locks automatically expire after lease time. Prevents deadlocks from crashed nodes. Requires renewal.

Ownership Verification

Use unique value (UUID) to verify lock ownership before release. Prevents releasing locks held by others.

Atomic Operations

Use Lua scripts or atomic Redis commands for lock acquisition and release. Ensures correctness.