Redis Distributed Locks
Lock Acquisition
class RedisLock {
public function __construct(
private Redis $redis,
private string $lockKey,
private int $timeout = 10,
private int $retryDelay = 100,
private int $maxRetries = 3
) {}
public function acquire(): bool {
$token = bin2hex(random_bytes(16));
for ($i = 0; $i < $this->maxRetries; $i++) {
// SET NX EX = set if not exists with expiry
$acquired = $this->redis->set(
$this->lockKey,
$token,
['NX', 'EX' => $this->timeout]
);
if ($acquired) {
$this->token = $token;
return true;
}
usleep($this->retryDelay * 1000);
}
return false;
}
public function release(): bool {
// Only release if we own the lock
$script = <<<LUA
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
LUA;
return $this->redis->eval($script, [$this->lockKey, $this->token], 1);
}
}
Lock Usage
$lock = new RedisLock($redis, 'order_process_' . $orderId, 30);
if ($lock->acquire()) {
try {
$this->processOrder($orderId);
} finally {
$lock->release();
}
} else {
throw new LockAcquisitionException('Could not acquire lock');
}
Lock Contention
Contention Scenarios
Scenario | Impact | Solution
──────────────────────|────────────────|─────────────────
Hot key contention | Many retries | Lock splitting
Long-held locks | Blocking | Shorter timeouts
Deadlock | System hang | Lock ordering
Frequent acquisition | High overhead | Lock-free alternatives
Lock Splitting
// BAD: Single lock for all products
$lock = new RedisLock($redis, 'inventory_lock', 10);
// GOOD: Lock per product
$lock = new RedisLock($redis, 'inventory_' . $productId, 5);
// Different products can be updated concurrently
Lock Ordering
// Prevent deadlock with consistent lock ordering
function transferFunds($from, $to, $amount) {
$locks = [$from, $to];
sort($locks); // Always acquire in same order
$lock1 = new RedisLock($redis, 'account_' . $locks[0]);
$lock2 = new RedisLock($redis, 'account_' . $locks[1]);
$lock1->acquire();
$lock2->acquire();
try {
// Transfer funds
} finally {
$lock2->release();
$lock1->release();
}
}
Lock Timeout
Timeout Strategies
Strategy | Approach | Use Case
───────────────────|────────────────────────|─────────────
Fixed timeout | Set expiry on lock | Simple ops
Lease renewal | Extend timeout | Long ops
Watchdog | Auto-renew while alive | Unknown duration
Fencing token | Monotonic counter | Critical ops
Watchdog Pattern
class WatchdogLock {
private $renewTimer;
public function acquire() {
$acquired = $this->lock->acquire();
if ($acquired) {
$this->startWatchdog();
}
return $acquired;
}
private function startWatchdog() {
$this->renewTimer = setInterval(function () {
// Renew lock every 5 seconds
$this->redis->expire($this->lockKey, $this->timeout);
}, 5000);
}
public function release() {
clearInterval($this->renewTimer);
$this->lock->release();
}
}
Fencing Token
// Monotonic counter prevents stale lock holders
$token = $this->redis->incr('fencing_token_' . $resource);
// Acquire lock with token
$lock = $this->acquireLock($resource, $token);
// Use token in operation
$this->db->update($resource, [
'data' => $value,
'fencing_token' => $token
], [
'fencing_token < ?' => $token // Only if token is newer
]);
Distributed Coordination
Coordination Patterns
1. Leader election: Choose one node as leader
2. Barrier synchronization: Wait for all nodes
3. Distributed queue: Fair task distribution
4. Rate limiting: Global request limiting
Leader Election
function electLeader($candidates) {
$lock = new RedisLock($redis, 'leader_election', 30);
if ($lock->acquire()) {
try {
$leader = $candidates[array_rand($candidates)];
$this->redis->set('current_leader', $leader, ['EX' => 30]);
return $leader;
} finally {
$lock->release();
}
}
return $this->redis->get('current_leader');
}
Distributed Rate Limiting
function checkRateLimit($key, $limit, $window) {
$now = microtime(true);
$windowStart = $now - $window;
$script = <<<LUA
redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1])
local count = redis.call('zcard', KEYS[1])
if count < tonumber(ARGV[2]) then
redis.call('zadd', KEYS[1], ARGV[3], ARGV[3])
redis.call('expire', KEYS[1], ARGV[4])
return 1
end
return 0
LUA;
return $this->redis->eval($script, [
$key, $windowStart, $limit, $now, $window
], 1);
}
Quiz
1. How to safely release a Redis lock?
2. What prevents deadlock in distributed locks?
3. What is a fencing token?
Flashcards
Question
Redis lock acquisition?
Click to reveal answer
Answer
SET key token NX EX timeout - set if not exists with expiry
Question
Safe lock release?
Click to reveal answer
Answer
Lua script: verify ownership, then delete
Question
Deadlock prevention?
Click to reveal answer
Answer
Consistent lock ordering across all nodes
Question
Fencing token?
Click to reveal answer
Answer
Monotonic counter to prevent stale lock holder operations
Revision Notes
Key Takeaways
- 1. Redis SET NX EX provides atomic lock acquisition
- 2. Always verify ownership before releasing locks
- 3. Lock splitting reduces contention on hot keys
- 4. Consistent lock ordering prevents deadlocks
- 5. Fencing tokens prevent stale lock holder operations
Interview Tips
- • Explain Redis lock implementation and safe release
- • Discuss lock contention and deadlock prevention
- • Describe watchdog and fencing token patterns
Cheat Sheet
Distributed Locks:
Acquire: SET key token NX EX timeout
Release: Lua script verify + delete
Retry: Loop with delay
Contention:
Lock splitting: per-entity locks
Lock ordering: prevent deadlock
Shorter timeouts: reduce blocking
Patterns:
Watchdog: Auto-renew while alive
Fencing token: Monotonic counter
Leader election: Choose coordinator
Rate limiting: Global request cap