Scheduling és async
@Scheduled, @Async, thread pool management, cron expressions
Scheduling és async
A scheduling és az async akkor lesz igazán hasznos, ha nem csak “fusson valami háttérben”, hanem pontosan tudod, milyen threaden, milyen gyakran és milyen failure modellel történik.
1. Definíció / Definition
Mi ez? / What is it?
A Spring scheduling időzített feladatok futtatására való, az async pedig arra, hogy bizonyos műveleteket ne a hívó thread blokkolásával végezzünk. A kettő rokon terület, de nem ugyanaz: az egyik azt mondja meg, mikor induljon el valami, a másik azt, hogy milyen végrehajtási modellen fusson.
Miért létezik? / Why does it exist?
Mert rengeteg feladat nem request-response stílusú. Kell cache warmup, riport generálás, email küldés, retry queue feldolgozás, periodikus cleanup, illetve olyan háttérmunka, ami túl lassú lenne HTTP request alatt.
Hol helyezkedik el? / Where does it fit?
A scheduling és async az application service és infrastruktúra határán ül. Erősen kötődik thread poolokhoz, tranzakciókhoz, monitoringhoz és cluster topológiához is.
2. Alapfogalmak / Core Concepts
2.1 `@Scheduled`
A @Scheduled annotációval időzített metódusokat futtatsz. Aktiválásához kell @EnableScheduling.
Fő módok:
- fixedRate: adott periódusonként indul, az indulások közti távolság fix.
- fixedDelay: a következő futás az előző befejezése után vár X időt.
- cron: naptár alapú időzítés.
| Típus | Mit mér? | Tipikus use case |
|---|---|---|
| fixedRate | start-to-start idő | polling, heartbeat |
| fixedDelay | end-to-start idő | cleanup, batch loop |
| cron | naptári szabály | napi riport, havi zárás |
2.2 Spring cron sajátosság
A Spring cron expression 6 mezős lehet, mert tartalmaz másodperc mezőt is.
A mezők sorrendje: másodperc perc óra hónap-nap hónap hét-napja
Példa: 0 0 2 * * * azt jelenti, hogy minden nap 02:00:00-kor fut.
Ez minden nap 02:00:00-kor fut. Klasszikus hiba, hogy valaki Unix cront másol be és elcsúszik minden mező.
2.3 `@Async`
Az @Async segítségével Spring proxy alapon más threaden futtat egy metódust. Jellemző return type:
voidCompletableFuture<T>Future<T>
Ha semmit nem konfigurálsz, könnyen kapsz rosszul skálázódó default viselkedést. Productionben általában saját TaskExecutor kell.
2.4 Task scheduling vs task execution
Ez kritikus különbség.
Kulcskülönbség:
TaskSchedulerdönti el, hogy mikor induljon a feladat.TaskExecutordönti el, hogy melyik thread hajtsa végre.ThreadPoolTaskScheduler: időzített futtatásra.
ThreadPoolTaskExecutor: async feladatvégrehajtásra.
A kettőt sokan összekeverik, aztán csodálkoznak a starvationön vagy a párhuzamossági hibákon.
2.5 Exception kezelés async világban
@Async void metódusnál a kivétel nem jön vissza a hívóhoz. Ezért kell:
AsyncUncaughtExceptionHandler, vagyCompletableFutureés explicit error handling.
2.6 Egy node vs több node
@Scheduled minden alkalmazás példányban lefut. Ha három podod van, ugyanaz a job háromszor fut. Ez néha jó, néha katasztrófa.
Cluster-safe megoldások:
- ShedLock: egyszerű distributed lock DB-vel.
- Quartz: összetettebb scheduler, perzisztens trigger modellel.
3. Gyakorlati használat / Practical Usage
Jó példa a napi riport generálás. A jobot elég naponta egyszer futtatni, tehát cron kell. Ha az alkalmazás több példányban megy, lockolni is kell, különben háromszor legenerálod ugyanazt a riportot és három email megy ki.
Másik klasszikus példa az email küldés. A felhasználó regisztrál, de az email provider válasza ne tartsa nyitva a request threadet. Itt @Async vagy még jobb esetben queue alapú feldolgozás jön szóba. Az async nem mágikus gyorsítás, hanem response time leválasztás.
Polloló integrációknál a fixedDelay sokszor jobb, mint a fixedRate. Ha a külső rendszer lassú, a fixedRate képes egymásra csúszó futásokat produkálni, míg a fixedDelay természetes backpressure-t ad.
Nagyobb rendszereknél külön scheduler és executor pool kell. A lassú batch jobok ne fogyasszák el az email küldésre vagy más async workflow-ra fenntartott threadeket. A thread poolok üzleti prioritást is tükrözhetnek.
4. Kód példák / Code Examples
4.1 Scheduling és async engedélyezése
@Configuration
@EnableScheduling
@EnableAsync
public class TaskConfig implements AsyncConfigurer {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(4);
scheduler.setThreadNamePrefix("scheduler-");
scheduler.initialize();
return scheduler;
}
@Bean(name = "mailExecutor")
public ThreadPoolTaskExecutor mailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("mail-");
executor.initialize();
return executor;
}
@Override
public Executor getAsyncExecutor() {
return mailExecutor();
}
}
4.2 Scheduled cleanup job
@Service
public class CleanupJob {
@Scheduled(cron = "0 0 */6 * * *")
public void removeExpiredTokens() {
// delete expired tokens every 6 hours
}
@Scheduled(fixedDelay = 30000)
public void refreshReferenceData() {
// rerun 30 seconds after previous execution completes
}
}
4.3 Async service CompletableFuture-rel
@Service
public class MailService {
@Async("mailExecutor")
public CompletableFuture<Void> sendWelcomeEmail(String email) {
simulateRemoteCall();
return CompletableFuture.completedFuture(null);
}
private void simulateRemoteCall() {
try {
Thread.sleep(300);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Mail sending interrupted", ex);
}
}
}
4.4 Async exception handler
@Configuration
public class AsyncErrorConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) ->
LoggerFactory.getLogger(method.getDeclaringClass())
.error("Async error in {} with params {}", method.getName(), Arrays.toString(objects), throwable);
}
}
5. Trade-offok / Trade-offs
Előnyök
- egyszerű háttérfeladat-kezelés Springen belül;
- jobb response time nem blokkoló user flow-knál;
- cron/fixedDelay/fixedRate elég sok use case-re elég;
- kifejezetten olvasható annotáció alapú modell.
Hátrányok
- proxy alapú működés miatt vannak meglepő edge case-ek;
- thread pool tuning nélkül hamar túlterheled a node-ot;
- több példányos futásnál könnyen duplikált jobok lesznek;
- az async nem helyettesíti a tartós queue alapú architektúrát.
Használd, ha egyszerűbb időzítés vagy background munka kell egy szolgáltatáson belül.
Ne ezt válaszd, ha pontos perzisztens ütemezés, masszív batch orchestration vagy garantált delivery kell. Oda Quartz, external scheduler vagy messaging rendszer lehet jobb.
6. Gyakori hibák / Common Mistakes
6.1 Self-invocation proxy bypass
Ha ugyanazon beanen belül egy @Async vagy @Scheduled logikát közvetlenül meghívsz, a Spring proxy sokszor nem lép be. Emiatt a metódus szinkronban fut vagy máshogy viselkedik, mint várnád.
6.2 Default executor használata vakon
Kis projektben működhet, productionben viszont thread starvation, nagy queue és lassú leállás lehet belőle. Mindig tudd, milyen pool dolgozik.
6.3 Long-running job fixedRate-tel
Ha egy job néha hosszabb ideig fut, mint a rate periódus, könnyen átfedés lesz. Ebből duplikáció, lock contention és erőforrás-probléma jön.
6.4 Async transaction félreértése
Az @Transactional és @Async külön threadre kerülve nem ugyanazt a contextet viszi tovább. Sokan azt hiszik, hogy ugyanaz a tranzakció “csak háttérben” fut tovább. Nem így van.
6.5 Clusterben lock nélküli scheduler
Több pod esetén egy havi számlázó job többször lefutva üzleti hibát is okozhat. Ezt nem logika szinten, hanem architekturálisan kell kezelni.
7. Senior szintű meglátások / Senior-level Insights
Senior szinten a legfontosabb kérdés nem az, hogy “működik-e a @Scheduled”, hanem az, hogy milyen delivery semantics kell. At-most-once? At-least-once? Lehet duplikáció? Idempotens a job? Ezek döntik el, hogy elég-e a beépített scheduler vagy kell distributed lock, queue, esetleg külön workflow engine.
Az @Async-ot nem érdemes teljesítmény-optimalizálásként kommunikálni. Attól, hogy a hívó thread nem vár, a munka még elvégződik valahol, valamilyen erőforrással. Ha az async rész túl sok CPU-t vagy IO-t fogyaszt, a rendszer egésze ugyanúgy belassul.
Thread poolokat business prioritás alapján szeparálj. A riportgenerálás, email küldés és cache refresh más kritikalitású. Egy közös pool szép rövid config, de incident közben nagyon fájhat.
A scheduler-jobok observabilityje ugyanolyan fontos, mint az API endpointoké. Kell run count, duration, success/failure metric és jó log. A “háttérben fut valami” monitoring nélkül gyorsan “háttérben romlik valami” lesz.
8. Szószedet / Glossary
@Scheduled: időzített futtatást biztosító Spring annotáció.@Async: aszinkron végrehajtást indító Spring annotáció.- fixedRate: indulások közti fix periódus.
- fixedDelay: befejezés utáni fix várakozás.
- cron: naptári mintára épülő időzítés.
- TaskScheduler: időzítési infrastruktúra.
- TaskExecutor: feladatvégrehajtó infrastruktúra.
- CompletableFuture: kompozálható aszinkron eredményobjektum.
- ShedLock: egyszerű distributed scheduling lock megoldás.
- Quartz: fejlettebb, perzisztens scheduler framework.
9. Gyorsreferencia / Cheatsheet
| Téma | Jó default | Figyelj erre |
|---|---|---|
@Scheduled |
egyszerű periodikus jobok | több pod = több futás |
| fixedRate | könnyű polling | átfedő futások |
| fixedDelay | backpressure barát | driftelhet az időzítés |
| cron | naptári események | Springben van seconds mező |
@Async |
IO jellegű háttérmunka | saját executor kell |
CompletableFuture |
eredmény és hiba kezelés | compose-olni tudni kell |
| TaskScheduler | időzítés | ne keverd executorral |
| TaskExecutor | async végrehajtás | queue és pool sizing |
| ShedLock | cluster-safe egyszeri job | lock storage kell |
🎮 Játékok
8 kérdés