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:
- 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).
- 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
outboxtablosuna 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 @Transactional'ı yerel 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/@RabbitListenerile 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 servis | Klasik @Transactional (saga'ya gerek yok) |
| Birkaç adımlı, net iş akışı | Saga Orchestration |
| Çok sayıda servis, gevşek bağ, yüksek ölçek | Saga 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ılabilir1// 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}Ornek2
çalıştırılabilir1// 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}Ornek3
çalıştırılabilir1// 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}