How to Ensure Single-Node Execution of Spring Boot @Scheduled Tasks in Distributed Environments
This guide explains why @Scheduled jobs run on every Spring Boot instance in a cluster, and presents three practical solutions—Spring Integration's Redis lock, Redisson, and ShedLock—complete with Maven dependencies, configuration snippets, code examples, and runtime screenshots to guarantee that only one node executes the scheduled task at a time.
1. Introduction
When a Spring Boot application uses the @Scheduled annotation, the annotated method is executed according to the defined schedule. In a distributed deployment (e.g., multiple instances behind a load balancer), each instance runs the job independently, causing duplicate executions.
This article presents three ways to ensure that a scheduled task is performed by only one node in a distributed environment.
2. Solution 1 – Spring Integration Redis Distributed Lock
Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>Lock configuration
@Bean
RedisLockRegistry redisLockRegistry(RedisConnectionFactory connectionFactory) {
// prefix "pack:job:" and lock expiration 10 seconds
return new RedisLockRegistry(connectionFactory, "pack:job:", 10000);
}Using the lock in a scheduled method
private final RedisLockRegistry lockRegistry;
@Scheduled(fixedDelay = 2000)
public void initData() throws Exception {
Lock lock = lockRegistry.obtain("dict_load");
if (!lock.tryLock()) {
return;
}
try {
System.err.println("%s - executing task...".formatted(Thread.currentThread().getName()));
} finally {
lock.unlock();
}
}The image below shows the lock being created in Redis:
Handling long‑running tasks
If a task runs longer than the lock expiration, an error occurs (see screenshot). To avoid this, a lock‑renewal thread can be added:
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1, r -> {
Thread t = new Thread(r, "lock-renewer");
t.setDaemon(true);
return t;
});
@Scheduled(fixedDelay = 2000)
public void initData() throws Exception {
Lock lock = lockRegistry.obtain("dict_load");
if (!lock.tryLock()) {
return;
}
// 1. start renewal
ScheduledFuture<?> renewTask = scheduler.scheduleAtFixedRate(() -> {
try {
lockRegistry.renewLock("dict_load");
} catch (Exception ignored) {}
}, 2000, 2000, TimeUnit.MILLISECONDS);
// 2. execute task
try {
System.err.println("%s - executing task...".formatted(Thread.currentThread().getName()));
TimeUnit.SECONDS.sleep(6);
} finally {
// 3. cancel renewal and release lock
renewTask.cancel(false);
lock.unlock();
}
}3. Solution 2 – Redisson
Redisson provides a higher‑level Redis lock with automatic expiration.
Dependency
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.50.0</version>
</dependency>Using Redisson in a scheduled method
private final RedissonClient redissonClient;
@Value("${server.port}")
private Integer port;
@Scheduled(fixedDelay = 2000)
public void initData() throws Exception {
RLock lock = redissonClient.getLock("dict_load");
if (!lock.tryLock()) {
logger.warn("Node [{}] failed to acquire lock...", port);
return;
}
try {
logger.info("node-{}, acquired lock, starting task...", port);
System.err.println("%s - executing task...".formatted(Thread.currentThread().getName()));
TimeUnit.SECONDS.sleep(3);
} finally {
lock.unlock();
}
}Running two nodes shows that only one instance performs the job (screenshot):
4. Solution 3 – ShedLock
ShedLock is a library that abstracts distributed locking for scheduled tasks.
Dependencies
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>7.5.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-redis-spring</artifactId>
<version>7.5.0</version>
</dependency>Enable scheduling and ShedLock
@SpringBootApplication
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT3S")
public class App {}
@Bean
LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory, "dict");
}Annotate the scheduled method
@Scheduled(fixedDelay = 2000)
@SchedulerLock(name = "dict_load_scheduler", lockAtMostFor = "5s", lockAtLeastFor = "3s")
public void initData() throws Exception {
System.err.println("%s - executing task...".formatted(Thread.currentThread().getName()));
}Explanation of parameters
lockAtMostFor : maximum time the lock is kept if the owning node crashes before releasing it (safety fallback).
lockAtLeastFor : minimum time the lock is held, ensuring the task cannot be executed again within this window.
Running the example produces the following result (screenshot):
5. Conclusion
All three approaches—Spring Integration’s RedisLockRegistry, Redisson, and ShedLock—allow a Spring Boot application to coordinate scheduled jobs across multiple instances, preventing duplicate execution. Choose the solution that best fits your project’s existing dependencies and desired level of abstraction.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
