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.
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.
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.
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
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.
