Implementing Distributed Locks with Redis: Problems and Solutions
This article explains how Redis can be used to implement distributed locks, outlines common pitfalls such as non‑atomic operations, lock expiration, incorrect unlocking, lack of re‑entrancy and waiting mechanisms, and presents Lua scripts, Java examples, and cluster‑level considerations to mitigate these issues.
Introduction
Distributed locks are a common interview topic in large tech companies. When modifying existing data in a clustered system, concurrent updates can cause data loss because read‑modify‑write is not atomic. Local locks work only on a single server, so a distributed lock is required to ensure consistency across multiple nodes.
Implementation
Redis locks mainly rely on the SETNX command.
Lock command: SETNX key value – sets the key only if it does not exist.
Unlock command: DEL key – deletes the key to release the lock.
Lock timeout: EXPIRE key timeout – sets an expiration to avoid permanent lock.
Lock/unlock pseudocode:
if (setnx(key, 1) == 1){
expire(key, 30)
try {
//TODO business logic
} finally {
del(key)
}
}1. SETNX and EXPIRE are not atomic
If SETNX succeeds but the server crashes before EXPIRE runs, the lock becomes a dead lock. A Lua script can combine the two operations atomically:
if (redis.call('setnx', KEYS[1], ARGV[1]) < 1)
then return 0;
end;
redis.call('expire', KEYS[1], tonumber(ARGV[2]));
return 1;
// usage example
EVAL "if (redis.call('setnx',KEYS[1],ARGV[1]) < 1) then return 0; end; redis.call('expire',KEYS[1],tonumber(ARGV[2])); return 1;" 1 key value 1002. Incorrect unlock
If thread A acquires the lock with a 30‑second timeout but runs longer, the lock expires and thread B acquires it; when A later calls DEL, it unintentionally releases B's lock. Storing a unique identifier (e.g., a UUID) as the lock value and verifying it before deletion solves the problem:
// lock
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
SET key uuid NX EX 30
// unlock
if (redis.call('get', KEYS[1]) == ARGV[1])
return redis.call('del', KEYS[1])
else
return 03. Timeout unlock causing concurrency
When a lock expires while the original holder is still working, both the original and a new holder may execute concurrently. Solutions include setting a sufficiently long timeout or implementing an automatic renewal (watchdog) thread.
4. Non‑reentrancy
A non‑reentrant lock cannot be acquired again by the same thread. Re‑entrancy can be achieved by counting lock acquisitions. A Java example using ThreadLocal to store a counter:
private static ThreadLocal<Map<String, Integer>> LOCKERS = ThreadLocal.withInitial(HashMap::new);
// lock
public boolean lock(String key) {
Map<String, Integer> lockers = LOCKERS.get();
if (lockers.containsKey(key)) {
lockers.put(key, lockers.get(key) + 1);
return true;
} else {
if (SET key uuid NX EX 30) {
lockers.put(key, 1);
return true;
}
}
return false;
}
// unlock
public void unlock(String key) {
Map<String, Integer> lockers = LOCKERS.get();
if (lockers.getOrDefault(key, 0) <= 1) {
lockers.remove(key);
DEL key
} else {
lockers.put(key, lockers.get(key) - 1);
}
}Alternatively, Redis hash structures can store both the lock identifier and a re‑entrancy count, as shown in the Redisson‑style Lua script below:
// if lock_key does not exist
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// if lock_key exists and the thread identifier matches
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// lock acquisition failed, return remaining TTL
return redis.call('pttl', KEYS[1]);5. Unable to wait for lock release
All the commands above return immediately, so a client cannot block until the lock is released. Two approaches are common:
Polling: repeatedly attempt to acquire the lock with a sleep interval until success or a timeout.
Publish/Subscribe: subscribe to a lock‑release channel and receive a notification when the lock becomes available.
Redis also provides the Redlock algorithm for distributed locking, but its usage is controversial and is omitted here.
Cluster Considerations
1. Master‑Slave Failover
Redis is usually deployed in a master‑slave configuration for high availability. If the master crashes after a client has acquired a lock but before the command is replicated, the new master (previous slave) will not have the lock, allowing another client to acquire it and causing duplicate ownership.
2. Split‑Brain
Network partitions can cause the master to become isolated from its slaves and Sentinel nodes, leading to multiple masters. Clients connected to different masters may each think they hold the same lock, resulting in concurrent execution.
Conclusion
Redis is known for high performance, but implementing distributed locks with it introduces several challenges such as atomicity, expiration handling, re‑entrancy, and cluster‑level anomalies. Redis locks should be viewed as a mitigation technique; for full correctness, database‑level concurrency controls are still required.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Top Architecture Tech Stack
Sharing Java and Python tech insights, with occasional practical development tool tips.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
