Harika. Outbox Pattern ile mesajın "yola çıkmasını" garantiye aldık. Şimdi madalyonun diğer yüzüne, yani Consumer (Alıcı) tarafına geçiyoruz.
Mesaj karşı tarafa ulaştı ama alıcı servis o an veritabanına bağlanamıyor ya da mesajı işlerken beklenmedik bir hata alıyor. Bu durumda mesajın kaybolmaması için uygulanan "Hata Yönetimi ve Yeniden Deneme (Retry) Stratejileri" konusuna giriyoruz.
Yeni Konu: Consumer-Side Resilience (Hata Yönetimi)
Mesaj işlenirken bir hata oluştuğunda izlenecek üç ana strateji vardır:
1. Yerel Yeniden Deneme (Local Retry)
Hata geçici bir durumsa (örneğin ağda milisaniyelik bir dalgalanma), Consumer mesajı Kafka'dan "onaylamadan" (ACK vermeden) hemen tekrar dener.
- Risk: Eğer hata kalıcıysa (kod hatası veya yanlış veri), Consumer bu mesajda takılı kalır ve arkadan gelen diğer binlerce mesajın işlenmesini engeller (Head-of-line blocking).
2. Üstten Yeniden Deneme (Non-blocking Retry)
Mesajı ana kuyruktan çıkarıp özel bir "Retry Topic"e göndeririz. Böylece ana kuyruk akmaya devam eder, sorunlu mesaj kenarda bekleyip belirli aralıklarla tekrar denenir.
3. Dead Letter Queue (DLQ - Ölü Mektup Kuyruğu)
Tüm denemelere rağmen işlenemeyen mesajların atıldığı "çöplük" değil, aslında bir "hastane"dir.
Dead Letter Queue (DLQ) Detaylı İnceleme
DLQ, sistemi tıkayan "zehirli mesajları" (Poison Pills) ana akıştan izole etmek için kullanılır.
İşleyiş Şeması (Java / Spring Kafka):
- Main Topic: Mesaj buraya gelir.
- Hata: Consumer mesajı işleyemez.
- Retry Topics: Spring Kafka (2.7+ sürümüyle) otomatik olarak
topic-retry-0,topic-retry-1gibi ara kuyruklar oluşturabilir. Her seviyede bekleme süresi artar (Exponential Backoff). - DLQ: Tüm denemeler bitince mesaj
topic-dlt(Dead Letter Topic) kuyruğuna düşer.
Java (Spring Boot) ile Uygulama
Spring Kafka, bu karmaşık yapıyı @RetryableTopic anotasyonu ile çok basitleştirir.
@Service
@Slf4j
public class OrderConsumer {
@RetryableTopic(
attempts = "3", // Toplam 3 deneme yap
backoff = @Backoff(delay = 2000, multiplier = 2.0), // 2s, 4s, 8s bekle
topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE,
dltStrategy = DltStrategy.FAIL_ON_ERROR,
include = {TemporaryException.class} // Sadece geçici hatalarda tekrar dene
)
@KafkaListener(topics = "orders-topic", groupId = "inventory-group")
public void consume(String message) {
log.info("Mesaj alınıyor: {}", message);
// İş mantığı sırasında hata oluşursa @RetryableTopic devreye girer
if (inventoryService.isDown()) {
throw new TemporaryException("Stok servisi şu an kapalı!");
}
inventoryService.updateStock(message);
}
@DltHandler
public void handleDlt(String message, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
log.error("Mesaj tüm denemelere rağmen başarısız oldu ve DLT'ye düştü: {} Topic: {}", message, topic);
// Burada veritabanına "Manuel inceleme gerekiyor" diye kayıt atılabilir
}
}
Neden Önemli? (Fintech Bakış Açısı)
Fintech veya para girişi olan işlerde (senin de belirttiğin gibi), bir mesajın işlenememesi demek paranın havada kalması demektir.
- Görünürlük: DLQ sayesinde hangi işlemlerin başarısız olduğunu anlık görebilirsin.
- Tıkanmama: Tek bir hatalı mesaj yüzünden tüm sistemin durmasını engellersin.
- Manuel Müdahale: DLQ'daki mesajları düzelttikten sonra tekrar ana kuyruğa basabilirsin (Reprocessing).
Bu Konunun Kritik Sorusu:
DLQ'ya düşen bir mesajı "tekrar işlemek" (Re-drive) için nasıl bir yöntem izlenmeli? Yoksa bu mesajlar sadece log olarak mı kalmalı?