Implementing Distributed Locks with Redis: Commands, Java Implementations, and Lua Script Optimizations
This article explains how to build a reliable distributed lock using Redis by introducing essential Redis commands, presenting initial Java pseudocode, addressing race‑condition pitfalls, improving the design with GETSET and Lua scripts, and providing complete, production‑ready Java implementations with unlock safety fixes.
Distributed locks are essential for coordinating access to shared resources in a distributed system, and Redis offers several commands that can be combined to achieve this functionality.
The key Redis commands discussed are SETNX (set if not exists), EXPIRE (set key expiration), GETSET (set new value and return old), and the scripting commands EVAL / EVALSHA for atomic Lua execution.
Using the atomicity of SETNX, a simple non‑blocking lock can be created; the article provides a Java‑style pseudocode example:
boolean tryLock(String key, int lockSeconds) {
if (SETNX key "1" == 1) {
EXPIRE key lockSeconds;
return true;
} else {
return false;
}
}
boolean unlock(String key) {
DEL key;
}However, this approach suffers from a race condition: if the client crashes after SETNX but before EXPIRE, the lock may never expire, causing a permanent deadlock.
To mitigate this, the article introduces an improved algorithm that stores the lock’s expiration timestamp as the value, checks for stale locks, and uses GETSET to atomically replace the timestamp when a stale lock is detected. A full Java implementation of this improved lock is provided:
public class RedisLock {
private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);
private final StringRedisTemplate stringRedisTemplate;
private final byte[] lockKey;
// ... constructor and fields omitted for brevity ...
private boolean tryLock(RedisConnection conn, int lockSeconds) throws Exception {
long nowTime = System.currentTimeMillis();
long expireTime = nowTime + lockSeconds * 1000 + 1000; // 1‑second tolerance
if (conn.setNX(lockKey, longToBytes(expireTime))) {
conn.expire(lockKey, lockSeconds);
return true;
} else {
byte[] oldValue = conn.get(lockKey);
if (oldValue != null && bytesToLong(oldValue) < nowTime) {
byte[] oldValue2 = conn.getSet(lockKey, longToBytes(expireTime));
if (Arrays.equals(oldValue, oldValue2)) {
conn.expire(lockKey, lockSeconds);
return true;
}
}
return false;
}
}
// public tryLock, tryLock with polling, unlock, and conversion helpers omitted for brevity
}Even with this improvement, the lock acquisition still requires at least two round‑trips to Redis, and time‑synchronization between servers remains a concern.
Starting with Redis 2.6, Lua scripts can be executed atomically on the server, eliminating the gap between SETNX and EXPIRE. The article shows a concise Lua script for lock acquisition:
if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then
redis.call('expire', KEYS[1], tonumber(ARGV[2]))
return true
else
return false
endUsing this script, a streamlined Java class is presented that loads the script once and reuses it for lock attempts, providing both simple and polling‑based lock methods.
public class RedisLock {
private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);
private final StringRedisTemplate stringRedisTemplate;
private final String lockKey;
private final List<String> keys;
private static final RedisScript<Boolean> SETNX_AND_EXPIRE_SCRIPT;
static {
StringBuilder sb = new StringBuilder();
sb.append("if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then
");
sb.append("\tredis.call('expire', KEYS[1], tonumber(ARGV[2]))
");
sb.append("\treturn true
");
sb.append("else
");
sb.append("\treturn false
");
sb.append("end");
SETNX_AND_EXPIRE_SCRIPT = new RedisScriptImpl<>(sb.toString(), Boolean.class);
}
// constructor, doTryLock, tryLock, tryLock with polling, and unlock methods omitted for brevity
}The article also identifies a serious unlock vulnerability: a simple DEL may remove another client’s lock if the original holder’s work exceeds the lock’s TTL. To fix this, a unique lock value (UUID + timestamp) is stored, and unlocking is performed via a Lua script that deletes the key only if the stored value matches.
private static final RedisScript<Boolean> DEL_IF_GET_EQUALS;
static {
StringBuilder sb = new StringBuilder();
sb.append("if (redis.call('get', KEYS[1]) == ARGV[1]) then
");
sb.append("\tredis.call('del', KEYS[1])
");
sb.append("\treturn true
");
sb.append("else
");
sb.append("\treturn false
");
sb.append("end");
DEL_IF_GET_EQUALS = new RedisScriptImpl<>(sb.toString(), Boolean.class);
}
public void unlock() {
if (!locked) {
throw new IllegalStateException("not locked yet!");
}
locked = false;
stringRedisTemplate.execute(DEL_IF_GET_EQUALS, Collections.singletonList(lockKey), lockValue);
}In summary, the article walks through the evolution from a naïve Redis lock to a robust, script‑based implementation that handles expiration, atomicity, and safe unlocking, providing complete Java source code for each stage.
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.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.
