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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Ensure Single-Node Execution of Spring Boot @Scheduled Tasks in Distributed Environments

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentRedisSpring Bootdistributed lockredissonscheduled tasksShedLock
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.