Distributed Locking with Redis in Spring Boot using Spring Integration
This guide shows how to integrate Redis with Spring Boot to build a lightweight distributed locking mechanism using Spring Integration’s RedisLockRegistry. It also touches on the internal design so you can tune behavior and avoid common pitfalls.
Why Redis-based distributed locks
When multiple application instances need mutual exclusion for critical sections (e.g., preventing double-processing of the same order), a distributed lock is needed. Spring Integration provides a production-ready Redis-backed lock registry (and also supports alternatives like ZooKeeper and JDBC), making it easy to adopt without writing custom locking code.
Dependencies
Add these dependencies to enable Redis, Spring Integration, and the Redis lock registry:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
Configuration
Register a RedisLockRegistry bean. Set a registry prefix and an expiration window to automatically clear abandoned locks.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.integration.redis.util.RedisLockRegistry;
@Configuration
public class LockConfig {
@Bean(destroyMethod = "destroy")
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory connectionFactory) {
// Key prefix "app:locks", 45 seconds expiration
return new RedisLockRegistry(connectionFactory, "app:locks", 45_000);
}
}
Notes:
- registryKey is the prefix under which lock keys are stored in Redis.
- expireAfter defaults to 60_000 ms if not specified. Set it according to you're longest critical section + margin.
A small service wrapper for locking
Encapsulate lock acquisition and release to keep application code clean and safe.
import org.springframework.integration.redis.util.RedisLockRegistry;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
@Service
public class DistributedLockService {
private final RedisLockRegistry registry;
public DistributedLockService(RedisLockRegistry registry) {
this.registry = registry;
}
public <T> T runWithLock(String lockKey, Duration waitTime, LockCallback<T> task) {
Lock lock = registry.obtain(lockKey);
boolean acquired = false;
try {
if (waitTime == null) {
lock.lock();
acquired = true;
} else {
acquired = lock.tryLock(waitTime.toMillis(), TimeUnit.MILLISECONDS);
}
if (!acquired) {
throw new IllegalStateException("Failed to acquire lock for key: " + lockKey);
}
return task.doInLock();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while acquiring lock", ie);
} finally {
if (acquired) {
try {
lock.unlock();
} catch (IllegalMonitorStateException ignored) {
// lock was not held by this thread; ignore
}
}
}
}
@FunctionalInterface
public interface LockCallback<T> {
T doInLock();
}
}
Example endpoint
The example below fires several concurrent tasks competing on the same lock key. Only one task holds the distributed lock at a time; others wait untill it’s released.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
public class LockDemoController {
private final DistributedLockService lockService;
private final ExecutorService pool = Executors.newFixedThreadPool(4);
public LockDemoController(DistributedLockService lockService) {
this.lockService = lockService;
}
@GetMapping("/locks/redis")
public String demo(@RequestParam("key") String key) {
for (int i = 0; i < 10; i++) {
pool.submit(() -> {
lockService.runWithLock(key, Duration.ofSeconds(5), () -> {
try {
Thread.sleep(3000); // simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(LocalDateTime.now());
return null;
});
});
}
return "OK";
}
}
A typical output shows timestamps spaced roughly 3 seconds apart, demonstrating serialized execution:
2020-06-23T11:15:34
2020-06-23T11:15:37
2020-06-23T11:15:40
...
Internal model and behavior
Core constructs
RedisLockRegistry exposes a Lock per logical key and maintains lightweight metadata locally to reduce Redis traffic.
- Unique registry identity:
- Each registry instance generates a random identifier used to tag lock ownership in Redis.
- Local cache of lock objects:
- For each lock key, a Redis-backed Lock wrapper is cached in a ConcurrentMap. Subsequent obtain(key) calls return the same wrapper instance.
Conceptually:
private final String instanceId = java.util.UUID.randomUUID().toString();
private final java.util.concurrent.ConcurrentMap<String, org.springframework.integration.redis.util.RedisLockRegistry.RedisLock> lockCache = new java.util.concurrent.ConcurrentHashMap<>();
Otbaining a lock
Lock objects are created per key on first access and then reused:
public Lock obtain(String key) {
return lockCache.computeIfAbsent(key, k -> new RedisLock(k));
}
Local fast-path and expiration
Each Redis-backed lock uses a local ReentrantLock to avoid hitting Redis when multiple threads in the same JVM contend for the same key. It also tracks the last successful acquisition timestamp to enable cleanup of long-idle locks.
Conceptually:
private final java.util.concurrent.locks.ReentrantLock localGuard = new java.util.concurrent.locks.ReentrantLock();
private volatile long acquiredAtMillis;
- The localGuard is taken before attempting the remote Redis operation, minimizing network chatter under intra-process contention.
- acquiredAtMillis helps the ExpirableLockRegistry implementation evict expired entries.
Blocking acquisition semantics
- lock(): loops until the lock is acquired, pausing briefly between attempts.
- tryLock(long, TimeUnit): waits up to the specified time, returning false on timeout.
- unlock(): releases the lock only if the current registry instance is the recorded owner in Redis.
Under the hood, acquisition and release are implemented with atomic Redis operations guarded by Lua scripts to ensure ownership checks and expiration updates happen atomically. The registry also sets a TTL for the lock key (expireAfter) so that locks are freed if the holder crashes.
Tuning expireAfter
- If your critical section can exceed the default 60 seconds, increase expireAfter when constructing RedisLockRegistry.
- Choose a value longer than the worst-case execution time to avoid premature expiration while still preventing deadlocks if a node dies.