Project: Distributed Lock

Introduction

In a distributed system, multiple application instances may compete for the same resource. A distributed lock prevents concurrent execution of a critical section by ensuring that only one process holds the lock at any time. This chapter builds a production-grade lock with Redis, then introduces Redisson, a Java library that handles edge cases such as lock renewal, reentrancy, and cluster topology automatically.

Prerequisites

The Naive Lock

The simplest distributed lock uses SETNX (Set if Not eXists) to claim a key, plus EXPIRE to prevent a crashed client from holding the lock forever.

java
public boolean lock(String key, String token, long seconds) {
    Boolean acquired = redisTemplate.opsForValue()
        .setIfAbsent(key, token, Duration.ofSeconds(seconds));
    return Boolean.TRUE.equals(acquired);
}
 
public void unlock(String key, String token) {
    String current = redisTemplate.opsForValue().get(key);
    if (token.equals(current)) {
        redisTemplate.delete(key);
    }
}

This works in happy-path scenarios, but it has serious flaws:

  1. No automatic renewal: If the critical section takes longer than the expiration, another client can steal the lock mid-operation.
  2. Not reentrant: The same thread cannot acquire the lock twice without deadlocking itself.
  3. Unsafe unlock: The check-and-delete is not atomic. Another client might acquire the lock between the GET and DEL.
  4. Clock skew: Reliance on local time is fragile.

A Safer Lock with Lua

Lua scripts execute atomically on the Redis server, eliminating the race condition in unlock.

Tip

Always Use a Unique Token

The token should be globally unique, typically a UUID plus a thread identifier. If a client crashes and its lock expires, a second client acquires the lock with a different token. The original client's late unlock call sees the mismatched token and safely does nothing.

Redisson: Production-Grade Locking

For production systems, Redisson implements the full locking contract: reentrancy, automatic renewal (watchdog), fairness options, and cluster awareness.

Dependency

xml
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.24.0</version>
</dependency>

Configuration

yaml
spring:
  redis:
    redisson:
      config: |
        singleServerConfig:
          address: "redis://localhost:6379"
          connectionPoolSize: 64

For Sentinel or Cluster, change the Redisson config block accordingly:

yaml
spring:
  redis:
    redisson:
      config: |
        sentinelServersConfig:
          masterName: mymaster
          sentinelAddresses:
            - "redis://sentinel1:26379"
            - "redis://sentinel2:26379"

Using RLock

The Watchdog Mechanism

Redisson's most valuable feature is the watchdog. When you lock with a lease time of 30 seconds, Redisson spawns a background task that renews the lock every 10 seconds—as long as the JVM process that acquired the lock is still alive. If the process crashes, the watchdog stops renewing, and the lock expires naturally after the lease time.

plaintext
Acquire lock (30s lease)


Every 10s: watchdog extends lock back to 30s


Unlock() called ──→ watchdog cancelled ──→ lock deleted

If you omit the lease time in tryLock, Redisson defaults to 30 seconds and enables the watchdog automatically.

Reentrant Locks

RLock is reentrant. The same thread can acquire the same lock multiple times, and it must unlock the same number of times to release it fully.

Lock Fairness

By default, Redisson locks are non-fair: the next acquirer is arbitrary. Under high contention, this can lead to thread starvation. Redisson provides a fair variant:

java
RLock fairLock = redissonClient.getFairLock("fairLock");

Fair locks queue waiting threads in arrival order, which reduces throughput slightly but guarantees that every waiter eventually acquires the lock.

Read-Write Locks

When multiple threads read a resource but only one writes, a read-write lock improves concurrency:

A Complete Controller Example

FAQ

What is the Redlock algorithm, and do I need it?

Redlock is an algorithm for distributed locking across multiple independent Redis masters. It is designed for environments where a single Redis instance is a single point of failure. In practice, most teams use a single Redis with Sentinel or Cluster, combined with Redisson's watchdog, and find it sufficient. Redlock adds significant complexity; adopt it only after proving that simpler approaches fail your availability requirements.

Can I use Redis distributed locks across microservices?

Yes, as long as every service connects to the same Redis (or Redis Cluster). The lock key is a global namespace. Prefix keys with your service name to avoid collisions: "inventory:lock:stock:42" vs "payment:lock:invoice:99".

What happens if the database transaction rolls back but the lock was already released?

Release the lock only after the database transaction commits. In Spring, use @Transactional and release the lock in a finally block after the transactional method returns. If the transaction rolls back, the lock remains held until the lease expires, which prevents other processes from seeing inconsistent data.

Is SET key value NX EX atomic?

Yes. Since Redis 2.6.12, SET supports the NX and EX modifiers together in a single command. This replaces the older, non-atomic SETNX + EXPIRE pair. Always use the combined form:

bash
SET lock:resource token NX EX 30