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.

Architecture Digest
Architecture Digest
Architecture Digest
Implementing a Distributed Lock with Redis in Java

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/
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaconcurrencyredisspringdistributed-lock
Architecture Digest
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.