İçeriğe geç

Saga Pattern

İleri 10 dk Çok sık karşılaşılır

Önce şunu oku: Monolit mi, mikroservis mi?

30 saniyede özet

Servisler ayrılınca 'hepsi ya da hiçbiri' garantisi kaybolur. Saga işi adım adım yapar; bir adım patlarsa öncekiler silinmez, iptal eden yeni adımlarla telafi edilir, tıpkı tatil rezervasyonunu iptal etmek gibi.

Bir tatil planlıyorsun: uçak, otel ve araba. İkisini ayırdın, üçüncüsü kalmamış. Şimdi ne yapacaksın?

Farklı şirketler arasında geri alma düğmesi yok; iptal var.
Adım adım oku
  1. İlk adım başarılı: uçak bileti ayrıldı.
  2. İkinci adım da başarılı: otel ayrıldı.
  3. Üçüncü adım başarısız: araba yok. Tek bir transaction olmadığı için her şeyi bir anda geri almak mümkün değil.
  4. Saga, başarılı olan adımları ters sırayla telafi adımlarıyla iptal eder: önce otel, sonra uçak.
  1. Bayt: Monolitte sipariş tek bir transaction'dı: ya hepsi olur, ya hiçbiri.

  2. Sen: Peki servislere böldükten sonra?

  3. Bayt: Ödeme alındı, kargo patladı. Parayı geri almak için bir geri alma düğmesi yok!

  4. Bayt: Tatil planında araba bulamazsan oteli geri almazsın, iptal edersin.

Tek parça bir uygulamada sipariş akışı tek bir sözle korunur: “ya hepsi olur, ya hiçbiri.”

Monolitte
@Transactional
public void placeOrder(Order order) {
orderRepo.save(order);
paymentService.charge(order); // bunlar aynı transaction içinde
inventoryService.reserve(order); // biri patlarsa hepsi geri alınır
}

Servisleri ayırdığın an bu garanti yok olur. Her servisin kendi veritabanı var, tek bir COMMIT yok. Saga bu boşluğu doldurur — ama verdiği şey aynı garanti değildir.

Geri alma yok, iptal var

Saga'nın üçüncü adımı olan kargo başarısız oldu. İlk adımda çekilen ödeme, veritabanından iz bırakmadan geri alınır mı? Cevabı göster

Hayır. Ödeme çoktan commit oldu; ancak yeni bir işlemle, bir iadeyle telafi edilir.

Dağıtık transaction’ı, her biri kendi veritabanına hemen commit eden yerel transaction’lar dizisine böl. Her adımın bir de telafi işlemiÖnceki adımın etkisini iş mantığı seviyesinde geri alan yeni bir işlem. Rollback değildir: ödeme silinmez, iade edilir — tarih korunur.Sözlükte gör → eylemi olsun. Bir adım başarısız olursa, önceki adımların telafileri ters sırada çalıştırılır.

Kafam karıştı, daha basit anlat

Saga’da geri alma yoktur, iptal vardır. Ödemeyi geri almazsın, iade edersin. Ekstrede iki satır görünür: ödeme ve iadesi.

Hızlı kontrolOrta

Saga deseni hangi problemi çözer?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin çalıştır

Aşağıda beş adımlı bir sipariş sagaBirden fazla servise yayılan işi yerel transaction'lara bölüp, başarısızlıkta telafi işlemleriyle geri saran desen. Atomiklik vermez, eventual consistency verir.Sözlükte gör →sı var. Hangi adım başarısız olsun kutusundan bir adım seç ve telafi zincirinin ters sırada nasıl geri sardığını izle.

Saga — ileri akış ve telafi zinciri

Tohum 3
Saga Orchestratoradımları sırayla çağırıyor
  1. Order Sipariş oluşturbekliyor
  2. Payment Ödemeyi çekbekliyor
  3. Inventory Stok rezerve etbekliyor
  4. Notification “Siparişin alındı” e-postası göndergeri alınamaz adımbekliyor
  5. Shipping Kargo oluşturbekliyor
