Eğitim Portalı/Spring Boot/Dağıtık Transaction ve Saga Pattern
Spring Boot03-spring-boot/08-dagitik-transaction-ve-saga

Dağıtık Transaction ve Saga Pattern

@Transactional ile tek bir veritabanında "ya hepsi ya hiçbiri" garantisini öğrendik. Ama mikroservis mimarisinde bir iş akışı birden çok servise ve veritabanına yayılır: sipariş servisi, stok servisi, ödeme servisi, k…

Dağıtık Transaction ve Saga Pattern

@Transactional ile tek bir veritabanında "ya hepsi ya hiçbiri" garantisini öğrendik. Ama mikroservis mimarisinde bir iş akışı birden çok servise ve veritabanına yayılır: sipariş servisi, stok servisi, ödeme servisi, kargo servisi — her biri kendi veritabanına sahip. Burada tek bir @Transactional işe yaramaz; bir servisin transaction'ı diğerinin veritabanını kapsayamaz. Bu bölüm, dağıtık dünyada tutarlılığı nasıl sağladığımızı derinlemesine ele alır.

Sorun: yerel transaction yetmez

Klasik transaction (ACID), tek bir kaynak (veritabanı) içinde güçlü tutarlılık sağlar. Ama "stok düş + ödeme al + kargo oluştur" adımları farklı servislerdeyse, biri başarılı diğeri başarısız olabilir ve sistem tutarsız kalır (para alındı ama kargo yok gibi). İki temel yaklaşım vardır: 2PC (güçlü tutarlılık) ve Saga (eventual consistency).

Yol 1: Two-Phase Commit (2PC / XA)

2PC, dağıtık bir "ya hepsi ya hiçbiri" protokolüdür. Bir koordinatör iki fazda çalışır:

  1. Prepare (hazırlık): Tüm katılımcılara "commit'e hazır mısın?" sorulur. Her katılımcı kaynaklarını kilitler ve oy verir (evet/hayır).
  2. Commit/Abort: Hepsi "evet" derse koordinatör commit, biri "hayır" derse abort emri verir.

Örnek 1 (./Ornek1.java) bunu simüle eder: bir senaryoda hepsi hazır → commit; diğerinde biri reddeder → hepsi abort.

Neden mikroservislerde tercih edilmez?

  • ✅ Güçlü, anlık tutarlılık.
  • Bloklama: Katılımcılar prepare ile commit arasında kaynakları kilitler; tek bir yavaş servis tüm işlemi bekletir.
  • Koordinatör çökerse katılımcılar "kararsız" (in-doubt) ve kilitli kalır.
  • ❌ Tüm servislerin XA desteği ve sıkı bağ gerektirir; ölçeklenmez, dayanıklılığı düşürür.

Bu nedenle modern dağıtık sistemler genelde 2PC yerine Saga'yı seçer.

Yol 2: Saga Pattern

Saga, dağıtık işlemi bir dizi yerel transaction'a böler. Her adım kendi veritabanında commit eder; bir adım başarısız olursa, daha önce tamamlanan adımlar telafi (compensating) transaction'larıyla geri alınır. Dağıtık kilit yoktur; sonuç eventual consistency'dir (anlık değil, kısa süre sonra tutarlı).

Telafi (compensation): Yapılan bir işlemi mantıksal olarak geri alan iş işlemi. Örneğin "ödeme al"ın telafisi "ödeme iade et"tir. Dikkat: telafi gerçek bir "rollback" değildir; ödeme zaten commit edilmiştir, sen bir iade işlemi yaparsın.

Saga'nın iki uygulama biçimi vardır:

a) Orchestration (orkestrasyon) — merkezi koordinatör

Bir orkestratör adımları sırayla çağırır ve hata olunca telafileri tetikler:

StokRezerve -> Ödeme -> Kargo        (ileri)
   ↑ hata olursa: telafi TERS sırada (Kargo iptal <- Ödeme iade <- Stok iade)

Örnek 2 (./Ornek2.java) bir sipariş sagasını orkestratörle yürütür: başarılı senaryoda tüm adımlar commit; kargo adımı başarısız olunca tamamlanan adımlar (ödeme, stok) ters sırada telafi edilir.

  • ✅ Akış tek yerde, net ve izlenebilir.
  • ❌ Orkestratör merkezî bir bağımlılık/karmaşıklık noktasıdır.

b) Choreography (koreografi) — olay tabanlı

