Implementing a Distributed Lock with Redis in Java
This article explains the principles of distributed locks, discusses why they are needed in distributed applications, and provides a complete Java implementation using Redis’s NX and EX parameters, including lock acquisition, blocking and non‑blocking modes, unlocking with Lua scripts, configuration, usage examples, and testing strategies.
Introduction
Distributed locks are widely used in distributed applications. To understand a new concept, it is helpful to know its origin, which also makes it easier to apply the idea to other scenarios.
Why Distributed Locks?
In a single‑machine system, concurrent access to shared resources (e.g., inventory deduction, ticket sales) can be handled with simple synchronization or locking mechanisms such as synchronized or ReentrantLock. When the application is split into multiple processes across different machines, these in‑process solutions are no longer sufficient.
Therefore, the industry commonly relies on a third‑party component that provides exclusive access across processes, such as:
Unique index in a relational database
Temporary ordered nodes in ZooKeeper
Redis SET command with NX and EX parameters
This article focuses on the Redis‑based solution.
Implementation Requirements
High performance for lock acquisition and release
Support for both blocking and non‑blocking lock modes
No deadlock (automatic expiration of stale locks)
High availability (works with Redis clusters)
Redis provides the necessary atomicity through the SET key value NX PX ttl command, which succeeds only when the key does not already exist and automatically expires after the specified TTL.
Lock Acquisition (tryLock)
The core code is shown below:
private static final String SET_IF_NOT_EXIST = "NX");
private static final String SET_WITH_EXPIRE_TIME = "PX";
public boolean tryLock(String key, String request) {
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)) {
return true;
} else {
return false;
}
}Note that the jedis.set method is used with the four arguments to guarantee the atomic execution of NX and EX.
Blocking Lock
A blocking lock repeatedly attempts to acquire the lock, sleeping briefly between attempts to avoid CPU waste:
// continuously block
public void lock(String key, String request) throws InterruptedException {
for (;;) {
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)) {
break;
}
// prevent busy spin
Thread.sleep(DEFAULT_SLEEP_TIME);
}
}
public boolean lock(String key, String request, int blockTime) throws InterruptedException {
while (blockTime >= 0) {
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)) {
return true;
}
blockTime -= DEFAULT_SLEEP_TIME;
Thread.sleep(DEFAULT_SLEEP_TIME);
}
return false;
}Unlocking
Simply deleting the key is unsafe because another process might acquire the same key after expiration. The unlock operation must verify that the lock is still owned by the caller. This is achieved with a Lua script that atomically checks the value and deletes the key only when they match:
public boolean unlock(String key, String request) {
// lua script
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = null;
if (jedis instanceof Jedis) {
result = ((Jedis) this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
} else if (jedis instanceof JedisCluster) {
result = ((JedisCluster) this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
} else {
return false;
}
if (UNLOCK_MSG.equals(result)) {
return true;
} else {
return false;
}
}The Lua script guarantees that the check‑and‑delete operation is atomic.
Features Satisfied
Performance: Redis handles lock operations with millisecond latency.
Both blocking and non‑blocking lock modes are provided.
Deadlock is avoided by using the expiration (TTL) feature.
Redis clusters improve availability and fault tolerance.
Usage
Add the Maven dependency:
<dependency>
<groupId>top.crossoverjie.opensource</groupId>
<artifactId>distributed-redis-lock</artifactId>
<version>1.0.0</version>
</dependency>Configure a Spring bean:
@Configuration
public class RedisLockConfig {
@Bean
public RedisLock build() {
RedisLock redisLock = new RedisLock();
HostAndPort hostAndPort = new HostAndPort("127.0.0.1", 7000);
JedisCluster jedisCluster = new JedisCluster(hostAndPort);
// Jedis or JedisCluster can be used
redisLock.setJedisCluster(jedisCluster);
return redisLock;
}
}Use the lock in your service:
@Autowired
private RedisLock redisLock;
public void use() {
String key = "key";
String request = UUID.randomUUID().toString();
try {
boolean lockAcquired = redisLock.tryLock(key, request);
if (!lockAcquired) {
System.out.println("locked error");
return;
}
// do something critical
} finally {
redisLock.unlock(key, request);
}
}The implementation is simple to integrate, though the API requires manually passing the key and request token when unlocking.
Testing
Because the lock depends on an external Redis instance, unit tests should mock the Redis client. Example using Mockito:
@Test
public void tryLock() throws Exception {
String key = "test";
String request = UUID.randomUUID().toString();
Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyLong())).thenReturn("OK");
boolean locktest = redisLock.tryLock(key, request);
System.out.println("locktest=" + locktest);
Assert.assertTrue(locktest);
Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyLong());
}Mocking removes the need for a real Redis server during unit testing.
Conclusion
The Redis‑based distributed lock works well but still has limitations, such as premature lock release when the TTL expires before the business logic finishes, and potential loss of locks if a master node crashes without a replica. For more robust solutions, consider using Redisson.
Source: https://crossoverjie.top/2018/03/29/distributed-lock/distributed-lock-redis/
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.
