Implementing Distributed and Scheduled Locks Using Redis and Spring Boot (Without ShedLock)!

Introduction

In modern applications, ensuring that scheduled tasks do not run concurrently across distributed systems is crucial for maintaining data integrity and consistency. One common solution for this problem is using distributed locks. In this guide, we will explore how to implement distributed and scheduled locks using Redis and Spring Boot, without relying on the ShedLock library.

We'll start by understanding the concept of distributed locking and why it's necessary in a microservices architecture. Next, we'll set up a Redis server, which will act as our locking mechanism.

https://www.youtube.com/watch?v=yyC5PIgdutw

Then, we'll dive into a step-by-step implementation in a Spring Boot application, demonstrating how to acquire and release locks to ensure that scheduled tasks run safely and without interference.

By the end of this tutorial, you'll have a clear understanding of how to use Redis for distributed locking in your Spring Boot applications, enabling you to manage task scheduling effectively in a distributed environment.

What is a Distributed Lock?

A distributed lock is a technique used in distributed systems to coordinate access to a shared resource or critical section across multiple nodes or processes. It ensures that only one node or process can access or modify the shared resource at a time, preventing concurrent access and potential conflicts. This mechanism is essential for maintaining data consistency and integrity in a distributed environment.

red and gold padlock on chain link fence

Required Tech Stack

  • Java Development Kit (JDK) 21: Ensure you have JDK 21 installed for the latest language features and improvements.

  • Spring Boot 3.0: Simplifies the development of stand-alone, production-grade Spring-based applications.

  • Redis: Acts as our distributed locking mechanism.

  • Spring Data Redis: Enables easy integration with Redis.

  • Maven or Gradle: Use as your build tool.

  • Lombok (Optional): Reduces boilerplate code.

Setting Up the Environment

  1. Install Redis: Follow the official Redis documentation to install Redis on your machine or use a cloud-based service.

  2. Create a Docker Compose File: This file sets up Redis and Redis Insight (a GUI for Redis) using Docker.

     version: '3.8'
     services:
       redis:
         container_name: redis
         image: redis
         command: redis-server --appendonly yes
         ports:
           - "6379:6379"
       redis-insight:
         image: redislabs/redisinsight
         container_name: redis-insight
         ports:
           - "8001:8001"
    

    Run the following command to start the services:

     docker-compose up -d
    
  3. Add Dependencies: In your pom.xml for a Maven project, include the following dependencies:

     <dependencies>
         <!-- Spring Boot Starter Web -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
    
         <!-- Lombok (Optional) -->
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
             <optional>true</optional>
         </dependency>
    
         <!-- Spring Boot Starter Test -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
    
         <!-- Spring Boot Starter Data Redis -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>
    
         <!-- Jedis for Redis operations -->
         <dependency>
             <groupId>redis.clients</groupId>
             <artifactId>jedis</artifactId>
         </dependency>
     </dependencies>
    

Spring Boot Application

  1. Main Application Class: This class is the entry point of our Spring Boot application.

     @SpringBootApplication
     @EnableScheduling
     public class SpringRedisDistributedLockApplication {
         public static void main(String[] args) {
             SpringApplication.run(SpringRedisDistributedLockApplication.class, args);
         }
     }
    
  2. Redis Configuration: Configure Redis connection and templates.

     @Configuration
     public class RedisConfig {
         @Bean
         public RedisConnectionFactory redisConnectionFactory() {
             return new LettuceConnectionFactory("localhost", 6379);
         }
    
         @Bean
         public RedisTemplate<String, Object> redisTemplate() {
             RedisTemplate<String, Object> template = new RedisTemplate<>();
             template.setConnectionFactory(redisConnectionFactory());
             template.setKeySerializer(new StringRedisSerializer());
             template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
             return template;
         }
     }
    
  3. Lock Controller: Expose an endpoint to perform operations with a distributed lock.

     @RestController
     public class LockController {
         @Autowired
         private LockService lockService;
    
         @GetMapping("/perform/{lockKey}")
         public String performOperation(@PathVariable String lockKey) throws InterruptedException {
             lockService.performWithLock(lockKey);
             return "Background Operation completed";
         }
     }
    
  4. Lock Service: Service to handle the lock logic.

     @Service
     @Slf4j
     public class LockService {
         private final RedisDistributedLock lock;
    
         @Autowired
         public LockService(RedisDistributedLock lock) {
             this.lock = lock;
         }
    
         public void performWithLock(String lockKey) throws InterruptedException {
             if (lock.acquireLock(lockKey, 15000, TimeUnit.MILLISECONDS)) {
                 log.info("Lock acquired. Background Operation started.");
    
                 // Simulate an operation that takes some time
                 Thread.sleep(200);
    
                 log.info("Background Operation completed.");
                 // Optionally, release the lock
                 // lock.releaseLock(lockKey);
             } else {
                 log.error("Failed to acquire lock. Resource is busy.");
             }
         }
     }
    
  5. Redis Distributed Lock: This component handles acquiring and releasing the lock.

     @Component
     public class RedisDistributedLock {
         private final RedisTemplate<String, Object> redisTemplate;
    
         @Autowired
         public RedisDistributedLock(RedisTemplate<String, Object> redisTemplate) {
             this.redisTemplate = redisTemplate;
         }
    
         public boolean acquireLock(String lockKey, long timeout, TimeUnit unit) {
             return redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", timeout, unit);
         }
    
         public void releaseLock(String lockKey) {
             redisTemplate.delete(lockKey);
         }
     }
    

