java
60 lines · 8 steps
Distributed scheduled jobs with Spring locks
A scheduled reconciliation job uses a JDBC-backed distributed lock so only one node in a cluster runs it at a time.
Explained by
highlit
1@Component
2public class InventoryReconciliationJob {
3
4 private static final Logger log = LoggerFactory.getLogger(InventoryReconciliationJob.class);
5 private static final String LOCK_KEY = "inventory-reconciliation";
6
7 private final LockRegistry lockRegistry;
8 private final InventoryReconciler reconciler;
9
10 public InventoryReconciliationJob(LockRegistry lockRegistry, InventoryReconciler reconciler) {
11 this.lockRegistry = lockRegistry;
12 this.reconciler = reconciler;
13 }
14
15 @Scheduled(cron = "0 */15 * * * *")
16 public void reconcile() {
17 Lock lock = lockRegistry.obtain(LOCK_KEY);
18 boolean acquired;
19 try {
20 acquired = lock.tryLock(5, TimeUnit.SECONDS);
21 } catch (InterruptedException e) {
22 Thread.currentThread().interrupt();
23 log.warn("Interrupted while acquiring reconciliation lock");
24 return;
25 }
26
27 if (!acquired) {
28 log.debug("Another node holds the reconciliation lock; skipping this run");
29 return;
30 }
31
32 try {
33 ReconciliationResult result = reconciler.run();
34 log.info("Reconciled {} SKUs, {} adjustments applied",
35 result.scanned(), result.adjustments());
36 } finally {
37 try {
38 lock.unlock();
39 } catch (IllegalStateException e) {
40 log.warn("Reconciliation lock expired before release: {}", e.getMessage());
41 }
42 }
43 }
44
45 @Configuration
46 static class LockConfig {
47
48 @Bean
49 public DefaultLockRepository lockRepository(DataSource dataSource) {
50 DefaultLockRepository repository = new DefaultLockRepository(dataSource);
51 repository.setTimeToLive(120_000);
52 return repository;
53 }
54
55 @Bean
56 public JdbcLockRegistry lockRegistry(LockRepository lockRepository) {
57 return new JdbcLockRegistry(lockRepository);
58 }
59 }
60}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A shared lock backed by a database turns a cluster-wide scheduled job into a single-executor operation without a dedicated coordinator.
- 2Always bound lock acquisition with a timeout and release in a finally block so a failed run never leaves the lock held.
- 3A time-to-live on the lock guards against nodes that crash mid-run, letting another instance recover the schedule.
Related explainers
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
rust
use std::collections::VecDeque; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant};
Building a counting semaphore in Rust
concurrency
synchronization
condition-variable
Advanced
9 steps
java
public class TimedSocketReader { private static final int READ_TIMEOUT_MS = 5_000; private static final int CONNECT_TIMEOUT_MS = 3_000;
Reading a socket with connect and read timeouts
sockets
timeouts
io
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/distributed-scheduled-jobs-with-spring-locks-explained-java-ee37/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.