ShedLock for Spring Boot solves a very common problem, which occurs when running scheduled jobs in your application when deployed with a high availability setup into a production environment. You might be ending up with multiple job executions at the same time.
There is no way with Springs @Scheduled
out of the box which solves this issue. Instead, frameworks like ShedLock or Quartz have their approaches.
ShedLock uses external storage to keep track of schedulers and locks. Your application needs to be connected to a database of your choice for this to work. This reaches from JdbcTemplate over Mongo DB to Elastic Search and many more!
Add a maven dependency for Spring Boot ShedLock to your pom.xml.
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.26.0</version>
</dependency>
Depending on your database, you need to create a database table for ShedLock. These scripts are typically executed via Flyway or Liquibase. Let's have a look at a PostgreSQL example:
# Postgres
CREATE TABLE shedlock(
name VARCHAR(64) NOT NULL,
lock_until TIMESTAMP NOT NULL,
locked_at TIMESTAMP NOT NULL,
locked_by VARCHAR(255) NOT NULL,
PRIMARY KEY (name)
);
Enabling ShedLock and configure it with, e.g. the defaultLockAtMostFor
would take a value as defined like 10s
or as in java.time.Duration. It obtains a Duration from a text string on the form of PnDTnHnMn.nS
.
@EnableSchedulerLock(defaultLockAtMostFor = "PT10S")
public class ShedLockSampleAppApplication {
public static void main(String[] args) {
SpringApplication.run(ShedLockSampleAppApplication.class, args);
}
}
Depending on the backing store we use, we need to define a corresponding LockBrovider bean.
@Configuration
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
}
Instead of setting up the database with your favourite DB migration tool, you can build it as well with the Lock Provider from ShedLock.
@Configuration
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.usingDbTime() // Works on Postgres, MySQL, MariaDb, MS SQL, Oracle, DB2, HSQL and H2
.build()
);
}
}
By specifying usingDbTime()
, the lock provider will use UTC time, based on the DB server clock. See more information in the ShedLock documentation.
The last thing to do is annotate the schedulers.
@Slf4j
@Component
public class ShedLockTaskScheduler {
@Scheduled(cron = "*/2 * * * * *")
@SchedulerLock(
name = "UNIQUE_KEY_FOR_SHEDLOCK_SCHEDULER",
lockAtLeastFor = "PT5S", // lock for at least a minute, overriding defaults
lockAtMostFor = "PT10S" // lock for at most 7 minutes
)
public void scheduledTaskToRun() {
// To assert that the lock is held (prevents misconfiguration errors)
LockAssert.assertLocked();
log.debug("Do other things ...");
}
}
You may have seen that we used LockAssert
to check if the lock was really triggered in the production code. This is only to prevent misconfiguration errors, like AOP misconfiguration, missing annotation etc.
We made it through the example! Annotated schedulers will be automatically locked so that only one instance is executed, even when deployed multiple instances of your app in production. Enjoy experimenting!
The complete source code is available on our Github Repository. We included Testcontainers as well as awaitility to ensure ShedLock is running as expected.
If you are interested to learn more about Spring and Spring Boot, get in touch and have a look at our training courses!