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 shedlock table migration.
  • lockAtMostFor shorter 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