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.
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
Install Redis: Follow the official Redis documentation to install Redis on your machine or use a cloud-based service.
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
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
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); } }
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; } }
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"; } }
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."); } } }
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.
@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://www.youtube.com/@maheshwarligade
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