Developer Playground

Distributed Locks in Spring Boot: Implementation Options and Best Practices

Updated: July 6, 2025

What are Distributed Locks?

Distributed locks are synchronization mechanisms used in distributed systems to prevent multiple processes, services, or servers from concurrently executing critical sections of code or accessing shared resources simultaneously.

  • Provides mutually exclusive access in distributed environments
  • Helps prevent race conditions across multiple application instances
  • Ensures integrity of shared resources and data consistency
  • Critical for horizontally scaled applications
  • Typically implemented using an external, shared data store
Distributed Lock Architecture App Instance 1 App Instance 2 App Instance 3 Redis Lock Storage Shared Resource Acquire Lock Lock Attempt (Failed) Waiting for Lock Protected Access Has Lock Denied Waiting Only one application instance can access the resource at a time

When to Use Distributed Locks

Distributed locks are essential in several scenarios where multiple application instances need coordinated access to shared resources:

  • Scheduled Tasks - Prevent duplicate execution of scheduled jobs across multiple nodes
  • Resource-Intensive Operations - Ensure only one instance performs heavy computation
  • Batch Processing - Coordinate batch processing to avoid duplicate work
  • Database Migrations - Ensure only one instance runs schema updates
  • Cache Invalidation - Coordinate cache refresh operations
  • Finite Resources - Manage limited connections or third-party API quotas
  • Critical Business Logic - Prevent race conditions in critical operations like payment processing

Redis-Based Implementation

Redis is one of the most popular choices for implementing distributed locks due to its speed, simplicity, and built-in features:

  • In-memory data store with persistence options
  • Atomic operations critical for lock reliability
  • Support for key expiration (automatic lock release)
  • Widely used and supported in the Spring ecosystem
  • High performance with low latency
  • Scalable through Redis Cluster

1. Simple Redis Distributed Lock

The most straightforward way to implement a distributed lock with Redis is by using the SET key value NX PX expiration_time command for acquisition and a Lua script for safe release.

Basic Lock Principles:

  1. Acquire lock using SET with NX (set-if-not-exists) and an expiration time (PX).
  2. Generate a unique lock identifier (e.g., UUID) as the lock value to ensure only the lock owner can release it.
  3. Execute the critical section if the lock is successfully acquired.
  4. Release the lock using a Lua script to ensure atomic check-and-delete (verifying the unique ID before deletion).
  5. Include auto-expiry (via PX) as a safety mechanism for processes that crash before releasing the lock.

⚠️ Critical Limitations of Simple Redis Locks

While simple and efficient, this approach has a critical single point of failure (SPOF) if relying on a single Redis master instance. If the Redis master goes down, all active locks are lost. More importantly, in a **master-replica setup**, if the master fails before the lock data is replicated to its replica, and the replica is promoted to master, a **race condition** can occur. The new master won't have the original lock, allowing another client to acquire it, leading to two clients concurrently holding the "same" lock. This can severely compromise data integrity in high-criticality applications.

2. Redlock Algorithm (for Enhanced Safety)

To address the limitations of simple Redis locks, especially the master-replica failover issue, the **Redlock algorithm** was proposed. Redlock provides a higher guarantee of safety by operating on **multiple, completely independent Redis master instances**.

How Redlock Works:

  1. A client attempts to acquire the lock on N (typically 5, an odd number) independent Redis master instances.
  2. For each instance, it sends the SET key value NX PX expiration_time command, with a short timeout.
  3. The lock is considered acquired only if the client successfully obtains it from a majority of the Redis instances (N/2 + 1).
  4. The client calculates the time elapsed during acquisition. If this time is greater than the lock's validity time, the lock is considered invalid and all acquired locks are released.
  5. To release the lock, the client sends a DEL command (using the safe Lua script) to all N instances, regardless of whether it initially acquired the lock on them.

**Redlock's strength lies in its distributed consensus model.** By requiring a majority vote from independent instances, it drastically reduces the chance of two clients simultaneously acquiring the same lock, even during complex failure scenarios like network partitions or master failovers where replication might lag.

⚠️ Redlock vs. Redis Cluster

It's crucial to understand that Redlock uses multiple independent Redis instances. While a Redis Cluster also involves multiple masters, its primary purpose is data sharding and automatic failover within a single logical cluster. Redlock's safety guarantees come from the *independence* of the Redis instances involved in the quorum, not necessarily from them being part of a single Redis Cluster. You could set up 5 separate standalone Redis servers, or 5 separate Master-Replica groups, and apply Redlock across them.

For production systems requiring the highest reliability for critical locks, Redlock (often implemented via a library like Redisson) or dedicated distributed coordination systems like Zookeeper or etcd are recommended.

Redis Lock Basic Implementation

