Outbox ve Idempotent Consumer — Olay Kaybolmasın, İki Kez de İşlenmesin
Önce şunu oku: Servisler Arası İletişim Desenleri
30 saniyede özet
Veritabanına ve mesaj kuyruğuna aynı anda yazamazsın: biri başarılı olup öbürü olmayabilir. Çözüm, mesajı siparişle birlikte veritabanındaki bir 'giden kutusuna' yazmak. Mesaj yine de iki kez gidebilir; alan taraf tekrarı tanımalı.
Muhasebe iki şey buldu: ödemesi hiç alınmamış siparişler ve hiç var olmamış siparişler için alınmış ödemeler. İkisinin de sebebi aynı iki satır: orders.save() ve kafka.send().
-
Bayt: Muhasebe ödemesi hiç alınmamış siparişler buldu!
-
Sen: Başka bir şey var mı?
-
Bayt: Bir de hiç var olmamış siparişler için alınmış ödemeler. İkisi de aynı iki satırdan!
-
Bayt: Deftere yazıp aynı anda kasaya haber vermek. Arada bir şey ters giderse ne olur?
İki yere aynı anda yazılmaz
Sipariş commit edildi. kafka.send() çağrılmadan hemen önce pod öldü. Yeniden başlayınca ne olur? Cevabı göster
Olay sonsuza kadar kaybolur. Sipariş veritabanında, ama hangi olayın gönderilmediğini gösteren hiçbir kayıt yok. Ödeme servisi bu siparişi hiç duymaz.
Adım adım oku
- Sipariş ve gönderilecek mesaj aynı transaction içinde yazılır: biri orders tablosuna, öteki outbox tablosuna.
- Ayrı bir kurye süreci outbox'taki mesajları okuyup Kafka'ya gönderir.
- Kurye çökerse mesaj kaybolmaz, kutuda bekler ve kurye dönünce yeniden gönderilir; bu yüzden aynı mesaj iki kez gidebilir.
- Alıcı işlediği mesajların kimliğini hatırlar; tekrar gelen mesajı fark eder ve ikinci kez işlemez.
Aynı işlemde iki ayrı sisteme yazmaya dual writeAynı işlemde iki ayrı sisteme (ör. veritabanı ve mesaj kuyruğu) yazmak. Ortak bir transaction olmadığı için biri başarılı olup diğeri başarısız olabilir.Sözlükte gör → denir. Veritabanı transaction’ı Kafka’ya gönderilen mesajı geri alamaz, Kafka da veritabanının commit edip etmediğini bilmez.
Sırayı çevirmek de çözmez: önce gönderip sonra commit edemezsen, olmayan bir sipariş için olay gitmiş olur.
Kafam karıştı, daha basit anlat
İki farklı yere aynı anda yazmaya çalışırsan, biri başarılı olup öteki başarısız olabilir. O zaman iki yer birbirinden farklı şey söyler.
Bir metot önce siparişi veritabanına kaydedip sonra `kafka.send()` çağırıyor. Temel sorun ne?
Nadiren de olsa, veritabanında olmayan siparişler için ödeme alınıyor. Hangi satırlar sorunun kaynağı?
Giden kutusu: mesajı siparişle birlikte yaz
outboxOlayı, iş verisiyle aynı yerel transaction içinde bir tabloya yazıp ayrı bir süreçle kuyruğa taşıma deseni. İki kaynağa yazma (dual write) problemini çözer.Sözlükte gör → kalıbı iki yazmayı tek sisteme indirir. Olay, siparişle aynı veritabanı transaction’ında bir tabloya yazılır; ya ikisi de var ya hiçbiri.
Ayrı bir relay commit edilmiş satırları okuyup yayınlar: periyodik bir sorguyla ya da CDC ile (Debezium). Relay yayınlayıp “gönderildi” işaretlemeden çökerse, satırı bir kez daha yayınlar.
Yani teslimat at-least-onceMesajın en az bir kez teslim edileceği garantisi; iki kez de gelebilir. Önce işle sonra commit et deseninin sonucudur ve tüketicinin idempotent olmasını gerektirir.Sözlükte gör →’tır: outbox olayın kaybolmamasını sağlar, tekrar gelmemesini değil.
Kafam karıştı, daha basit anlat
Mektubu doğrudan postaya verme, önce aynı defterdeki “giden kutusu” sayfasına yaz. Sipariş ve mektup birlikte yazılır ya da hiçbiri yazılmaz. Postacı mektubu sonra alıp götürür.
Transactional outbox kalıbı dual write sorununu nasıl çözer?
Outbox kurduktan sonra ödeme servisinin idempotent olması neden hâlâ gerekir?
Kendin gör
Outbox ve idempotent consumer — sipariş ve ödeme tutarlı mı?
Tohum 127827- Oynat ya da adımla.
Şu an ne oldu?
POST /orders
Sipariş kaydedilecek ve ödeme servisine OrderPlaced olayı gidecek. İki ayrı sistem: veritabanı ve Kafka.
Görevler0/4
Ödemesi hiç alınmayan bir sipariş üretaçık
İpucu
Sipariş kaydedildi ama olay hiç yola çıkmadı.
Olmayan bir sipariş için para çekaçık
İpucu
Olay commit'ten önce gönderilirse ve commit başarısız olursa?
Aynı sipariş için iki kez para çekaçık
İpucu
At-least-once teslimat ve her mesajı işleyen bir tüketici.
Bir arızaya rağmen sipariş ve ödeme tutarlı kalsınaçık
İpucu
Olay siparişle aynı transaction'da, tüketici tekrarı fark ediyor.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
- Arızayı “commit sonrası süreç ölüyor” yap. Sipariş var, ödeme yok.
- “Gönderim sonrası commit başarısız” seç. Olmayan sipariş için ödeme.
- Outbox’a geç. İki arıza da tutarlı sonuçlanıyor.
- “Consumer offset’i commit etmeden ölüyor” seç. Outbox’la bile iki çekim.
- Ödeme servisini idempotent yap. Tek çekim.
Her arıza senaryosu, dual write ile hangi sonucu doğurur?
Satır satır: tekrarı tanıyan alıcı
Aynı olay ikinci kez gelirse
@KafkaListener(topics = "orders")@Transactionalpublic void on(OrderPlaced event) { try { processed.insert(event.eventId()); // UNIQUE(event_id) } catch (DuplicateKeyException alreadyDone) { return; } payments.charge(event.orderId(), event.amount());}Debug
ilk teslimat evt-42 geldi. Transaction açık.
- event_id
- = evt-42
Sol/sağ ok tuşlarıyla da gezebilirsin.
Bu bir idempotent consumerAynı mesajı birden fazla kez alsa da etkisini yalnızca bir kez uygulayan tüketici. Genelde işlenmiş olay kimliklerini benzersiz bir kısıtla kaydeder.Sözlükte gör →’dır. Tekrarı olay kimliğiyle tanı, offset’le değil: relay aynı olayı yeniden yayınlarsa yeni bir offset alır.
Idempotent bir tüketici tekrar gelen mesajı neyle tanımalı?
Tuzaklar
@Transactional içinde kafka.send(). Rollback olan sipariş için olay gider.
AFTER_COMMIT dinleyicisinde send. Hayalet olayı önler, ama commit sonrası çökmede olayı yine kaybeder.
Sırayı unutmak. Aynı siparişin olayları farklı partition’lara giderse iptal ödemeden önce işlenebilir. Partition anahtarı sipariş kimliği olmalı.
Exactly-once’a güvenmek. Kafka’nın garantisi Kafka’nın içini kapsar; tüketicinin veritabanı yazmasını değil.
`@TransactionalEventListener(phase = AFTER_COMMIT)` içinde `kafka.send()` yapmak dual write sorununu çözer mi?
Aşağıdaki örnek bir bankadan ve dual write sorununu outbox ile çözüyor: havale ve olayı aynı transaction’da yazılıyor, ayrı bir aktarıcı olayları Kafka’ya taşıyor, limit servisi de aynı olayı iki kez saymıyor.
Derinleş · Havale olayı: outbox ve idempotent tüketici 5 dosya · ~101 satır · ilk okumada atlayabilirsin
Kendini sına
Veritabanına ve mesaj kuyruğuna tek bir transaction içinde yazmak mümkündür.
Outbox relay'i olayları yayınlarken aynı siparişe ait OrderPlaced ve OrderCancelled olaylarının sırası bozuluyor. En olası neden ve çözüm?
Aklında kalacak üç şey
- 1 Veritabanına kaydedip ardından mesaj göndermek iki ayrı yazmadır. Arada bir çökme ya mesajı kaybettirir ya da olmayan bir sipariş için mesaj gönderir.
- 2 Outbox, mesajı siparişle aynı transaction'da yazar; ayrı bir süreç onu sonra yayınlar. Mesaj kaybolmaz, ama iki kez gidebilir.
- 3 Alan taraf mesaj kimliğini, kendi işiyle aynı transaction'da benzersiz bir kısıtla kaydederek tekrarı tanır.
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.
- Mikroservisler & Design PatternsCQRS ve Event Sourcing — Olanı Yaz, Okumayı AyırBir hesabın bakiyesini hiçbir yere yazmadan, yalnızca olup biteni kaydederek tutabilir misin?Derse git
- Mikroservisler & Design PatternsWebhook Almak — Ödeme Tamamlandı Diyen Gerçekten Sağlayıcı mı?Cüzdana 550 TL eklenmeliydi. 7050 TL eklendi. Çağrıların hepsi aynı adrese geldi; hangileri gerçekti?Derse git
- Spring Boot EkosistemiSpring Kafka Hata Yönetimi — Bozuk Mesaj Partition'ı Durdurur mu?İşlenemeyen bir Kafka mesajı birkaç denemeden sonra nereye gider?Derse git