Scheduled Tasks
Giriş
Evinizde bir bulaşık makinesi düşünün. Her gün saat 02:00'de otomatik olarak çalışmaya başlıyor — siz uyurken bulaşıklar yıkanıyor. Veya her 30 dakikada bir çalışan bir robot süpürge. İşte Spring'in @Scheduled anotasyonu, uygulamanızdaki bu "otomatik pilot" görevlerdir — belirli aralıklarla veya belirli zamanlarda çalışan, insan müdahalesi gerektirmeyen işlemler.
Birçok uygulamada belirli görevlerin periyodik olarak çalıştırılması gerekir: veritabanı temizleme, raporlama, cache yenileme, dış servislerle senkronizasyon, sağlık kontrolleri, süresi dolan oturumların silinmesi, günlük yedekleme. Spring'in @Scheduled anotasyonu bu tür görevleri tanımlamanın en kolay yoludur — harici bir scheduler (Quartz, cron daemon) gerektirmez, Spring Boot uygulaması kendi scheduler'ını içerir.
Bu derste @Scheduled anotasyonunun etkinleştirilmesini, fixedRate ve fixedDelay stratejilerini, cron ifadelerini, timezone desteğini, dinamik scheduling'i, scheduling thread pool yapılandırmasını, dağıtık sistemlerdeki zorlukları (ShedLock) ve production best practice'lerini derinlemesine öğreneceğiz.
@EnableScheduling
Zamanlanmış görevleri etkinleştirmek için @EnableScheduling anotasyonu gereklidir:
@SpringBootApplication
@EnableScheduling
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}Bu anotasyon Spring context'inde bir ScheduledAnnotationBeanPostProcessor kaydeder. Bu processor, @Scheduled anotasyonlu metotları tarar ve uygun zamanlama ile çalıştırır.
⚠️ Dikkat:
@EnableSchedulingolmadan@Scheduledmetotlar sessizce görmezden gelinir — hata almazsınız, görevler çalışmaz.
fixedRate vs fixedDelay
İki temel zamanlama stratejisi vardır ve aralarındaki fark kritiktir:
fixedRate — Sabit Frekans
Görev başlangıçları arasındaki süreyi belirler. Önceki çalışma ne kadar sürerse sürsün, her X milisaniyede bir tetiklenir:
@Component
@Slf4j
public class ScheduledTasks {
// Her 5 saniyede bir çalışır (önceki bitmese bile zamanlama değişmez)
@Scheduled(fixedRate = 5000)
public void heartbeat() {
log.info("💓 Heartbeat: {}", Instant.now());
}
}Zaman çizelgesi — görev 3 saniye sürüyorsa:
fixedRate = 5000ms, görev süresi = 3s
|--TASK--| . . |--TASK--| . . |--TASK--|
0 3 5 8 10 13 (saniye)
2s boş 2s boşAma görev 7 saniye sürerse (taşma!):
fixedRate = 5000ms, görev süresi = 7s
|----TASK----|----TASK----|----TASK----|
0 7 14 21
5s geçti ama görev bitmedi → bir sonraki HEMEN başlar
Birikim oluşur → görevler art arda çalışırfixedDelay — Sabit Gecikme
Görevin bitişi ile bir sonraki başlangıcı arasındaki süreyi belirler. Önceki görev tamamlanmadan yenisi asla başlamaz:
// Görev bittikten 5 saniye sonra tekrar çalışır
@Scheduled(fixedDelay = 5000)
public void syncWithExternalService() {
log.info("🔄 Senkronizasyon başladı");
externalService.sync(); // 3 saniye sürsün
log.info("✅ Senkronizasyon tamamlandı");
// Tamamlandıktan 5 saniye sonra tekrar çalışır (8. saniyede)
}Zaman çizelgesi:
fixedDelay = 5000ms, görev süresi = 3s
|--TASK--| . . . . . |--TASK--| . . . . . |--TASK--|
0 3 8 11 16 19
└─── 5s ────┘ └─── 5s ────┘Karşılaştırma Tablosu
| Özellik | fixedRate | fixedDelay |
|---|---|---|
| Zamanlama | Başlangıçtan başlangığa | Bitişten başlangığa |
| Taşma | Görev uzarsa birikim olur | Birikmez, arayı bekler |
| Kullanım | Sabit frekanslı (heartbeat, metrik) | Sıralı, çakışmasın istenen |
| Risk | Thread pool tükenebilir | Görev uzarsa frekans düşer |
| Garantisi | "Her X ms'de bir TETİKLE" | "Bitimden X ms sonra TETİKLE" |
Ne Zaman Hangisi?
fixedRate kullanın:
→ Heartbeat, health check, metrik toplama
→ Görevin süresi kısa ve öngörülebilir
→ Birikim olsa bile kabul edilebilir
fixedDelay kullanın:
→ Dış servis senkronizasyonu
→ Veritabanı temizleme
→ Görev süresi değişken
→ İki çalışma arasında kesinlikle boşluk olmalıinitialDelay
Uygulama başlangıcında zamanlanmış görevin hemen çalışmamasını isteyebilirsiniz. initialDelay, ilk çalıştırmayı geciktirir:
// Uygulama başladıktan 10 saniye sonra ilk çalışma, sonra her 60 saniyede bir
@Scheduled(fixedRate = 60000, initialDelay = 10000)
public void warmUpCache() {
log.info("Cache warm-up başladı");
cacheService.warmUp();
}
// Uygulama başladıktan 30 saniye sonra ilk çalışma, sonra 5 dakikada bir
@Scheduled(fixedDelay = 300000, initialDelay = 30000)
public void syncData() {
externalService.fullSync();
}Neden initialDelay önemlidir?
Uygulama tam başlamadan scheduled task çalışırsa, bağımlılıklar (veritabanı, cache, dış servisler) henüz hazır olmayabilir
Birden fazla scheduled task aynı anda başlarsa başlangıç yükü artar
Health check bitmeden görevlerin çalışması istenmiyor olabilir
Cron Expressions
Daha karmaşık zamanlama senaryoları için cron ifadeleri kullanılır. Spring, Unix cron'undan farklı olarak 6 alanlı cron formatı kullanır:
┌───────────── saniye (0-59)
│ ┌───────────── dakika (0-59)
│ │ ┌───────────── saat (0-23)
│ │ │ ┌───────────── gün (1-31)
│ │ │ │ ┌───────────── ay (1-12 veya JAN-DEC)
│ │ │ │ │ ┌───────────── haftanın günü (0-7 veya MON-SUN, 0 ve 7 = Pazar)
│ │ │ │ │ │
* * * * * *Özel Karakterler
| Karakter | Anlam | Örnek |
|---|---|---|
* | Her değer | * * * * * * → her saniye |
, | Liste | 0,15,30,45 * * * * * → 0, 15, 30, 45. saniyede |
- | Aralık | 0 0 9-17 * * * → 9:00–17:00 arası her saat başı |
/ | Artış | 0 */5 * * * * → her 5 dakikada bir |
? | Belirsiz (gün/hafta) | 0 0 12 ? * MON → her Pazartesi 12:00 |
L | Son | 0 0 0 L * * → ayın son günü gece yarısı |
# | N. hafta günü | 0 0 10 ? * MON#2 → ayın 2. Pazartesisi 10:00 |
W | En yakın hafta içi gün | 0 0 10 15W * * → 15'ine en yakın hafta içi gün |
Yaygın Cron Örnekleri
// Her gün gece yarısı (00:00:00)
@Scheduled(cron = "0 0 0 * * *")
public void midnightCleanup() {
sessionRepository.deleteExpired();
tempFileService.cleanOldFiles();
}
// Her Pazartesi sabah 08:00
@Scheduled(cron = "0 0 8 * * MON")
public void weeklyReport() {
Report report = reportService.generateWeekly();
emailService.sendToManagement(report);
}
// Hafta içi her gün 09:00-17:00 arası her 30 dakikada
@Scheduled(cron = "0 0/30 9-17 * * MON-FRI")
public void businessHoursSync() {
inventoryService.syncWithWarehouse();
}
// Her ayın 1'i saat 06:00
@Scheduled(cron = "0 0 6 1 * *")
public void monthlyBilling() {
billingService.generateMonthlyInvoices();
}
// Her 10 saniyede bir
@Scheduled(cron = "0/10 * * * * *")
public void frequentCheck() {
healthMonitor.checkExternalServices();
}
// Her yılın 1 Ocak'ı saat 00:00
@Scheduled(cron = "0 0 0 1 1 *")
public void yearlyArchive() {
archiveService.archivePreviousYear();
}Timezone Desteği
Cron ifadelerinde zaman dilimi belirtebilirsiniz. Bu, farklı bölgelerdeki müşteriler için kritiktir:
@Scheduled(cron = "0 0 9 * * *", zone = "Europe/Istanbul")
public void istanbulMorning() {
log.info("İstanbul saati ile 09:00 — günaydın!");
}
@Scheduled(cron = "0 0 18 * * MON-FRI", zone = "America/New_York")
public void newYorkEndOfDay() {
log.info("New York saati ile 18:00 — gün sonu raporu");
reportService.generateDailyClose();
}💡 İpucu:
zonebelirtmezseniz JVM'in varsayılan timezone'u kullanılır. Production'da her zaman zone belirtin — sunucu timezone'una bağımlı olmayın.
Dinamik Scheduling
application.yml'den Cron Okuma
app:
scheduling:
cleanup-cron: "0 0 2 * * *"
report-cron: "0 0 8 * * MON"
sync-interval: 60000@Component
public class ConfigurableTasks {
@Scheduled(cron = "${app.scheduling.cleanup-cron}")
public void cleanup() {
log.info("Temizlik görevi çalışıyor");
}
@Scheduled(cron = "${app.scheduling.report-cron}")
public void weeklyReport() {
log.info("Haftalık rapor oluşturuluyor");
}
@Scheduled(fixedRateString = "${app.scheduling.sync-interval}")
public void sync() {
log.info("Senkronizasyon çalışıyor");
}
}Programatik Scheduling (Runtime'da Değiştirilebilir)
@Configuration
public class DynamicSchedulerConfig implements SchedulingConfigurer {
@Autowired
private ScheduleRepository scheduleRepository;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
// Runtime'da cron ifadesi değiştirilebilen görev
taskRegistrar.addTriggerTask(
() -> log.info("Dinamik görev çalıştı: {}", Instant.now()),
triggerContext -> {
// Her çalışmada veritabanından güncel cron'u oku
String cron = scheduleRepository.findCronByTaskName("dynamic-task")
.orElse("0 0 * * * *"); // varsayılan: her saat
return new CronTrigger(cron).nextExecution(triggerContext);
}
);
}
}Scheduling Thread Pool
Varsayılan olarak Spring, zamanlanmış görevler için tek bir thread kullanır. Birden fazla zamanlanmış görev varsa, biri diğerini bloklayabilir:
❌ Tek thread ile problem:
Thread-1: [===Cleanup (30s)===][===Report (5s)===][===Sync (2s)===]
0 30 35 37
Cleanup 30 saniye sürdü → Report ve Sync 30 saniye GECİKTİ!Çözüm: Thread pool boyutunu artırın:
spring:
task:
scheduling:
pool:
size: 5
thread-name-prefix: "scheduler-"Veya programatik olarak:
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}✅ 5 thread ile:
Thread-1: [===Cleanup (30s)===]
Thread-2: [=Report (5s)=]
Thread-3: [Sync]
0 30
Hepsi aynı anda başlar! Birbirini beklemez.Dağıtık Sistemlerde Scheduling: ShedLock
Uygulamanız birden fazla instance'da çalışıyorsa (Kubernetes, auto-scaling), aynı scheduled task her instance'da çalışır. Gece yarısı temizlik görevi 5 instance'da 5 kez çalışır — bu genellikle istenmeyen bir durumdur.
ShedLock Çözümü
ShedLock, paylaşılan bir kilit (lock) mekanizması ile scheduled task'ın sadece bir instance'da çalışmasını garanti eder:
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>5.10.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>5.10.0</version>
</dependency>@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.usingDbTime()
.build()
);
}
}
// SQL tablosu (bir kez oluşturun)
// CREATE TABLE shedlock (
// name VARCHAR(64) NOT NULL PRIMARY KEY,
// lock_until TIMESTAMP NOT NULL,
// locked_at TIMESTAMP NOT NULL,
// locked_by VARCHAR(255) NOT NULL
// );@Component
@Slf4j
public class DistributedTasks {
@Scheduled(cron = "0 0 2 * * *")
@SchedulerLock(
name = "nightly-cleanup",
lockAtLeastFor = "5m", // En az 5 dakika kilitle (çok hızlı biterse bile)
lockAtMostFor = "30m" // En fazla 30 dakika kilitle (çökerse bile)
)
public void nightlyCleanup() {
log.info("Gece temizliği başladı — sadece BİR instance çalıştırıyor");
cleanupService.execute();
}
@Scheduled(cron = "0 0 8 * * MON")
@SchedulerLock(name = "weekly-report", lockAtMostFor = "1h")
public void weeklyReport() {
reportService.generateAndEmail();
}
}Yaygın Hatalar
1. Exception Yönetimi
// ❌ YANLIŞ — exception yakalanmazsa sonraki çalışmalar ETKİLENMEZ
// ama log'a düşmez (bazen)
@Scheduled(fixedRate = 60000)
public void riskyTask() {
externalService.sync(); // Exception fırlatabilir
}
// ✅ DOĞRU — her zaman try-catch ile sarın
@Scheduled(fixedRate = 60000)
public void safeTask() {
try {
externalService.sync();
} catch (Exception e) {
log.error("Scheduled task hatası", e);
meterRegistry.counter("scheduled.errors", "task", "sync").increment();
}
}2. Uzun Süren Görevler + Tek Thread
// ❌ Cleanup 30 saniye sürüyor, healthCheck 5 saniyede bir çalışmalı
// Ama tek thread olduğu için healthCheck, cleanup bitene kadar bekler
@Scheduled(cron = "0 0 2 * * *")
public void cleanup() { /* 30 saniye */ }
@Scheduled(fixedRate = 5000)
public void healthCheck() { /* 100ms */ }
// ✅ ÇÖZÜM: Thread pool boyutunu artırın
// spring.task.scheduling.pool.size=53. @Scheduled + @Async Karmaşası
// ⚠️ DİKKAT: @Scheduled + @Async birlikte kullanılabilir ama dikkatli olun
@Scheduled(fixedRate = 5000)
@Async // Scheduled thread'i serbest bırakır, görev async pool'da çalışır
public void asyncScheduledTask() {
// Bu şekilde scheduled thread pool bloklanmaz
// AMA: görev hâlâ çalışırken yeni trigger gelirse
// birden fazla instance aynı anda çalışabilir!
}Gerçek Dünya Örneği: E-Ticaret Scheduled Tasks
@Component
@Slf4j
@RequiredArgsConstructor
public class ECommerceScheduledTasks {
private final CartCleanupService cartCleanupService;
private final ReportService reportService;
private final CacheService cacheService;
private final HealthCheckService healthCheckService;
private final MeterRegistry meterRegistry;
// Terk edilmiş sepetleri temizle — her gece 03:00
@Scheduled(cron = "0 0 3 * * *", zone = "Europe/Istanbul")
@SchedulerLock(name = "abandoned-cart-cleanup", lockAtMostFor = "1h")
public void cleanupAbandonedCarts() {
try {
log.info("🛒 Terk edilmiş sepet temizliği başladı");
int cleaned = cartCleanupService.cleanOlderThan(Duration.ofDays(7));
log.info("✅ {} terk edilmiş sepet temizlendi", cleaned);
meterRegistry.gauge("carts.cleaned", cleaned);
} catch (Exception e) {
log.error("❌ Sepet temizleme hatası", e);
}
}
// Günlük satış raporu — her gece 23:30
@Scheduled(cron = "0 30 23 * * *", zone = "Europe/Istanbul")
@SchedulerLock(name = "daily-sales-report", lockAtMostFor = "30m")
public void dailySalesReport() {
try {
log.info("📊 Günlük satış raporu oluşturuluyor");
SalesReport report = reportService.generateDaily(LocalDate.now());
reportService.emailToManagement(report);
log.info("✅ Günlük rapor gönderildi");
} catch (Exception e) {
log.error("❌ Rapor oluşturma hatası", e);
}
}
// Popüler ürün cache'ini yenile — her 15 dakikada
@Scheduled(fixedRate = 900000, initialDelay = 60000)
public void refreshPopularProductsCache() {
try {
cacheService.refreshPopularProducts();
} catch (Exception e) {
log.warn("Cache yenileme hatası — eski cache kullanılmaya devam edecek", e);
}
}
// Dış servis sağlık kontrolü — her 30 saniyede
@Scheduled(fixedRate = 30000)
public void externalServiceHealthCheck() {
Map<String, Boolean> results = healthCheckService.checkAll();
results.forEach((service, healthy) -> {
if (!healthy) {
log.warn("⚠️ Dış servis DOWN: {}", service);
}
meterRegistry.gauge("external.health." + service, healthy ? 1 : 0);
});
}
}Özet
@Scheduled, Spring Boot'ta periyodik görevler tanımlamanın en kolay yoludur.@EnableSchedulingile aktifleştirilir.fixedRate sabit frekanslı görevler (heartbeat, metrik) için, fixedDelay sıralı ve çakışmaması gereken görevler (senkronizasyon, temizlik) için kullanılır.
Cron ifadeleri karmaşık zamanlama senaryolarını destekler. Spring, 6 alanlı format kullanır (saniye dahil).
zoneparametresi ile timezone belirtin.Varsayılan tek thread birden fazla görevi bloklayabilir.
spring.task.scheduling.pool.size=5ile thread sayısını artırın.Dağıtık sistemlerde (birden fazla instance) aynı görevin tekrar çalışmasını önlemek için ShedLock kullanın.
Her scheduled metodu try-catch ile sarın — yakalanmayan exception loglara düşmeyebilir. Exception, sonraki çalışmaları engellemez ama loglanması gerekir.
initialDelay ile uygulama tam başlamadan görev çalışmasını önleyin. Bağımlılıklar (veritabanı, cache, dış servisler) hazır olmadan çalışan görevler hata fırlatır.
Dinamik scheduling ihtiyacınız varsa
SchedulingConfigurerarayüzünü implement edin. Veritabanından cron ifadesi okuyarak runtime'da zamanlama değiştirin — yeniden deploy gerekmez.
AI Asistan
Sorularını yanıtlamaya hazır