⚠️ Critical: Safe Lock Release

Using a simple DEL command to release locks is dangerous and can lead to serious concurrency issues. Always use a Lua script that atomically verifies the lock owner before deletion to ensure that only the process that acquired the lock can release it.

-- NEVER use simple DEL for release:
DEL lock:myresource -- UNSAFE: Any client can delete the lock!

-- ALWAYS use atomic check-and-delete:
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

This Lua script ensures that only the lock owner (identified by the unique_value) can release the lock.

Implementation Options in Spring Boot

Spring Boot offers several ways to implement distributed locks. Let's explore the four main approaches, each with its own strengths and use cases.

  • Spring Integration - Provides comprehensive messaging solutions with distributed lock abstractions
  • ShedLock - A lightweight library specifically designed for preventing duplicate scheduled task execution
  • Redisson - A Redis client that offers various distributed objects and services
  • Custom Bean Post Processor - Implementing distributed locks with custom annotations

In the following sections, we'll examine each of these implementation options in detail, with code examples and analysis of their pros and cons.

1. Spring Integration

Spring Integration provides a comprehensive messaging solution that includes distributed lock abstractions, particularly useful for applications already using Spring Integration.

Spring Integration Lock Architecture Spring Application Context Scheduler RedisConnectionFactory RedisLockRegistry DistributedTaskService

Maven Dependencies

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-core</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Configuration

@Configuration
public class RedisLockConfiguration {

    @Bean
    public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
        return new RedisLockRegistry(
            redisConnectionFactory,
            "lock-registry",
            30000); // 30s expiry
    }
}

Usage Example

@Service
public class DistributedTaskService {

    private final RedisLockRegistry redisLockRegistry;

    @Autowired
    public DistributedTaskService(RedisLockRegistry redisLockRegistry) {
        this.redisLockRegistry = redisLockRegistry;
    }

    @Scheduled(fixedRate = 60000)
    public void scheduledTask() {
        Lock lock = redisLockRegistry.obtain("scheduled-task-lock");

        boolean acquired = false;
        try {
            acquired = lock.tryLock(2, TimeUnit.SECONDS);
            if (acquired) {
                // Critical section - only one instance will execute this
                performTask();
            } else {
                // Lock not acquired, handle accordingly
                log.info("Task already running on another instance");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (acquired) {
                lock.unlock();
            }
        }
    }

    private void performTask() {
        // Task implementation
        log.info("Executing critical task at {}", Instant.now());
    }
}

Advantages and Disadvantages

Advantages

  • Integrates well with Spring ecosystem
  • Standard Java Lock interface
  • Supports multiple lock registry implementations
  • Built-in lease time management
  • Good for applications already using Spring Integration

Disadvantages

  • Spring Integration is a heavy dependency
  • Manual lock handling is error-prone
  • No declarative annotation-based locking
  • Less flexible lock customization
  • Error recovery requires careful implementation

2. ShedLock

ShedLock is a lightweight library specifically designed to prevent duplicate execution of scheduled tasks in distributed environments. It's particularly well-suited for scheduled tasks and offers a simple, annotation-based approach.

ShedLock Limitations

It's important to understand that ShedLock is specifically designed for coordinating scheduled tasks across multiple instances and is not intended as a general-purpose distributed lock mechanism. ShedLock focuses on preventing concurrent execution of scheduled methods rather than providing locks for arbitrary code blocks.

If you need distributed locks for general application logic, consider using Redisson or Spring Integration instead. Using ShedLock outside its intended use case may lead to unexpected behavior or decreased reliability.

ShedLock Architecture Spring Application Context Redis LockProvider ShedLock LockManager Spring TaskScheduler @ScheduledLock Tasks Intercepts Uses Schedules Manages Locks

Maven Dependencies

<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>4.43.0</version>
</dependency>

<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-redis-spring</artifactId>
    <version>4.43.0</version>
</dependency>

Configuration

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class SchedulerConfiguration {

    @Bean
    public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
        return new RedisLockProvider(connectionFactory, "shedlock:");
    }
}

Usage Example

@Component
public class ScheduledTasks {

    private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

    @Scheduled(fixedRate = 60000)
    @SchedulerLock(
        name = "scheduledTaskName",
        lockAtLeastFor = "10s",
        lockAtMostFor = "50s"
    )
    public void scheduledTask() {
        // This task will only run on one instance at a time
        // If another instance tries to execute it while locked,
        // the task will be skipped silently
        log.info("Executing scheduled task at {}", Instant.now());

        // Task implementation
        performTask();
    }

    private void performTask() {
        // Implementation of the actual task
        try {
            // Simulate work
            Thread.sleep(5000);
            log.info("Task completed successfully");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("Task interrupted", e);
        }
    }
}