Hız
Adım 0

Şu an ne oldu?

Order: Sipariş oluştur

Koordinatör bu adımı çağırdı ve cevabını bekliyor. Akışın tamamı tek bir yerde tanımlı.

Aklında kalsın: Her adım kendi veritabanına hemen commit eder. Bu yüzden saga ortasında başka bir okuyucu yarım kalmış bir durum görebilir.

Görevler0/3

  • Geri alınamayan bir adımı telafi etmek zorunda kalaçık

    İpucu

    Varsayılan senaryoyu sonuna kadar oynat. E-posta gönderildikten sonra bir adım düşerse ne "geri alınır"?

  • Hiçbir geri alınamaz etki bırakmadan geri saraçık

    İpucu

    Hatayı e-posta adımından önceye taşı. Asıl ders: geri alınamayan adımları saganın sonuna koy.

  • Mutlu yolda giden bir sagayı elle düşüraçık

    İpucu

    Başarısız adımı "Hiçbiri" yap, oynat ve akış ortadayken "Şu anki adımı düşür"e bas. Gerçek hatalar senaryoya uymaz.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

Mutlaka dene:

  • Shipping adımını düşür (varsayılan). Telafiler ters sırada geçiyor, ama “siparişin alındı” e-postası zaten gitmişti.
  • Order adımını düşür. Geri alınacak bir şey yok; ilk adımın başarısızlığı en ucuzudur.
  • Notification adımını düşür. E-posta gitmediği için uyarı da yok: adım sırası bir tasarım kararıdır.
  • Koordinasyon stilini değiştir. Adımlar aynı, akışı bilen değişiyor.

Simülatörün altındaki Görevler listesi bunları hedefe çeviriyor. Sonuncusu senaryonun dışına çıkıyor, çünkü gerçek hatalar önceden seçtiğin adımda gelmez.

Kafam karıştı, daha basit anlat

Bir adım başarısız olunca, o ana kadar tamamlanmış adımlar ters sırayla iptal edilir. En son yapılan iş ilk geri alınır.

Hızlı kontrolOrta

Telafi işlemi (compensating transaction) nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Orchestration mı, choreography mi?

OrchestrationChoreography
Akış nerede tanımlıTek bir koordinatördeHiçbir yerde — olay zincirinden türer
Yeni adım eklemekKoordinatörü değiştirYeni servis, yeni olay aboneliği
Hata ayıklamaKoordinatörün log’unu okuDağıtık trace şart
BağlılıkKoordinatör herkesi tanırServisler yalnızca olayları tanır
RiskKoordinatör tek hata noktasıDöngüsel olay zincirleri, görünmez akış

Pratik kural: adım sayısı üçü geçiyorsa orchestration. Choreography az sayıda adımda zarif, çok sayıda adımda kimsenin tamamını anlayamadığı bir olay çorbasına dönüşür.

Kafam karıştı, daha basit anlat

Orchestration, adımları yöneten bir orkestra şefidir. Choreography’de ise şef yoktur, herkes bir öncekinin bitirdiğini duyunca kendi işine başlar.

Hızlı kontrolİleri

Orkestrasyon (orchestration) ile koreografi (choreography) arasındaki temel fark nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Saga’nın vermediği şey: yalıtım

ACID’in dört harfinden saga yalnızca birini gerçekten kaybettirir ama o bir tanesi çok şey ifade eder.

Atomicity
Bölünmezlik: ya hepsi olur ya hiçbiri. Saga bunu telafi adımlarıyla geri kazanır.
Consistency
Tutarlılık: kurallar korunur, sonunda.
Isolation
Yalıtım: yarım kalmış işi başkası görmez.
Saga'da kaybolan harf bu.
Durability
Kalıcılık: her servis kendi verisini kalıcı yazar.

