Spring Boot cookbook · Intermediate
Run scheduled jobs once across replicas with ShedLock
Run a nightly cleanup with @Scheduled without every instance executing it.
Approach
@Scheduled runs on every instance. Add ShedLock with a JDBC or Redis lock provider so only one instance runs each job; set lockAtMostFor above the expected duration as a safety net and lockAtLeastFor to avoid double runs from clock skew.
Solution
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30M")
class SchedulingConfig {
@Bean
LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.usingDbTime()
.build());
}
}
@Component
class CleanupJobs {
private static final Logger log = LoggerFactory.getLogger(CleanupJobs.class);
private final SessionRepository sessions;
CleanupJobs(SessionRepository sessions) { this.sessions = sessions; }
@Scheduled(cron = "0 15 2 * * *", zone = "UTC")
@SchedulerLock(name = "purgeExpiredSessions", lockAtMostFor = "PT20M", lockAtLeastFor = "PT1M")
public void purgeExpiredSessions() {
int deleted = sessions.deleteExpired(Instant.now());
log.info("Purged expired sessions count={}", deleted);
}
}
// Flyway: CREATE TABLE shedlock(name VARCHAR(64) PRIMARY KEY, lock_until TIMESTAMP NOT NULL,
// locked_at TIMESTAMP NOT NULL, locked_by VARCHAR(255) NOT NULL);
Pitfalls
- Forgetting the
shedlocktable migration. lockAtMostForshorter than the job's runtime (another node starts it).- Long jobs on the scheduler's single default thread blocking other jobs.
Variations
- Kubernetes CronJob running a one-off command with the same image.
- Quartz with a clustered JDBC job store for complex scheduling.
Further reading: ShedLock
Reference: https://github.com/lukas-krecan/ShedLock
More Spring Boot recipes
- Add correlation IDs to Spring Boot logs
- Add retries and a circuit breaker with Resilience4j
- Authenticate machine clients with API keys in Spring Security
- Cache service results in Redis with @Cacheable
- Consume Kafka messages with retries and a dead-letter topic
- Handle concurrent updates with optimistic locking
- Implement soft delete with Hibernate
- Make POST endpoints idempotent in Spring Boot