Lock Parameters Explained

  • name: Unique identifier for the lock (mandatory)
  • lockAtMostFor: Maximum time the lock will be held, even if the task doesn't complete (safety measure)
  • lockAtLeastFor: Minimum time the lock will be held, preventing task execution on other nodes

Advantages and Disadvantages

Advantages

  • Simple, declarative annotation-based approach
  • Lightweight with minimal dependencies
  • No need for manual lock handling
  • Multiple storage backends (Redis, JDBC, MongoDB, etc.)
  • Specialized for scheduled tasks
  • Very easy to implement and configure

Disadvantages

  • Limited to scheduled tasks only
  • Fails silently (skips execution rather than waiting)
  • No built-in support for lock extension
  • Less granular control over lock behavior
  • Not suitable for interactive or real-time operations

3. Redisson

Redisson is a powerful Redis client for Java that offers various distributed objects and services, including robust distributed lock implementations. It provides advanced features like automatic lock renewal, fair locks, read/write locks, and semaphores.

Redisson's Pub/Sub Lock Mechanism

One of Redisson's key advantages is its efficient implementation using Redis Pub/Sub messaging. Unlike naive polling approaches, when a lock is released, Redisson publishes a message to notify waiting clients immediately. This provides several benefits:

  • Reduced latency for lock acquisition by waiting clients
  • Lower CPU consumption compared to continuous polling
  • Decreased Redis server load with fewer GET operations
  • More efficient network utilization

This efficient notification system makes Redisson particularly well-suited for scenarios where locks may be held for variable durations or where quick handover of locks between processes is important.

Redisson Lock Architecture Redisson Distributed Locks Redis Server Lock Data Watchdog Thread Redisson Client RLock RFairLock RReadWriteLock RMultiLock RSemaphore RPermitExpirableSemaphore Lua Scripts

Maven Dependencies

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.20.0</version>
</dependency>

<!-- Or use the Spring Boot starter -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.20.0</version>
</dependency>

Configuration

@Configuration
public class RedissonConfig {

    // START MODIFICATION 2.1: Redisson Config for Redlock
    // For a true Redlock setup, Redisson needs to connect to multiple independent Redis instances.
    // This is typically achieved by configuring multiple single servers or using replicated/master-slave configurations
    // where each "node" in the list is an independent Redis master (potentially with its own replicas).
    // Here, we provide an example of how you would configure Redisson to connect to multiple independent Redis servers
    // which are used as votes for the Redlock algorithm.

    // Example 1: Multiple independent single Redis server clients for explicit RedissonRedLock (most direct Redlock implementation)
    // You would define multiple @Bean methods for each RedissonClient.
    // For instance:
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient1() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient2() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6380");
        return Redisson.create(config);
    }
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient3() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6381");
        return Redisson.create(config);
    }

    // Example 2: Configuring a RedissonClient to manage a replicated setup (multiple independent masters or master-replica sets)
    // This is often how Redisson helps with Redlock under the hood for its RLock.
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useReplicatedServers() // Use this for multiple independent master nodes or master-replica groups
              .addNodeAddress("redis://localhost:6379", "redis://localhost:6380", "redis://localhost:6381") // Example: 3 independent Redis instances
              .setScanInterval(2000) // Scan interval for detecting new masters/slaves (if applicable)
              .setConnectionPoolSize(64)
              .setConnectionMinimumIdleSize(10)
              .setConnectTimeout(10000);
        
        // OR for Redis Cluster where locks might be sharded, use:
        // config.useClusterServers()
        //         .addNodeAddress("redis://host1:7000", "redis://host2:7001", "redis://host3:7002");

        return Redisson.create(config);
    }
    // END MODIFICATION 2.1
}

Basic Lock Usage

@Service
public class RedissonLockService {

    private final RedissonClient redissonClient;
    private static final Logger log = LoggerFactory.getLogger(RedissonLockService.class);

    // START MODIFICATION 2.2: Adjust constructor if you define multiple RedissonClient beans for Redlock
    // If using explicit RedissonRedLock, you might inject multiple clients here:
    // private final RedissonClient redissonClient1;
    // private final RedissonClient redissonClient2;
    // private final RedissonClient redissonClient3;
    //
    // @Autowired
    // public RedissonLockService(RedissonClient redissonClient, RedissonClient redissonClient1, RedissonClient redissonClient2, RedissonClient redissonClient3) {
    //     this.redissonClient = redissonClient; // This is for general RLock usage if configured (e.g., useReplicatedServers)
    //     this.redissonClient1 = redissonClient1; // For explicit Redlock method
    //     this.redissonClient2 = redissonClient2;
    //     this.redissonClient3 = redissonClient3;
    // }
    // If only using the single 'redissonClient' from the RedissonConfig above (configured for multiple nodes),
    // then the current constructor is fine.
    
