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
python
import threading from functools import wraps
A thread-safe debounce decorator in Python
decorators
debounce
threading
Advanced
6 steps
rust
use axum::{ extract::State, http::StatusCode, Json,
Idempotent webhook handling in Axum
deduplication
idempotency
shared-state
Intermediate
8 steps
go
package middleware import ( "log"
Per-IP rate limiting middleware in Gin
rate-limiting
middleware
concurrency
Intermediate
8 steps
java
package com.example.time; import java.time.Duration; import java.time.Instant;
Adding ISO-8601 durations across time zones in Java
date-time
iso-8601
parsing
Intermediate
7 steps
java
import java.util.Comparator; import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Pattern;
Ranking the most frequent words in Java
streams
regex
grouping
Intermediate
7 steps
java
public final class HttpHeaders { private final NavigableMap<String, List<String>> values = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
A case-insensitive HTTP headers map in Java
case-insensitivity
multimap
immutability
Intermediate
7 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.