Sipariş oluşturuldu, ödeme çekildi, stok rezervasyonu henüz yapılmadı. Bu anda başka bir istek gelip siparişi okursa yarım kalmış bir durum görür. Veritabanı seviyesinde bunu engelleyen bir kilit yok.

Kullanılan çareler:

  • Semantik kilit. Siparişe PENDING koy; okuyanlar geçici olduğunu bilsin.
  • Değişmez alan (commutative update). set balance = 100 yerine balance = balance - 20.
  • Kötümser görünüm. En kötü durumu göster: “işleminiz onaylanıyor”.

Adım sırası bir tasarım kararıdır. Adımları rastgele sıralamazsın. İki kural:

  1. Geri alınabilir adımlar önce, geri alınamazlar sonra. Simülatör bildirimi kasten kargodan önce koyuyor; doğrusu onu en sona taşımaktı.
  2. En çok başarısız olan adım başta. Ödeme reddini dördüncü sıraya koyarsan her reddedilen kartta üç telafi çalışır.
Hızlı kontrolİleri

Her adımı, telafi edilebilir olup olmadığına göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Telafi edilebilir

Etkisi iş mantığıyla geri alınabilir

    Telafi edilemez

    Dışarı çıkan ve geri alınamayan etki

      Telafi işleminin kendisi başarısız olursa ne yaparsın?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Tuzaklar: kaçınılmaz iki ayrıntı

      Idempotency: telafi çağrısı da ağ üzerinden gider ve başarısız olabilir, dolayısıyla tekrar denenir. “Ödemeyi iade et” iki kez çalışırsa iki kez iade yapmamalı. Her adım ve her telafi bir idempotency anahtarı taşır.

      PaymentService.java
      @Transactional
      public void refund(RefundCommand command) {
      if (refundRepo.existsByIdempotencyKey(command.key())) {
      return; // bu iade zaten işlendi, sessizce çık
      }
      var refund = paymentGateway.refund(command.paymentId(), command.amount());
      refundRepo.save(new Refund(command.key(), refund.id()));
      }

      Outbox: commit edip sonra olay yayınlamak iki ayrı işlemdir; arada düşersen saga asılı kalır. Olayı aynı transaction’da bir outbox tablosuna yaz, ayrı bir süreç broker’a bassın.

      Aşağıdaki örnek bir bankadan: vadesiz hesaptan kredi kartı borcu ödemek iki servise ve iki veritabanına yayılır. Orkestrasyonlu saga ile durum veritabanında, her adımın bir telafisi var, cevaplar mesajla geliyor ve cevap gelmeyen adımlar zaman aşımıyla telafi ediliyor.

      Derinleş · Kredi kartı borcu ödeme sagası: orkestrasyon, telafi ve zaman aşımı 5 dosya · ~157 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/main/java/com/bank/saga/ CardPaymentSaga.java Saga'nın durumu kalıcı: hangi adımda, ne zaman başladı. Pod ölse de kaldığı yerden devam eder.

      src/main/java/com/bank/saga/CardPaymentSaga.java
      @Entity
      public class CardPaymentSaga {
      // Pay a credit card bill from a current account: two services, two databases.
      public enum Step { HOLDING_FUNDS, PAYING_CARD, CONFIRMED, COMPENSATING, CANCELLED }
      @Id
      private UUID id;
      private long paymentId;
      @Enumerated(EnumType.STRING)
      private Step step;
      private Instant stepStartedAt;
      private String failureReason;
      @Version
      private long version; // two replies racing for the same saga: one wins, one retries
      public static CardPaymentSaga start(long paymentId, Clock clock) {
      var saga = new CardPaymentSaga();
      saga.id = UUID.randomUUID();
      saga.paymentId = paymentId;
      saga.moveTo(Step.HOLDING_FUNDS, clock);
      return saga;
      }
      public void moveTo(Step next, Clock clock) {
      this.step = next;
      this.stepStartedAt = Instant.now(clock);
      }
      public void fail(String reason, Clock clock) {
      this.failureReason = reason;
      moveTo(Step.COMPENSATING, clock);
      }
      // getters omitted
      }

      src/main/java/com/bank/saga/ CardPaymentOrchestrator.java Orkestratör: her cevaba göre sonraki adımı ya da telafiyi seçer. Geçişler tek yerde.

      src/main/java/com/bank/saga/CardPaymentOrchestrator.java
      @Service
      class CardPaymentOrchestrator {
      private final SagaRepository sagas;
      private final SagaCommands commands;
      private final Clock clock;
      CardPaymentOrchestrator(SagaRepository sagas, SagaCommands commands, Clock clock) {
      this.sagas = sagas;
      this.commands = commands;
      this.clock = clock;
      }
      @Transactional
      public UUID begin(long paymentId) {
      CardPaymentSaga saga = sagas.save(CardPaymentSaga.start(paymentId, clock));
      commands.holdFunds(saga.getId(), paymentId); // via outbox, same transaction
      return saga.getId();
      }
      @Transactional
      public void fundsHeld(UUID sagaId) {
      CardPaymentSaga saga = sagas.findById(sagaId).orElseThrow();
      if (saga.getStep() != CardPaymentSaga.Step.HOLDING_FUNDS) return; // duplicate or late reply
      saga.moveTo(CardPaymentSaga.Step.PAYING_CARD, clock);
      commands.payCard(sagaId, saga.getPaymentId());
      }
      @Transactional
      public void cardPaid(UUID sagaId) {
      CardPaymentSaga saga = sagas.findById(sagaId).orElseThrow();
      if (saga.getStep() != CardPaymentSaga.Step.PAYING_CARD) return;
      saga.moveTo(CardPaymentSaga.Step.CONFIRMED, clock);
      commands.confirmPayment(saga.getPaymentId()); // the hold becomes a real debit
      }
      @Transactional
      public void stepFailed(UUID sagaId, String reason) {
      CardPaymentSaga saga = sagas.findById(sagaId).orElseThrow();
      CardPaymentSaga.Step failedAt = saga.getStep();
      if (failedAt == CardPaymentSaga.Step.CONFIRMED || failedAt == CardPaymentSaga.Step.CANCELLED) return;
      saga.fail(reason, clock);
      // Undo only what already happened, newest first.
      if (failedAt == CardPaymentSaga.Step.PAYING_CARD) commands.releaseHold(sagaId, saga.getPaymentId());
      commands.cancelPayment(saga.getPaymentId(), reason);
      saga.moveTo(CardPaymentSaga.Step.CANCELLED, clock);
      }
      }

      src/main/java/com/bank/saga/ SagaReplies.java Diğer servislerin cevapları mesajla gelir; aynı cevap iki kez gelirse durum kontrolü onu yok sayar.

      src/main/java/com/bank/saga/SagaReplies.java
      @Component
      class SagaReplies {
      private final CardPaymentOrchestrator orchestrator;
      SagaReplies(CardPaymentOrchestrator orchestrator) {
      this.orchestrator = orchestrator;
      }
      @KafkaListener(topics = "saga-replies", groupId = "card-payment-saga")
      public void on(SagaReply reply) {
      switch (reply) {
      case SagaReply.FundsHeld r -> orchestrator.fundsHeld(r.sagaId());
      case SagaReply.CardPaid r -> orchestrator.cardPaid(r.sagaId());
      case SagaReply.StepFailed r -> orchestrator.stepFailed(r.sagaId(), r.reason());
      }
      }
      }
      sealed interface SagaReply {
      UUID sagaId();
      record FundsHeld(UUID sagaId) implements SagaReply {}
      record CardPaid(UUID sagaId) implements SagaReply {}
      record StepFailed(UUID sagaId, String reason) implements SagaReply {}
      }

      src/main/java/com/bank/saga/ SagaCommands.java Telafiler: geri alma değil, yeni ve ters bir işlem (hesaptaki blokeyi kaldırmak, ödemeyi iptal etmek).

      src/main/java/com/bank/saga/SagaCommands.java
      @Component
      class SagaCommands {
      private final OutboxRepository outbox;
      SagaCommands(OutboxRepository outbox) {
      this.outbox = outbox;
      }
      // Commands go through the outbox: saga state and outgoing command commit together.
      void holdFunds(UUID sagaId, long paymentId) { send("accounts", "HoldFunds", sagaId, paymentId); }
      void payCard(UUID sagaId, long paymentId) { send("cards", "PayCardBalance", sagaId, paymentId); }
      void confirmPayment(long paymentId) { send("accounts", "CaptureHold", null, paymentId); }
      // Compensations are new forward actions, not rollbacks.
      void releaseHold(UUID sagaId, long paymentId) { send("accounts", "ReleaseHold", sagaId, paymentId); }
      void cancelPayment(long paymentId, String reason) { send("payments", "CancelPayment", null, paymentId); }
      private void send(String target, String type, UUID sagaId, long paymentId) {
      outbox.save(OutboxEvent.command(target, type, sagaId, paymentId));
      }
      }

      src/main/java/com/bank/saga/ SagaTimeouts.java Cevap gelmeyen adım sonsuza kadar beklemez: zaman aşımı telafiyi başlatır.

      src/main/java/com/bank/saga/SagaTimeouts.java
      @Component
      class SagaTimeouts {
      private final SagaRepository sagas;
      private final CardPaymentOrchestrator orchestrator;
      private final Clock clock;
      SagaTimeouts(SagaRepository sagas, CardPaymentOrchestrator orchestrator, Clock clock) {
      this.sagas = sagas;
      this.orchestrator = orchestrator;
      this.clock = clock;
      }
      // A reply that never comes must not leave the customer's money on hold forever.
      @Scheduled(fixedDelay = 30_000)
      @SchedulerLock(name = "sagaTimeouts", lockAtMostFor = "PT5M")
      public void failStuckSagas() {
      Instant cutoff = Instant.now(clock).minus(Duration.ofMinutes(10));
      for (UUID id : sagas.findStuckSince(cutoff, List.of(CardPaymentSaga.Step.HOLDING_FUNDS, CardPaymentSaga.Step.PAYING_CARD))) {
      orchestrator.stepFailed(id, "timeout");
      }
      }
      }

      Kendini sına

      Şimşek turu1/5

      Servisler ayrılınca tek bir veritabanı transaction'ı artık hepsini kapsayamaz.

      Soru 1/4İleri

      Aşağıdaki ifadeleri doğru kutuya yerleştir.

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Sınıflandırılmamış

      ACID rollback

      Tek veritabanı, tek transaction

        Compensating transaction

        Saga içinde geri alma

          Aklında kalacak üç şey

          1. 1 Telafi işlemi geri almak değildir: geri almak izi siler, telafi yeni bir kayıt yazar. Ödeme iadesi hesap dökümünde ayrı bir satırdır.
          2. 2 İki stil var: orchestration'da akış tek yerden yönetilir ama o yönetici tek hata noktasıdır; choreography'de kimse akışın tamamını bilmez, hata ayıklamak zorlaşır.
          3. 3 Saga yalıtım vermez. İş yarıdayken başka biri yarım kalmış durumu görebilir; bunu iş tarafının kabul etmesi gerekir.
          Sonraki kapı Bir istek dört servise uğruyorsa, hepsinin aynı anda çalışıyor olma ihtimali ne olur? Servisler Arası İletişim Desenleri · 10 dk

          5 kart sonraki derste seni bekliyor

          0/5 kart bu dersten toplandı

          Bu dersin üstüne kurulanlar

          Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.