    @Autowired
    public RedissonLockService(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }
    // END MODIFICATION 2.2

    public void executeWithLock(String lockName, Runnable task) {
        RLock lock = redissonClient.getLock(lockName);

        try {
            // Try to acquire lock with 10s wait time and 30s lease time
            boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);

            if (isLocked) {
                try {
                    log.info("Lock acquired, executing task {}", lockName);
                    task.run();
                } finally {
                    lock.unlock();
                    log.info("Lock released for {}", lockName);
                }
            } else {
                log.warn("Failed to acquire lock for {}", lockName);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("Lock acquisition interrupted for {}", lockName, e);
        }
    }
    
    // START MODIFICATION 2.3: Add explicit Redlock usage example
    /**
     * **Explicit Redlock algorithm lock acquisition attempt (using RedissonRedLock)**
     * To use this method, multiple RedissonClient beans must be defined and injected in RedissonConfig.
     * @param lockKey Resource key to lock
     * @param waitTimeMs Maximum time to wait for lock (milliseconds)
     * @param leaseTimeMs Lock validity period (milliseconds)
     * @param redissonClients Independent RedissonClient instances participating in Redlock
     * @return Whether lock acquisition was successful
     */
    public boolean tryRedLock(String lockKey, long waitTimeMs, long leaseTimeMs, RedissonClient... redissonClients) {
        RLock[] locks = new RLock[redissonClients.length];
        for (int i = 0; i < redissonClients.length; i++) {
            locks[i] = redissonClients[i].getLock(lockKey);
        }

        RedissonRedLock redLock = new RedissonRedLock(locks);
        try {
            boolean acquired = redLock.tryLock(waitTimeMs, leaseTimeMs, TimeUnit.MILLISECONDS);
            if (acquired) {
                System.out.println(Thread.currentThread().getName() + " acquired RedLock: " + lockKey);
            } else {
                System.out.println(Thread.currentThread().getName() + " failed to acquire RedLock: " + lockKey);
            }
            return acquired;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("RedLock acquisition interrupted: " + e.getMessage());
            return false;
        } finally {
            if (redLock.isLocked() && redLock.isHeldByCurrentThread()) { // Check if current thread holds the lock
                redLock.unlock();
                System.out.println(Thread.currentThread().getName() + " released RedLock: " + lockKey);
            }
        }
    }
    // END MODIFICATION 2.3
}

Advanced Lock Types

@Service
public class AdvancedLockService {

    private final RedissonClient redissonClient;

    @Autowired
    public AdvancedLockService(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    // Fair lock - guarantees order of acquisition
    public void executeFairLock(String lockName, Runnable task) {
        RLock fairLock = redissonClient.getFairLock(lockName);
        executeWithLock(fairLock, task);
    }

    // Read lock - allows multiple readers but exclusive writers
    public void executeReadLock(String lockName, Runnable task) {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockName);
        RLock readLock = rwLock.readLock();
        executeWithLock(readLock, task);
    }