Merkezi koordinatör yoktur. Her servis ilgili olayı dinler, işini yapar ve yeni bir olay yayınlar. Telafi de olaylarla tetiklenir:

SiparisOlusturuldu -> StokRezerveEdildi -> OdemeAlindi -> Gonderildi
                              ↑ OdemeReddedildi olayını Stok dinler -> rezervasyonu geri alır

Örnek 3 (./Ornek3.java) bir olay veriyolu (Kafka/RabbitMQ benzeri) üzerinden bunu simüle eder: mutlu yolda olaylar zinciri akar; ödeme reddedilince stok servisi telafi yapar.

  • ✅ Gevşek bağlılık, tek hata noktası yok, kolay ölçeklenir.
  • ❌ Uçtan uca akışı izlemek zordur (olaylar dağınık).

Saga'yı güvenli kılan yardımcı desenler

  • Outbox Pattern: "Veritabanına yaz + olay yayınla" iki ayrı sistemdir; biri başarılı diğeri başarısız olursa tutarsızlık doğar. Çözüm: olayı, iş verisiyle aynı yerel transaction'da bir outbox tablosuna yaz; ayrı bir süreç bu tablodan okuyup mesaj kuyruğuna güvenle iletir.
  • Idempotency (etkisizlik): Mesajlar tekrar teslim edilebilir; her işleyici aynı mesajı iki kez alsa da sonucu değiştirmemeli (örn. işlenen mesaj id'lerini takip et).
  • TCC (Try-Confirm/Cancel): Saga'nın bir varyantı; her adım önce "try" (rezervasyon) sonra "confirm" veya "cancel" yapar.
  • Eventual consistency'yi tasarıma yansıt: Kullanıcıya "siparişiniz işleniyor" de; durum birkaç saniye/dakika içinde kesinleşir.

Spring/Java ekosisteminde Saga

Spring'in @Transactionalyerel transaction içindir; dağıtık saga için hazır bir anotasyon yoktur. Pratikte:

  • Mesajlaşma: Spring Boot + RabbitMQ/Kafka (Spring for Apache Kafka, Spring AMQP) ile choreography kurarsın; @KafkaListener/@RabbitListener ile olayları dinlersin.
  • Outbox: Debezium (CDC) veya manuel outbox tablosu.
  • Hazır framework'ler: Axon Framework, Eventuate Tram Saga, Camunda/Temporal (workflow orkestrasyonu) saga'yı yönetmeyi kolaylaştırır.
  • Dayanıklılık: Resilience4j (retry, circuit breaker) saga adımlarını güvenceye alır.

Hangisini seçmeli?

DurumÖneri
Tek veritabanı, tek servisKlasik @Transactional (saga'ya gerek yok)
Birkaç adımlı, net iş akışıSaga Orchestration
Çok sayıda servis, gevşek bağ, yüksek ölçekSaga Choreography
Mutlak anlık tutarlılık şart (nadiren)2PC/XA (maliyetine katlanarak)

Özet

Dağıtık transaction'da neden yerel @Transactional'ın yetmediğini; 2PC/XA'nın güçlü ama bloklayıcı olduğunu (Örnek 1); Saga ile işi yerel transaction'lara bölüp telafi transaction'larıyla tutarlılığı sağladığımızı — hem orchestration (Örnek 2) hem choreography (Örnek 3) biçimiyle — gördük; Outbox, idempotency, TCC gibi yardımcı desenleri ve Spring/Java ekosistemindeki araçları ele aldık. Dağıtık dünyada hedef "anlık" değil eventual consistency'dir; doğru desen, işin doğasına göre seçilir.

Kod Örnekleri(3)

Ornek1

çalıştırılabilir
Ornek1.java
1// Ornek1: Dağıtık transaction neden zor? — Two-Phase Commit (2PC/XA) simülasyonu.
2// Birden çok servis/veritabanı tek bir "ya hepsi ya hiçbiri" işleminde nasıl anlaşır?
3// (Tek JVM'de simülasyon; amaç protokolün mantığını ve ZAYIFLIĞINI göstermek.)
4// Çalıştırma: java Ornek1.java
5import java.util.List;
6
7public class Ornek1 {
8
9    // Bir katılımcı (örn. ayrı bir mikroservis/veritabanı).
10    static class Katilimci {
11        final String ad;
12        final boolean hazirlanabilir; // bu katılımcı prepare aşamasında "evet" diyebilir mi?
13        boolean kilitli = false;
14        Katilimci(String ad, boolean hazirlanabilir) { this.ad = ad; this.hazirlanabilir = hazirlanabilir; }
15
16        // FAZ 1: prepare — "commit'e hazır mısın?" Kaynakları KİLİTLER ve oy verir.
17        boolean prepare() {
18            if (hazirlanabilir) {
19                kilitli = true; // kaynak kilitlendi; commit/abort gelene kadar BEKLER
20                System.out.println("    [" + ad + "] prepare -> EVET (kaynak kilitlendi)");
21                return true;
22            }
23            System.out.println("    [" + ad + "] prepare -> HAYIR (örn. yetersiz stok)");
24            return false;
25        }
26        void commit() { kilitli = false; System.out.println("    [" + ad + "] COMMIT (kilit serbest)"); }
27        void abort()  { kilitli = false; System.out.println("    [" + ad + "] ABORT (geri alındı)"); }
28    }
29
30    // Koordinatör (transaction manager): 2PC protokolünü yürütür.
31    static boolean ikiFazliCommit(List<Katilimci> katilimcilar) {
32        // FAZ 1: tüm katılımcılara prepare sor.
33        System.out.println("  FAZ 1 (prepare): tüm katılımcılara soruluyor...");
34        boolean hepsiHazir = true;
35        for (Katilimci k : katilimcilar) {
36            if (!k.prepare()) hepsiHazir = false;
37        }
38        // FAZ 2: oybirliği varsa COMMIT, yoksa hepsini ABORT.
39        System.out.println("  FAZ 2: " + (hepsiHazir ? "oybirliği -> COMMIT" : "biri reddetti -> ABORT"));
40        for (Katilimci k : katilimcilar) {
41            if (hepsiHazir) k.commit(); else k.abort();
42        }
43        return hepsiHazir;
44    }
45
46    public static void main(String[] args) {
47        System.out.println("=== Senaryo 1: hepsi hazır -> COMMIT ===");
48        boolean s1 = ikiFazliCommit(List.of(
49                new Katilimci("StokServisi", true),
50                new Katilimci("OdemeServisi", true),
51                new Katilimci("KargoServisi", true)));
52        System.out.println("  Sonuç: " + (s1 ? "BAŞARILI (tümü commit)" : "iptal"));
53
54        System.out.println("\n=== Senaryo 2: biri hazır değil -> tümü ABORT ===");
55        boolean s2 = ikiFazliCommit(List.of(
56                new Katilimci("StokServisi", false),  // stok yetersiz -> prepare HAYIR
57                new Katilimci("OdemeServisi", true),
58                new Katilimci("KargoServisi", true)));
59        System.out.println("  Sonuç: " + (s2 ? "başarılı" : "İPTAL (tümü geri alındı)"));
60
61        System.out.println("""
62
63                --- 2PC (XA) neden mikroservislerde tercih edilmez? ---
64                + Güçlü tutarlılık (atomiklik) sağlar.
65                - BLOKLAMA: katılımcılar prepare ile commit arasında kaynakları KİLİTLER; yavaş servis hepsini bekletir.
66                - Koordinatör çökerse katılımcılar belirsizlikte kalır (kilitli, kararsız).
67                - Tüm servislerin XA desteği + sıkı bağ gerekir; ölçeklenmez, dayanıklılığı düşürür.
68                Bu yüzden modern dağıtık sistemler 2PC yerine SAGA + 'eventual consistency' kullanır (Örnek 2-3).""");
69    }
70}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.

Ornek2

çalıştırılabilir
Ornek2.java
1// Ornek2: SAGA PATTERN — Orchestration (orkestrasyon).
2// Dağıtık bir işlem, her biri kendi yerel transaction'ı olan adımlara bölünür.
3// Bir adım başarısız olursa, tamamlanmış adımlar TERS sırada TELAFİ (compensate) edilir.
4// (Tek JVM simülasyonu; gerçekte her adım ayrı mikroservis çağrısıdır.)
5// Çalıştırma: java Ornek2.java
6import java.util.ArrayList;
7import java.util.List;
8
9public class Ornek2 {
10
11    // Bir saga adımı: ileri işlem + telafi (geri alma) işlemi.
12    interface SagaAdimi {
13        String ad();
14        void calistir();      // yerel transaction (ileri)
15        void telafiEt();      // compensating transaction (geri al)
16    }
17
18    // Orkestratör: adımları sırayla çalıştırır; hata olursa tamamlananları TERS sırada telafi eder.
19    static class Orkestrator {
20        boolean calistir(List<SagaAdimi> adimlar) {
21            List<SagaAdimi> tamamlanan = new ArrayList<>();
22            try {
23                for (SagaAdimi adim : adimlar) {
24                    System.out.println("  -> " + adim.ad() + " çalışıyor...");
25                    adim.calistir();
26                    tamamlanan.add(adim);
27                }
28                System.out.println("  ✓ Saga başarıyla tamamlandı (tüm adımlar commit).");
29                return true;
30            } catch (RuntimeException hata) {
31                System.out.println("  ✗ HATA: " + hata.getMessage() + " -> telafi başlıyor (ters sıra):");
32                // Tamamlanan adımları SON yapılandan başlayarak geri al.
33                for (int i = tamamlanan.size() - 1; i >= 0; i--) {
34                    System.out.println("     telafi: " + tamamlanan.get(i).ad());
35                    tamamlanan.get(i).telafiEt();
36                }
37                System.out.println("  Saga geri alındı (tutarlı duruma dönüldü).");
38                return false;
39            }
40        }
41    }
42
43    // Sipariş sagası adımları: stok rezerve -> ödeme -> kargo.
44    static SagaAdimi adim(String ad, boolean basarili, Runnable ileri, Runnable telafi) {
45        return new SagaAdimi() {
46            public String ad() { return ad; }
47            public void calistir() {
48                ileri.run();
49                if (!basarili) throw new RuntimeException(ad + " başarısız");
50            }
51            public void telafiEt() { telafi.run(); }
52        };
53    }
54
55    public static void main(String[] args) {
56        Orkestrator orkestrator = new Orkestrator();
57
58        System.out.println("=== Senaryo 1: tüm adımlar başarılı ===");
59        orkestrator.calistir(List.of(
60                adim("StokRezervasyonu", true,
61                        () -> System.out.println("       stok 1 adet düşüldü"),
62                        () -> System.out.println("       stok iade edildi")),
63                adim("OdemeAlma", true,
64                        () -> System.out.println("       450 TL tahsil edildi"),
65                        () -> System.out.println("       450 TL iade edildi")),
66                adim("KargoPlanlama", true,
67                        () -> System.out.println("       kargo oluşturuldu"),
68                        () -> System.out.println("       kargo iptal edildi"))));
69
70        System.out.println("\n=== Senaryo 2: KARGO adımı başarısız -> telafi ===");
71        orkestrator.calistir(List.of(
72                adim("StokRezervasyonu", true,
73                        () -> System.out.println("       stok 1 adet düşüldü"),
74                        () -> System.out.println("       stok iade edildi")),
75                adim("OdemeAlma", true,
76                        () -> System.out.println("       450 TL tahsil edildi"),
77                        () -> System.out.println("       450 TL iade edildi")),
78                adim("KargoPlanlama", false,  // <-- burada patlar
79                        () -> System.out.println("       kargo deneniyor..."),
80                        () -> System.out.println("       (telafi gerekmez, tamamlanmadı)"))));
81
82        System.out.println("""
83
84                --- Saga: Orchestration ---
85                Merkezi bir ORKESTRATÖR adımları yönetir ve hata olunca telafileri tetikler.
86                Her adım kendi YEREL transaction'ıdır; 2PC gibi dağıtık kilit YOKTUR.
87                Telafi (compensation): yapılanı geri alan iş işlemi (ödeme iadesi, stok iadesi...).
88                Sıra: ileri 1->2->3; hata olursa telafi 3<-2<-1 (ters).
89                Artı: net akış, kolay izleme. Eksi: orkestratör merkezî bir bağımlılıktır.
90                Sonuç 'eventual consistency'dir: anlık değil, kısa süre sonra tutarlı.""");
91    }
92}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.

Ornek3

çalıştırılabilir
Ornek3.java
1// Ornek3: SAGA PATTERN — Choreography (koreografi, olay tabanlı).
2// Merkezi orkestratör YOKTUR; her servis olayları dinler, işini yapar ve YENİ olay yayınlar.
3// Hata olunca telafi de olaylarla tetiklenir. (Tek JVM'de basit bir olay veriyolu simülasyonu.)
4// Çalıştırma: java Ornek3.java
5import java.util.ArrayList;
6import java.util.HashMap;
7import java.util.List;
8import java.util.Map;
9import java.util.function.Consumer;
10
11public class Ornek3 {
12
13    // --- Olaylar (servisler arası mesajlar) ---
14    sealed interface Olay permits SiparisOlusturuldu, StokRezerveEdildi, OdemeAlindi, OdemeReddedildi, Gonderildi {}
15    record SiparisOlusturuldu(String urun) implements Olay {}
16    record StokRezerveEdildi(String urun) implements Olay {}
17    record OdemeAlindi(String urun) implements Olay {}
18    record OdemeReddedildi(String urun, String sebep) implements Olay {}
19    record Gonderildi(String urun) implements Olay {}
20
21    // --- Basit olay veriyolu (gerçekte Kafka/RabbitMQ) ---
22    static class OlayVeriyolu {
23        private final Map<Class<?>, List<Consumer<Olay>>> aboneler = new HashMap<>();
24        <T extends Olay> void abone(Class<T> tip, Consumer<Olay> handler) {
25            aboneler.computeIfAbsent(tip, k -> new ArrayList<>()).add(handler);
26        }
27        void yayinla(Olay olay) {
28            System.out.println("  >> olay: " + olay.getClass().getSimpleName()
29                    + (olay instanceof OdemeReddedildi r ? " (" + r.sebep() + ")" : ""));
30            aboneler.getOrDefault(olay.getClass(), List.of()).forEach(h -> h.accept(olay));
31        }
32    }
33
34    public static void main(String[] args) {
35        // odemeBasarili=true -> mutlu yol; false -> ödeme reddedilir, telafi tetiklenir.
36        System.out.println("=== Senaryo 1: ödeme başarılı (mutlu yol) ===");
37        akisiKur(true).yayinla(new SiparisOlusturuldu("Klavye"));
38
39        System.out.println("\n=== Senaryo 2: ödeme reddedildi -> telafi (stok iadesi) ===");
40        akisiKur(false).yayinla(new SiparisOlusturuldu("Mouse"));
41
42        System.out.println("""
43
44                --- Saga: Choreography ---
45                Merkezi koordinatör YOK; her servis ilgili olayı dinleyip işini yapar ve yeni olay yayınlar.
46                Akış: SiparisOlusturuldu -> StokRezerveEdildi -> OdemeAlindi -> Gonderildi.
47                Hata: OdemeReddedildi olayını Stok servisi dinler ve rezervasyonu geri alır (telafi).
48                Artı: gevşek bağlılık, tek hata noktası yok, kolay ölçeklenir.
49                Eksi: akışı uçtan uca izlemek zorlaşır (olaylar dağınık). Bu yüzden 'outbox pattern' ve
50                izleme (distributed tracing) kullanılır.""");
51    }
52
53    // Servisleri olaylara abone ederek koreografiyi kurar.
54    static OlayVeriyolu akisiKur(boolean odemeBasarili) {
55        OlayVeriyolu bus = new OlayVeriyolu();
56
57        // Stok servisi: sipariş gelince rezerve eder; ödeme reddedilirse rezervasyonu geri alır.
58        bus.abone(SiparisOlusturuldu.class, o -> {
59            String urun = ((SiparisOlusturuldu) o).urun();
60            System.out.println("     [Stok] " + urun + " rezerve edildi");
61            bus.yayinla(new StokRezerveEdildi(urun));
62        });
63        bus.abone(OdemeReddedildi.class, o -> {
64            OdemeReddedildi r = (OdemeReddedildi) o;
65            System.out.println("     [Stok] TELAFİ: " + r.urun() + " rezervasyonu iptal edildi (stok iadesi)");
66        });
67
68        // Ödeme servisi: stok rezerve olunca ücreti tahsil etmeye çalışır.
69        bus.abone(StokRezerveEdildi.class, o -> {
70            String urun = ((StokRezerveEdildi) o).urun();
71            if (odemeBasarili) {
72                System.out.println("     [Ödeme] " + urun + " için ücret tahsil edildi");
73                bus.yayinla(new OdemeAlindi(urun));
74            } else {
75                System.out.println("     [Ödeme] " + urun + " için tahsilat BAŞARISIZ");
76                bus.yayinla(new OdemeReddedildi(urun, "yetersiz bakiye"));
77            }
78        });
79
80        // Kargo servisi: ödeme alınınca gönderir.
81        bus.abone(OdemeAlindi.class, o -> {
82            String urun = ((OdemeAlindi) o).urun();
83            System.out.println("     [Kargo] " + urun + " gönderildi");
84            bus.yayinla(new Gonderildi(urun));
85        });
86
87        return bus;
88    }
89}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.