Fintech alanında çalıştığımızı ve bir raporlama uygulamamız olduğunu varsayalım. Bu uygulama her gece saat 02:00’de zamanlanmış bir işi çalıştırır. Bu iş, gün boyunca gerçekleşen tüm işlemlerin (para transferleri, fatura ödemeleri vb.) raporunu oluşturur. İşlemler raporlandıktan sonra “raporlandı” olarak işaretlenir ve ileri işlem için başka bir sisteme iletilir.

Şimdi, bu uygulamanın Kubernetes üzerinde birden fazla pod ile barındırıldığını düşünelim. Diyelim ki iki pod var ve ikisi aynı anda zamanlanmış işi tetiklemeye çalışıyor. Böyle bir senaryoda ne olur?

  1. Çift rapor üretilebilir.
  2. Veri tutarsızlıkları oluşabilir.
  3. Bazı işlemler raporlardan çıkarılabilir.

 Kubernetes Zamanlanmış İş Hata Akışı

Böyle bir durumda, işin yalnızca bir pod tarafından tetiklendiğinden emin olmamız gerekir. Peki bunu nasıl sağlayabiliriz?

Bu sorunun en güvenilir ve pratik çözümlerinden biri ShedLock’tur. ShedLock, dağıtık kilitleme mekanizması sağlayarak zamanlanmış işlerin birden fazla instance arasında tekil (singleton) çalıştırılmasını garanti eder.

Nasıl Çalışır?

ShedLock’un temel çalışma prensibi oldukça basittir:İş başlamadan önce, bir instance kilit almaya çalışır. Böylece birden fazla pod aynı anda işi çalıştırmaya kalksa bile, yalnızca kilidi alabilen pod devam eder. İş tamamlandığında kilit serbest bırakılır. Önemli nokta şudur ki bu kilit uygulama belleğinde tutulmaz — harici bir sistemde (ör. veritabanı, MongoDB, Redis vb.) saklanır.

  1. Zamanlanmış bir iş çalışmadan hemen önce, ShedLock yapılandırılmış kilit sağlayıcısını (ör. veritabanı) kullanarak kilit almaya çalışır. Eğer kilit başarıyla alınırsa:
  2. → iş çalıştırılır. → kilit belirlenen süre boyunca tutulur.
  3. Eğer kilit başka bir instance tarafından alınmışsa: → iş çalıştırılmaz. → uygulama işi sessizce atlar. Bu süreç tamamen thread-safe ve instance’lar arası senkronizedir.

 ShedLock Zamanlanmış İş Akışı

ShedLock yalnızca kilit almakla kalmaz, aynı zamanda kilidin ne kadar süreyle tutulacağını da kontrol eder. Bunu yönetmek için iki temel parametreye dayanır: LockAtMostFor ve LockAtLeastFor.

LockAtMostFor

Bu, iş başladığında kilidin en fazla ne kadar süre tutulabileceğini tanımlar. İşin çökmesi veya uygulamanın beklenmedik şekilde kapanması durumunda kilidin süresiz tutulmasını önleyen bir güvenlik önlemidir. Örneğin: lockAtMostFor = 10m ise, iş tamamlanmamış olsa bile kilit 10 dakika sonra serbest bırakılır. Böylece diğer instance’lar gerekirse işi devralabilir.

Bulut Zamanlanmış İş Diyagramı

LockAtLeastFor

Bu, iş erken tamamlansa bile kilidin minimum ne kadar süreyle tutulacağını tanımlar. Çok hızlı tamamlanan işlerin çok sık tetiklenmesini engellemek için faydalıdır.

Örneğin: lockAtLeastFor = 2m ve iş yalnızca 10 saniyede tamamlansa bile, kilit en az 2 dakika boyunca tutulur. Böylece iş planlanan aralıktan önce tekrar çalıştırılmaz.

Bir iş başladığında, ShedLock alınan kilidi lockAtLeastFor süresince tutar; iş başarıyla tamamlansa da tamamlanmasa da.

İş bittiğinde kilit serbest bırakılır. Ancak işler her zaman planlandığı gibi gitmez — iş sırasında hata oluşursa kilit süresiz tutulabilir. Bunu önlemek için lockAtMostFor tanımlanır; bu sayede kilit en fazla belirtilen süre sonunda otomatik olarak serbest bırakılır.

ShedLock Çoklu Zamanlanmış İşler

Şimdi, ShedLock’un Spring tabanlı bir uygulamada nasıl uygulanacağına bakalım.

Öncelikle uygulamamıza shedlock-spring ve shedlock-provider-jdbc-template bağımlılıklarını ekliyoruz.

JDBC tabanlı uygulamalar için shedlock-provider-jdbc-template, Hibernate/JPA kullanan uygulamalar için ise shedlock-provider-jpa tercih edilmelidir.

<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>6.4.0</version>
</dependency>
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-jdbc-template</artifactId>
    <version>6.4.0</version>
</dependency> 

Kilit bilgisini saklamak için veritabanında bir tablo oluşturmamız gerekir. Örnek tablo scripti:

CREATE TABLE shedlock (
    name VARCHAR(64) NOT NULL,
    lock_until TIMESTAMP(3) NULL,
    locked_at TIMESTAMP(3) NULL,
    locked_by VARCHAR(255) NULL,
    PRIMARY KEY (name)
);  

Şimdi LockProvider tanımlamamız gerekiyor:

@Configuration
public class ShedLockConfig {
    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
        return new JdbcTemplateLockProvider(
            JdbcTemplateLockProvider.Configuration.builder()
                .withJdbcTemplate(new JdbcTemplate(dataSource))
                .usingDbTime()
                .build()
        );
    }
}   

Artık ShedLock’u zamanlanmış işlerimizde kullanabiliriz.

@Scheduled(cron = "0 0 2 * * *")
@SchedulerLock(name = "reportingJob", lockAtLeastFor = "5m", lockAtMostFor = "10m")
public void generateDailyReport() {
    // raporlama işlemleri  
}  

name parametresi, veritabanındaki kilit kaydı için benzersiz anahtar görevi görür. Eğer aynı isimde bir iş başka bir instance tarafından zaten çalıştırılıyorsa, ShedLock bunu algılar ve işin tekrar tetiklenmesini engeller. Bu nedenle her zamanlanmış iş için farklı bir name değeri verilmelidir.

Buraya kadar ShedLock’un ne olduğunu, neden kullanıldığını ve nasıl uygulanabileceğini inceledik.

Dağıtık sistemlerde, zamanlanmış görevlerin kontrolsüz bir şekilde birden fazla instance tarafından çalıştırılması; veri tutarsızlıklarına, gereksiz kaynak tüketimine ve operasyonel hatalara yol açabilir.

ShedLock bu soruna basit ama etkili bir çözüm sunar. Minimum yapılandırma ile entegre edilebilir, farklı veritabanı sağlayıcılarını destekler ve Spring uygulamalarıyla sorunsuz çalışır.

Uygulamanız @Scheduled anotasyonları kullanıyor ve aynı işin birden fazla kez tetiklenme ihtimali olan bir ortamda (özellikle Kubernetes veya benzeri container orkestrasyon sistemlerinde) çalışıyorsa, ShedLock sizin için doğru araç olabilir.