Prevent Duplicate Spring @Scheduled Jobs in Multi‑Instance Deployments with ShedLock

When a Spring Boot application using @Scheduled is deployed on multiple servers, each instance triggers the same job, leading to duplicate processing; this article explains why the problem occurs, reviews simple single‑node and Redis lock approaches, and provides a step‑by‑step guide to integrate the ShedLock framework for reliable, annotation‑driven distributed locking.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Prevent Duplicate Spring @Scheduled Jobs in Multi‑Instance Deployments with ShedLock

Problem with @Scheduled in multi‑instance deployment

Spring Boot's @Scheduled makes it easy to write periodic jobs, but when the application runs on multiple servers the same job is triggered on each instance, causing duplicate processing and file generation.

Common solutions

1. Single‑machine execution

Run the job only on a designated host by checking the hostname at runtime.

@Scheduled(cron = "0 0 2 * * ?")
public void scheduledTask() {
    String hostname = InetAddress.getLocalHost().getHostName();
    if (!"app-server-01".equals(hostname)) {
        return;
    }
    // do something
}

Drawback: if that machine goes down the job never runs.

2. Redis distributed lock (SETNX or Redisson)

Use Redis to acquire a lock before executing the task.

@Scheduled(cron = "0 0 2 * * ?")
public void scheduledTask() {
    Boolean locked = stringRedisTemplate.opsForValue()
        .setIfAbsent("task:data-sync", "1", 10, TimeUnit.MINUTES);
    if (Boolean.FALSE.equals(locked)) {
        return;
    }
    try {
        // do something
    } finally {
        stringRedisTemplate.delete("task:data-sync");
    }
}

Drawback: lock logic must be added to every job and the project must already use Redis.

ShedLock solution

ShedLock is a dedicated framework that prevents duplicate execution of scheduled tasks. It stores lock information in a chosen backend (database, Redis, MongoDB, etc.) and provides an annotation‑based API.

Integration steps

Add Maven dependencies

<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>4.42.0</version>
</dependency>
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-jdbc-template</artifactId>
    <version>4.42.0</version>
</dependency>

Create lock table

CREATE TABLE shedlock (
    name VARCHAR(64) NOT NULL,
    lock_until TIMESTAMP(3) NOT NULL,
    locked_at TIMESTAMP(3) NOT NULL,
    locked_by VARCHAR(255) NOT NULL,
    PRIMARY KEY (name)
);

Configure LockProvider

@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class ShedLockConfig {
    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
        return new JdbcTemplateLockProvider(
            JdbcTemplateLockProvider.Configuration.builder()
                .withDataSource(dataSource)
                .withTableName("shedlock")
                .build()
        );
    }
}

Annotate scheduled method

@Scheduled(cron = "0 0 2 * * ?")
@SchedulerLock(name = "dataSyncTask", lockAtMostFor = "5m")
public void syncData() {
    // do something
}

Annotation parameters

name : unique lock name, usually the task name.

lockAtMostFor : maximum time the lock is held; prevents dead‑locks if the node crashes. Typical value is about twice the expected execution time.

lockAtLeastFor : minimum lock duration; useful when the task finishes quickly to avoid immediate lock release and race conditions. Example: lockAtLeastFor = "1m".

How ShedLock works

Before a task runs, ShedLock attempts to insert or update a row in the lock table. The SQL uses WHERE lock_until < NOW() so that only an expired lock can be taken. If the update affects a row, the lock is acquired; otherwise the task is skipped. The lock expires automatically, no explicit unlock call is required.

Suitable scenarios

Scheduled jobs that must not run concurrently.

Projects that prefer annotation‑driven lock handling.

Environments where automatic lock expiration is needed to avoid deadlocks.

When not to use

High‑throughput lock contention such as flash sales.

Requirements for re‑entrant, read‑write, or other advanced lock features.

Business logic that needs precise control over lock acquisition and release timing.

Practical considerations

Backend choice : The example uses JDBC, but ShedLock also supports Redis, MongoDB, ZooKeeper, Hazelcast, etc.

Custom table name : Default is shedlock; can be changed via the provider configuration.

Machine identifier : The locked_by column stores the host name; you can customize it, e.g., .withLockedByValue("app-" + getServerIp()).

Multiple data sources : Ensure the lock provider points to the primary database to avoid replication lag issues.

Conclusion

ShedLock offers a clean, annotation‑based way to guarantee that a scheduled task runs on only one instance in a cluster. Its advantages are simplicity, automatic lock expiration, and support for various storage backends. However, it is limited to scheduling scenarios and should not replace general‑purpose distributed lock solutions like Redisson when those are required.

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.

JavaSpring Bootdistributed lockShedLock@Scheduled
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.