    // Write lock - exclusive access
    public void executeWriteLock(String lockName, Runnable task) {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockName);
        RLock writeLock = rwLock.writeLock();
        executeWithLock(writeLock, task);
    }

    // Multi lock - acquires multiple locks atomically
    public void executeMultiLock(List<String> lockNames, Runnable task) {
        RLock[] locks = lockNames.stream()
            .map(name -> redissonClient.getLock(name))
            .toArray(RLock[]::new);

        RLock multiLock = redissonClient.getMultiLock(locks);
        executeWithLock(multiLock, task);
    }

    private void executeWithLock(RLock lock, Runnable task) {
        try {
            boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (isLocked) {
                try {
                    task.run();
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Auto-Renewing Lock with Watchdog

@Service
public class WatchdogLockService {

    private final RedissonClient redissonClient;
    private static final Logger log = LoggerFactory.getLogger(WatchdogLockService.class);

    @Autowired
    public WatchdogLockService(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    public void executeLongRunningTask(String lockName, Runnable task) {
        RLock lock = redissonClient.getLock(lockName);

        try {
            // When no leaseTime is provided, the Watchdog automatically extends the lock
            // Default watchdog timeout is 30 seconds
            lock.lock();

            log.info("Lock acquired with watchdog for {}", lockName);

            try {
                // Execute long-running task
                // The lock will be automatically extended until explicitly unlocked
                task.run();
            } finally {
                lock.unlock();
                log.info("Lock released for {}", lockName);
            }
        } catch (Exception e) {
            log.error("Error in long-running task for {}", lockName, e);
            throw e;
        }
    }

    // You can also customize the watchdog timeout
    public void executeWithCustomWatchdog(String lockName, Runnable task) {
        Config config = new Config();
        config.setLockWatchdogTimeout(60000); // 1 minute
        RedissonClient customClient = Redisson.create(config);

        RLock lock = customClient.getLock(lockName);
        try {
            lock.lock();
            task.run();
        } finally {
            lock.unlock();
            customClient.shutdown();
        }
    }
}

Annotation-Based Locking

// Configuration
@Configuration
@EnableAspectJAutoProxy
public class RedissonLockAspectConfig {

    @Bean
    public LockAspect lockAspect(RedissonClient redissonClient) {
        return new LockAspect(redissonClient);
    }
}

// Custom annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLocked {
    String name();                   // Lock name
    boolean fair() default false;    // Use fair lock
    long waitTime() default 10L;    // Seconds to wait for lock
    long leaseTime() default -1L;   // -1 for watchdog, otherwise seconds
}

// Lock aspect
@Aspect
public class LockAspect {

    private final RedissonClient redissonClient;

    public LockAspect(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    @Around("@annotation(redissonLocked)")
    public Object aroundLock(ProceedingJoinPoint joinPoint, RedissonLocked redissonLocked) throws Throwable {
        String lockName = redissonLocked.name();
        RLock lock = redissonLocked.fair() ?
                redissonClient.getFairLock(lockName) :
                redissonClient.getLock(lockName);

        boolean locked = false;
        try {
            if (redissonLocked.leaseTime() == -1) {
                locked = lock.tryLock(redissonLocked.waitTime(), TimeUnit.SECONDS);
            } else {
                locked = lock.tryLock(redissonLocked.waitTime(),
                            redissonLocked.leaseTime(), TimeUnit.SECONDS);
            }

            if (locked) {
                return joinPoint.proceed();
            } else {
                throw new RuntimeException("Failed to acquire lock: " + lockName);
            }
        } finally {
            if (locked) {
                lock.unlock();
            }
        }
    }
}

// Usage example
@Service
public class AnnotatedLockService {

    @RedissonLocked(name = "critical-operation", fair = true, waitTime = 5, leaseTime = 30)
    public void criticalOperation() {
        // This method will be protected by a distributed lock
        // with 5s wait time and 30s lease time
    }

    @RedissonLocked(name = "long-running-task", leaseTime = -1)
    public void longRunningTask() {
        // This method will use watchdog for automatic lock renewal
    }
}

Advantages and Disadvantages

Advantages

  • Advanced lock capabilities (fair locks, read/write locks, etc.)
  • Automatic lock renewal through Watchdog mechanism
  • Support for Redis Cluster
  • Excellent performance and reliability
  • Comprehensive API for distributed operations
  • Low-level control over lock behavior
  • Robust Lua script-based implementation

Disadvantages

  • Heavier dependency than simpler solutions
  • Steeper learning curve
  • No built-in annotation support (requires custom implementation)
  • Configuration can be complex
  • May be overkill for simple locking requirements
  • More resource-intensive than other solutions
  • Troubleshooting can be challenging due to complexity
  • Additional Redis dependency if not already using Redis

4. Custom Bean Post Processor

The fourth approach involves creating a custom annotation and a Bean Post Processor to apply distributed locks in a declarative way. This method offers a flexible, lightweight, and customizable solution that can be tailored to specific requirements.

Custom Bean Post Processor Architecture Spring Application Context Redis Lock Storage @DistributedLock Custom Annotation LockBeanPostProcessor Processes Beans Proxy Generation Service with Locked Methods Enhanced Proxy

This approach involves creating a custom annotation to mark methods that need locking, then using Spring's Bean Post Processor mechanism to create proxies around these methods. These proxies handle the distributed lock acquisition and release before and after the method execution.

Maven Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>

<!-- For CGLIB proxies -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
</dependency>

Step 1: Create Custom Annotation

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {

    /**
     * Lock name. If empty, the method name will be used.
     * Can use SpEL expressions. For example, "#productId" will be resolved
     * to the method parameter named "productId"
     */
    String name() default "";

    /**
     * Timeout in milliseconds for acquiring the lock.
     * If the lock cannot be acquired within this time, the method will throw exception.
     */
    long timeout() default 5000;

    /**
     * Lock expiration time in milliseconds.
     * After this time, the lock will be automatically released.
     */
    long expiration() default 30000;

    /**
     * Whether to retry acquiring the lock if failed.
     */
    boolean retry() default false;

    /**
     * Action to take if lock acquisition fails.
     */
    LockAction failureAction() default LockAction.EXCEPTION;

    /**
     * Actions that can be taken when lock acquisition fails
     */
    enum LockAction {
        /** Throw exception */
        EXCEPTION,

        /** Skip method execution */
        SKIP,

        /** Execute anyway (useful for non-critical operations) */
        EXECUTE_ANYWAY
    }
}

Step 2: Create Redis Lock Service

@Service
public class RedisLockService {

    private final StringRedisTemplate redisTemplate;
    private static final String LOCK_PREFIX = "distributed:lock:";
    private static final Logger log = LoggerFactory.getLogger(RedisLockService.class);

    @Autowired
    public RedisLockService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean acquireLock(String lockName, String lockValue, long expirationMs) {
        String key = LOCK_PREFIX + lockName;
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(key, lockValue, Duration.ofMillis(expirationMs));

        return Boolean.TRUE.equals(result);
    }

    public boolean releaseLock(String lockName, String lockValue) {
        String key = LOCK_PREFIX + lockName;
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                       "return redis.call('del', KEYS[1]) else return 0 end";

        // Execute the Lua script atomically
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(key),
            lockValue
        );

        return Long.valueOf(1L).equals(result);
    }

    public boolean tryAcquireLock(String lockName, String lockValue, long timeoutMs, long expirationMs) {
        long deadline = System.currentTimeMillis() + timeoutMs;
        boolean acquired = false;

        while (!acquired && System.currentTimeMillis() < deadline) {
            acquired = acquireLock(lockName, lockValue, expirationMs);

            if (!acquired) {
                try {
                    Thread.sleep(100); // Small delay before retry
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return false;
                }
            }
        }

        return acquired;
    }
}

Step 3: Create Expression Evaluator for Dynamic Lock Names

@Component
public class ExpressionEvaluator {

    private final ExpressionParser parser = new SpelExpressionParser();

    public String evaluate(String expression, Method method, Object[] args) {
        if (!expression.contains("#")) {
            return expression;
        }

        EvaluationContext context = new StandardEvaluationContext();
        Parameter[] parameters = method.getParameters();

        for (int i = 0; i < parameters.length; i++) {
            context.setVariable(parameters[i].getName(), args[i]);
        }

        return parser.parseExpression(expression).getValue(context, String.class);
    }
}

Step 4: Create Bean Post Processor

@Component
public class DistributedLockBeanPostProcessor implements BeanPostProcessor {

    private final RedisLockService lockService;
    private final ExpressionEvaluator expressionEvaluator;
    private static final Logger log = LoggerFactory.getLogger(DistributedLockBeanPostProcessor.class);

    @Autowired
    public DistributedLockBeanPostProcessor(RedisLockService lockService, ExpressionEvaluator expressionEvaluator) {
        this.lockService = lockService;
        this.expressionEvaluator = expressionEvaluator;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class targetClass = AopUtils.getTargetClass(bean);

        if (!hasDistributedLockMethods(targetClass)) {
            return bean;
        }

        ProxyFactory proxyFactory = new ProxyFactory(bean);
        proxyFactory.addAdvice(new DistributedLockInterceptor(lockService, expressionEvaluator));

        return proxyFactory.getProxy();
    }

    private boolean hasDistributedLockMethods(Class clazz) {
        Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
        for (Method method : methods) {
            if (method.isAnnotationPresent(DistributedLock.class)) {
                return true;
            }
        }
        return false;
    }

    private class DistributedLockInterceptor implements MethodInterceptor {

        private final RedisLockService lockService;
        private final ExpressionEvaluator expressionEvaluator;

        public DistributedLockInterceptor(RedisLockService lockService, ExpressionEvaluator expressionEvaluator) {
            this.lockService = lockService;
            this.expressionEvaluator = expressionEvaluator;
        }

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            Method method = invocation.getMethod();

            if (!method.isAnnotationPresent(DistributedLock.class)) {
                return invocation.proceed();
            }

            DistributedLock annotation = method.getAnnotation(DistributedLock.class);
            String lockName = getLockName(annotation, method, invocation.getArguments());
            String lockValue = UUID.randomUUID().toString();
            boolean lockAcquired = false;

            try {
                if (annotation.retry()) {
                    lockAcquired = lockService.tryAcquireLock(lockName, lockValue,
                                                      annotation.timeout(), annotation.expiration());
                } else {
                    lockAcquired = lockService.acquireLock(lockName, lockValue, annotation.expiration());
                }

                if (lockAcquired) {
                    return invocation.proceed();
                } else {
                    switch (annotation.failureAction()) {
                        case EXCEPTION:
                            throw new DistributedLockAcquisitionException(
                                "Failed to acquire distributed lock: " + lockName);
                        case SKIP:
                            log.info("Skipping method execution due to lock acquisition failure: ", lockName);
                            return null;
                        case EXECUTE_ANYWAY:
                            log.warn("Executing method despite lock acquisition failure: ", lockName);
                            return invocation.proceed();
                        default:
                            throw new IllegalStateException("Unknown failure action: " + annotation.failureAction());
                    }
                }
            } finally {
                if (lockAcquired) {
                    lockService.releaseLock(lockName, lockValue);
                }
            }
        }

        private String getLockName(DistributedLock annotation, Method method, Object[] args) {
            String lockName = annotation.name();

            if (StringUtils.isEmpty(lockName)) {
                return method.getDeclaringClass().getSimpleName() + "." + method.getName();
            }

            return expressionEvaluator.evaluate(lockName, method, args);
        }
    }

    public static class DistributedLockAcquisitionException extends RuntimeException {
        public DistributedLockAcquisitionException(String message) {
            super(message);
        }
    }
}

Step 5: Usage Example

@Service
public class ProductService {

    private final ProductRepository productRepository;
    private static final Logger log = LoggerFactory.getLogger(ProductService.class);

    @Autowired
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    // Simple lock using method name
    @DistributedLock(timeout = 2000, expiration = 10000)
    public void updateInventory() {
        log.info("Updating inventory...");
        // Implementation
    }

    // Lock with dynamic name based on method parameter
    @DistributedLock(name = "product:#productId", retry = true)
    public void updateProductStock(String productId, int quantity) {
        log.info("Updating stock for product {} with quantity {}", productId, quantity);

        // Critical section - ensure only one thread updates stock for this product
        Product product = productRepository.findById(productId);
        product.setStockQuantity(product.getStockQuantity() + quantity);
        productRepository.save(product);
    }

    // Lock with custom failure handling
    @DistributedLock(
        name = "daily-report",
        failureAction = DistributedLock.LockAction.SKIP,
        expiration = 60000
    )
    public void generateDailyReport() {
        log.info("Generating daily report...");
        // Implementation
    }
}

Advantages and Disadvantages

Advantages

  • Fully customizable to specific needs
  • Declarative, annotation-based approach
  • Support for dynamic lock names using SpEL
  • Flexible failure handling strategies
  • No additional library dependencies
  • Lightweight with low overhead
  • Can work with any Redis client

Disadvantages

  • Requires custom implementation and maintenance
  • No built-in support for advanced features (e.g., fair locks)
  • Potential for errors in lock management
  • Needs thorough testing for reliability
  • Lacks community support of established libraries
  • May require deeper understanding of Spring AOP
  • **Inherits safety limitations of simple Redis locks** (e.g., during master failover with replication lag), as it uses StringRedisTemplate directly.

Comparison of Approaches

Now that we've explored the four main approaches to implementing distributed locks in Spring Boot, let's compare them across various dimensions to help you choose the right solution for your needs.

Feature Comparison

Feature Spring Integration ShedLock Redisson Custom BPP
Complexity Medium Low Medium-High High
Setup Effort Medium Low Low High
Annotation Support No Yes Via AOP (Custom) Yes
Flexibility Medium Low High High
Scope of Usage General Purpose Scheduled Tasks Only General Purpose General Purpose
Advanced Features Limited Limited Extensive Customizable
Lock Renewal No No Yes (Watchdog) Needs Custom Implementation
Storage Options Redis, Zookeeper, JDBC Redis, JDBC, MongoDB, etc. Redis Depends on Implementation
Dependencies Spring Integration ShedLock Redisson None (Standard Spring)
Maintenance Low Low Low High

When to Choose Each Approach

Choose Spring Integration when:

  • You're already using Spring Integration in your project
  • You need a simple solution with standard Java Lock interface
  • You prefer using well-established Spring components
  • You don't mind manual lock handling in code

Choose ShedLock when:

  • You only need locks for scheduled tasks
  • You prefer a simple, annotation-based approach
  • You want minimal setup effort
  • You need database-backed locks (JDBC, MongoDB, etc.)
  • You prefer skipping execution over waiting for lock acquisition

Choose Redisson when:

  • You need advanced lock features (fair locks, read/write locks, etc.)
  • You have long-running tasks requiring lock renewal
  • You're already using Redis extensively
  • You need high reliability and performance
  • You value a mature, actively maintained library
  • You need multiple lock types (MultiLock, ReadWriteLock, etc.)
  • **You require Redlock's strong safety guarantees for critical operations against Redis failures (e.g., master failover with replication lag), understanding the need for multiple independent Redis instances.**

Choose Custom Bean Post Processor when:

  • You have specific requirements not met by other solutions
  • You prefer complete control over the implementation
  • You want to minimize external dependencies
  • You need custom lock behavior (dynamic naming, failure handling, etc.)
  • You're comfortable with Spring AOP and Bean Post Processors
  • You're willing to maintain the implementation yourself
  • **You understand this approach typically implements a simple Redis lock and inherits its safety limitations during Redis master failover unless specifically extended to Redlock principles.**

Best Practices

Regardless of the distributed lock implementation you choose, following these best practices will help ensure a robust and reliable solution:

1. Lock Granularity

Be specific with lock names to avoid unnecessary contention. Use fine-grained locks when possible, instead of coarse-grained locks.

✅ Good Practice

// Lock specific to a product
@DistributedLock(name = "product:" + "#productId")
public void updateProductStock(String productId, int quantity) {
    // Implementation
}

❌ Bad Practice

// Too broad, causes unnecessary contention
@DistributedLock(name = "inventory")
public void updateProductStock(String productId, int quantity) {
    // Implementation
}

2. Lock Timeouts

Set appropriate timeouts for both lock acquisition and expiration. Balance between preventing deadlocks and giving enough time for operations to complete.

✅ Good Practice

// Reasonable timeouts based on operation duration
boolean acquired = lock.tryLock(5, TimeUnit.SECONDS); // Wait up to 5s
if (acquired) {
    try {
        // Critical section
    } finally {
        lock.unlock();
    }
}

❌ Bad Practice

// Indefinite waiting can cause application hanging
lock.lock(); // Will wait forever - dangerous!
try {
    // Critical section
} finally {
    lock.unlock();
}

3. Error Handling

Always release locks in a finally block to prevent deadlocks. Handle lock acquisition failures gracefully.

✅ Good Practice

boolean acquired = false;
try {
    acquired = lock.tryLock(5, TimeUnit.SECONDS);
    if (acquired) {
        // Critical section
    } else {
        // Handle lock acquisition failure
        log.warn("Could not acquire lock");
    }
} catch (Exception e) {
    log.error("Error while processing with lock", e);
} finally {
    if (acquired) {
        try {
            lock.unlock();
        } catch (Exception e) {
            log.error("Error releasing lock", e);
        }
    }
}

❌ Bad Practice

// No error handling, can lead to zombie locks
lock.lock();
// Critical section that might throw exception
processData(); // If this throws, lock is never released!
lock.unlock();

4. Lock Naming

Use consistent naming conventions for locks. Consider prefixing locks with application or module name to avoid conflicts.

✅ Good Practice

// Clear, hierarchical naming convention
String lockName = "app:order:processing:" + orderId;
RLock lock = redissonClient.getLock(lockName);

❌ Bad Practice

// Generic names can cause conflicts
String lockName = "processing";
RLock lock = redissonClient.getLock(lockName);

5. Redis Configuration

For production environments, consider using Redis Cluster for high availability or Redis Sentinel for failover protection. Properly configure connection pools and timeouts.

✅ Good Practice

// High availability configuration
Config config = new Config();
config.useClusterServers()
      .setScanInterval(2000)
      .addNodeAddress("redis://node1:6379", "redis://node2:6379")
      .setRetryAttempts(3)
      .setRetryInterval(1500);

RedissonClient redisson = Redisson.create(config);

❌ Bad Practice

// Single point of failure, no retry configuration
Config config = new Config();
config.useSingleServer()
      .setAddress("redis://localhost:6379");

RedissonClient redisson = Redisson.create(config);

6. Testing

Thoroughly test distributed locking logic, including edge cases like timeout, lock expiration, process crashes, etc.

✅ Good Practice

Use integration tests with embedded Redis and simulate various failure scenarios. Consider using tools like Toxiproxy to simulate network issues.

❌ Bad Practice

Testing only the happy path, not considering failure cases, or relying solely on unit tests without integration tests.

7. Monitoring and Alerting

Implement monitoring for lock acquisition times, failures, and zombie locks. Set up alerting for unusual lock behavior.

✅ Good Practice

// With metrics tracking
Timer.Sample sample = Timer.start(registry);
try {
    boolean acquired = lock.tryLock(5, TimeUnit.SECONDS);

    if (acquired) {
        // Critical section
    } else {
        meterRegistry.counter("lock.acquisition.failure",
                    "lockName", lockName).increment();
    }
} finally {
    sample.stop(meterRegistry.timer("lock.acquisition.time",
                                         "lockName", lockName));
}

Conclusion

Distributed locks are essential for coordinating operations in horizontally scaled applications. When choosing an implementation, consider your specific requirements, existing infrastructure, and the pros and cons of each approach. Remember that distributed locks add complexity to your system, so use them judiciously and follow best practices to ensure reliability.

For most applications, ShedLock offers the easiest solution for scheduled tasks, while Redisson provides the most comprehensive feature set for general-purpose locking needs. Spring Integration is a good middle ground if you're already using the framework, and a custom implementation gives you maximum flexibility at the cost of maintenance overhead.


Advertisement