Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

Distributed Locking with Redis in Spring Boot using Spring Integration

Tech 2

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.
Tags: spring-boot

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.