How to Test

Send a HTTP request to test the lock mechanism. You can use ab (ApacheBench) to simulate multiple requests:

ab -n 10 -c 5 "http://localhost:8080/perform/lock-key"
  • Request Count: 20

  • Concurrent Request Count: 10

This setup ensures that only one operation is performed at a time with the given lockKey, demonstrating the effectiveness of our distributed lock implementation.

Understanding the Locking Operation

When we perform a locking operation using the first HTTP request:

  • 1x => Lock acquire: The lock is successfully acquired.

  • 9x => Lock acquire failed: Subsequent attempts to acquire the lock fail due to it being already locked.

CronService Overview

The CronService class is responsible for executing scheduled tasks, ensuring that only one instance performs an operation at a time using a distributed lock.

a person holding a pen and writing on a calendar

@Component
@Slf4j
public class CronService {

    private final RedisDistributedLock lock;
    private final String LOCK_KEY = "cron-lock-key";

    @Autowired
    public CronService(RedisDistributedLock lock) {
        this.lock = lock;
    }

    @Scheduled(fixedDelay = 25000L)
    private void cronMethod() throws InterruptedException {
        log.info("Cron job running...");

        if (lock.acquireLock(this.LOCK_KEY, 15000, TimeUnit.MILLISECONDS)) {
            log.info("Lock acquired. Background Operation started.");

            // Simulate an operation that takes some time
            Thread.sleep(200);

            log.info("Background Operation completed.");
        } else {
            log.error("Failed to acquire lock. Resource is busy.");
        }
    }
}

Explanation of CronService

The cronMethod is scheduled to run with a fixed delay of 25 seconds (fixedDelay = 25000L). Here’s what happens:

  • Lock Acquisition: Attempts to acquire the lock using lock.acquireLock(this.LOCK_KEY, 25000, TimeUnit.MILLISECONDS).

  • Execution: If the lock is acquired successfully, it performs the operation inside the if block.

  • Concurrency Handling: Only one instance of the operation executes at a time due to Redis distributed locking.

  • Error Handling: Logs an error message if the lock acquisition fails, indicating the resource is busy.

Configuration and Running Multiple Instances

  • Change Server Port: Set server.port=0 to allow Spring Boot to choose an available port automatically.

  • Allow Multiple Instances: Configure your Spring Boot application to allow multiple instances to run simultaneously.

Running 2 Spring Boot Instances

When running multiple instances:

  • Both instances can trigger the job method, but only one instance executes the code inside the if block at any given time due to Redis locking.

  • This ensures that operations are synchronized and prevent concurrent access issues.

This setup demonstrates how Redis distributed locks effectively manage concurrent executions of scheduled tasks across multiple Spring Boot instances, ensuring data consistency and reliability in distributed environments.

Conclusion

Implementing distributed locks with Redis and Spring Boot offers robust synchronization for concurrent tasks across distributed systems. By leveraging Redis as a reliable locking mechanism, we ensure that only one instance can access critical resources at any time, thus preventing data inconsistency and concurrency issues. This setup enhances application reliability and scalability, supporting seamless execution of scheduled tasks while maintaining data integrity. Through this approach, developers can effectively manage distributed environments, achieving efficient task scheduling and optimal system performance.

More such articles:

https://medium.com/techwasti

https://www.youtube.com/@maheshwarligade

https://techwasti.com/series/spring-boot-tutorials

https://techwasti.com/series/go-language

0
Subscribe to my newsletter

Read articles from Maheshwar Ligade directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Maheshwar Ligade
Maheshwar Ligade

Learner, Love to make things simple, Full Stack Developer, StackOverflower, Passionate about using machine learning, deep learning and AI