
In modernen Software-Systemen sind Scheduled Jobs allgegenwärtig und spielen eine entscheidende Rolle in der Automatisierung von Prozessen. Sie werden genutzt, um regelmäßige, wiederkehrende Aufgaben auszuführen, wie zum Beispiel:
- Datenbankbereinigung: Entfernen von veralteten oder unnötigen Daten zur Verbesserung der Leistung.
- E-Mails oder Benachrichtigungen: Automatisiertes Versenden von Mitteilungen an Benutzer oder Systeme.
- Batch-Jobs: Verarbeitung großer Datenmengen in einem definierten Zeitrahmen.
In einer Single-Node-Anwendung ist die Implementierung solcher Jobs relativ einfach, da garantiert werden kann, dass ein Job genau einmal ausgeführt wird.
Doch was passiert, wenn unsere Anwendung in einer verteilten Umgebung läuft, wie etwa in Kubernetes, AWS ECS oder Multi-Node-Setups mit mehreren Instanzen? Hier wird die Thematik des übergreifenden Lockings für Scheduled Jobs besonders relevant.
In solchen Umgebungen kann es leicht zu Situationen kommen, in denen derselbe Job von mehreren Instanzen gleichzeitig ausgeführt wird, was zu Dateninkonsistenzen, Überlastung oder sogar Systemausfällen führen kann.
In diesem Blogbeitrag werden wir diese Herausforderungen eingehend behandeln, verschiedene Ansätze zur Implementierung von Locking-Mechanismen in Spring Boot untersuchen und Best Practices vorstellen, um sicherzustellen, dass Scheduled Job effizient und zuverlässig in verteilten Systemen ausgeführt werden.
Problem Darstellung:
Stellen wir uns folgendes Szenario vor: Wir haben eine skalierte Spring Boot-Anwendung, die mit zwei Instanzen parallel läuft.
In dieser Anwendung gibt es einen Scheduled Job, der alle zehn Minuten Daten von einem externen System abruft, diese verarbeitet, persistiert und anschließend in eine Queue sendet. Zum Abschluss wird eine Benachrichtigung versendet.
Ein kritischer Faktor in diesem Szenario ist, dass jeder Datenabruf beim externen System hohe Kosten verursacht.
Wenn unsere Anwendung den Scheduled Job ohne Synchronisation ausführt, ruft jede Instanz das externe System separat auf – in diesem Fall also doppelt. In einem größeren Cluster mit mehreren Instanzen vervielfacht sich dieses Problem.
All dies kann zu den folgenden Konsequenzen führen:
- Dateninkonsistenzen: Die gleichen Daten werden mehrmals verarbeitet, was eine Sonderbehandlung benötigt.
- Erhöhte Kosten durch doppelte API-Aufrufe:
Mehrfache API-Aufrufe und redundante Job-Ausführungen verbrauchen unnötig Ressourcen wie CPU, Speicher und Netzwerk, was zu höheren Betriebskosten und schnellerem Erreichen von Rate Limits führt, insbesondere in Cloud-Umgebungen mit nutzungsbasierten Kostenmodellen. - Deadlocks oder unerwartete Race Conditions: Gleichzeitiger Zugriff mehrerer Instanzen auf einen Job kann zu Deadlocks oder Race Conditions führen, was Datenbankprobleme oder falsche Lagerbestände verursacht.
- Leistungsprobleme: Durch mehrere gleichzeitigen Ausführungen kann die Systemlast erhöht werden, was zu Verzögerungen oder einer Überlastung der Infrastruktur führen kann.
- Fehlerhafte Benachrichtigungen: Der Empfänger könnte mehrfach benachrichtigt werden, was den Eindruck von Spam erwecken kann.
- Ressourcenkonflikte: Mehrere Instanzen könnten gleichzeitig auf dieselben Ressourcen zugreifen, etwa auf eine gemeinsame Datenbank, was zu Sperren oder Fehlern führen kann.
- Überlastung der Datenbank: Mehrfach ausgeführte Jobs erhöhen die CPU- und I/O-Last auf der Datenbank, indem sie redundante Berechnungen durchführen und identische Daten mehrfach speichern
- Volllaufende Queues: Dadurch können Nachrichten verworfen oder dupliziert werden, das System kann sich verlangsamen oder blockieren, und erhöhte Speicherlast kann die Gesamtleistung beeinträchtigen.
Um diese Probleme zu vermeiden und sicherzustellen, dass der Scheduled Job nur einmal ausgeführt wird, benötigen wir eine Lösung, die die parallele Ausführung in einer verteilten Umgebung verhindert. Eine solche Lösung bietet ShedLock.
Was ist Shedlock:
Unser Ziel ist es sicherzustellen, dass der Scheduled Job nur von einer einzigen Instanz ausgeführt wird. Dafür müssen alle Instanzen wissen, ob der Task bereits von einer anderen Instanz ausgeführt wird – das ist entscheidend, um eine parallele Ausführung zu verhindern.
Die Lösung besteht darin, den Status des Tasks zentral zu speichern und zu verwalten. Genau hier kommt ShedLock ins Spiel: Es ermöglicht die Speicherung von Sperrinformationen (Locks) in einer gemeinsam genutzten Datenbank oder einem verteilten Speichersystem. Damit ShedLock funktioniert, muss ein geeigneter LockProvider deklariert werden. Die Sperrinformationen werden in einer Tabelle oder einem Dokument innerhalb der Datenbank gespeichert, sodass alle Instanzen darauf zugreifen können. Dadurch wird sichergestellt, dass zu jedem Zeitpunkt nur eine Instanz den Job ausführt.
ShedLock ist eine Open-Source-Bibliothek für Java, die sicherstellt, dass geplante Aufgaben in verteilten Systemen nicht gleichzeitig von mehreren Instanzen ausgeführt werden – unabhängig davon, wie viele Instanzen einer Anwendung aktiv sind. Wenn ein Task auf einer Instanz gestartet wird, erwirbt diese eine Sperre, die verhindert, dass dieselbe Aufgabe auf einer anderen Instanz parallel ausgeführt wird. Falls die Sperre bereits existiert, wird die Ausführung auf anderen Knoten nicht verzögert, sondern einfach übersprungen.
ShedLock ist ideal für Szenarien, in denen geplante Aufgaben nicht parallel laufen dürfen, aber dennoch regelmäßig wiederholt werden müssen. Die Sperren sind zeitbasiert, weshalb ShedLock davon ausgeht, dass die Uhren auf den Knoten synchronisiert sind.
ShedLock kann nahtlos in verschiedene Datenbanksysteme integriert werden und unterstützt sowohl relationale Datenbanken mit JDBC-Treiber als auch NoSQL-Datenbanken (z. B. MongoDB). Zudem kann es mit verteilten Caching-Systemen wie Redis verwendet werden, um das Lock-Management in verteilten Anwendungen zu realisieren.
ShedLocK im Einsatz:
Im Folgenden wird demonstriert, wie ShedLock in einer Spring Boot-Anwendung eingerichtet werden kann.
In unserem Beispiel werden wir Maven als Build-Tool und MongoDB als Datenbank verwenden.
- Abhängigkeiten hinzufügen:
Um ShedLock in Spring zu integrieren, müssen wir die folgenden Abhängigkeiten einbinden.
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>6.2.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-mongo</artifactId>
<version>6.2.0</version>
</dependency>
Code-Sprache: Java (java)Die erste Abhängigkeit (shedlock-spring) enthält die Kernfunktionalität von ShedLock, und integriert ShedLock in Spring Boot, während die zweite (shedlock-provider-mongo) MongoDB als Lock-Provider konfiguriert.
- LockProvider für MongoDB definieren:
Damit ShedLock mit MongoDB genutzt werden kann, muss ein LockProvider eingerichtet werden. Dieser speichert die Sperrinformationen in einer speziellen MongoDB-Collection, die ShedLock automatisch anlegt.Hinweis: Wir setzen voraus, dass bereits einMongoClientals Spring Bean im Anwendungskontext existiert.
import com.mongodb.client.MongoClient;
import net.javacrumbs.shedlock.provider.mongo.MongoLockProvider;
import net.javacrumbs.shedlock.core.LockProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class ShedLockConfig {
private static final String DATABASE_NAME = "mydatabase";
@Bean
public LockProvider lockProvider(MongoClient mongoClient) {
return new MongoLockProvider(mongoClient.getDatabase(DATABASE_NAME));
}
}
Code-Sprache: Java (java)- Scheduled Job mit ShedLock in Spring Boot implementieren:
Um sicherzustellen, dass Aufgaben nicht gleichzeitig auf mehreren Instanzen ausgeführt werden, nutzen wir die Annotationen@EnableSchedulerLockund@SchedulerLock.- @EnableSchedulerLock: Diese Annotation wird in der Main-Klasse der Anwendung gesetzt und aktiviert ShedLock und steuert das Standardverhalten des Locks.
Ein wichtiger Parameter istdefaultLockAtMostFor, der festlegt, wie lange ein Lock maximal gehalten wird, wenn in der Annotation@SchedulerLockkeine explizite Dauer fürlockAtMostForangegeben wird. - @SchedulerLock: Diese Annotation wird auf den Scheduled Job angewendet und stellt sicher, dass der Job nicht gleichzeitig auf mehreren Instanzen ausgeführt wird.
Die folgenden Parameter können konfiguriert werden:- name: Definiert den eindeutigen Namen des Locks in der Datenbank.
- lockAtLeastFor: Der Lock bleibt mindestens für die angegebene Zeit aktiv, auch wenn die Aufgabe schneller abgeschlossen wird.
- lockAtMostFor: Der Lock wird maximal für die angegebene Dauer gehalten. Wenn die Aufgabe länger dauert oder hängen bleibt, wird der Lock spätestens nach dieser Zeit freigegeben.
- @EnableSchedulerLock: Diese Annotation wird in der Main-Klasse der Anwendung gesetzt und aktiviert ShedLock und steuert das Standardverhalten des Locks.
Die vollständige Implementierung sieht wie folgt aus:
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class SpringBootShedlockApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootShedlockApplication.class, args);
}
}
@Component
class MyScheduledTask {
@Scheduled(cron = "0 0/10 * * * ?")
@SchedulerLock(name = "MyScheduledTask_Lock", lockAtLeastFor = "PT5M", lockAtMostFor = "PT14M")
public void executeTask() {
System.out.println("Scheduled Task läuft...");
}
}
Code-Sprache: Java (java)
Wie funktioniert das Zusammenspiel?
- Alle 15 Minuten startet Spring den
executeTask()-Job. - Bevor der Task ausgeführt wird, setzt ShedLock den Lock mit dem Namen
MyScheduledTask_Lock. - Falls eine andere Instanz diesen Lock bereits hält, wird der Task nicht erneut gestartet.
- Der Lock bleibt mindestens 5 Minuten bestehen (
lockAtLeastFor), aber maximal 14 Minuten (lockAtMostFor). - Falls der Task abstürzt oder hängt, wird der Lock nach 14 Minuten automatisch freigegeben.
Fazit:
In diesem Wissensbeitrag wurde erläutert, wie ShedLock in einem Spring-Boot-Projekt integriert werden kann, um die Ausführung der Scheduled Jobs in verteilten Systemen zu synchronisieren. Dabei wurden die zentralen Codeausschnitte für die Implementierung mit ShedLock vorgestellt.
Zwar gibt es Alternativen wie Quartz, Redisson oder Apache Curator, jedoch gehen diese mit unterschiedlichen Herausforderungen einher: Die Einrichtung von Quartz kann komplex und zeitaufwendig sein, Redisson erfordert eine Redis-Instanz, und Apache Curator basiert auf Zookeeper, was zusätzlichen Infrastrukturaufwand bedeutet.
ShedLock hingegen zeichnet sich durch seine Leichtgewichtigkeit und einfache Integration in bestehende Spring-Boot-Projekte aus. Es unterstützt diverse Datenbanksysteme und und lässt sich mit geringem Aufwand einrichten.
Zusammenfassend bietet ShedLock eine effiziente Lösung zur Synchronisation von Scheduled Jobs in verteilten Systemen und erleichtert die Bewältigung der damit verbundenen Herausforderungen.


