Türk Bayrağı
Teknik Not
Software Architecture 3 dk okuma

Dağıtık Sistemlerde "Dual-Write" Problemi ve Transactional Outbox Pattern Çözümü

Mikroservis mimarilerinde ve asenkron haberleşen sistemlerde (Event-Driven Architecture) en sık karşılaştığımız tasarımsal zorluklardan biri dağıtık transaction yönetimidir. Monolitik mimarilerde ACID özellikleriyle (Ato...

Mikroservis mimarilerinde ve asenkron haberleşen sistemlerde (Event-Driven Architecture) en sık karşılaştığımız tasarımsal zorluklardan biri dağıtık transaction yönetimidir. Monolitik mimarilerde ACID özellikleriyle (Atomicity, Consistency, Isolation, Durability) kolayca çözdüğümüz veri tutarlılığı, işin içine RabbitMQ, Apache Kafka gibi Message Broker'lar ve izole edilmiş servis veritabanları girdiğinde ciddi bir baş ağrısına dönüşür.

Bu yazıda, özellikle ERP entegrasyonları, ödeme sistemleri ve sipariş yönetim ağlarında sıkça karşımıza çıkan Dual-Write (Çift Yazma) problemine ve bu problemin endüstri standardı çözümü olan Transactional Outbox Pattern yaklaşımına derinlemesine bakacağız.

Dual-Write Problemi Nedir?

Bir sipariş oluşturulduğunu varsayalım. İşlem adımları tipik olarak şöyledir:

  1. Sipariş verilerini (Order) yerel ilişkisel veritabanına (PostgreSQL, MS SQL vb.) kaydet.

  2. Diğer servisleri (Stok, Fatura, Bildirim) haberdar etmek için Message Broker'a (örneğin RabbitMQ) bir OrderCreated event'i fırlat.

Buradaki kritik hata noktası (Point of Failure) şudur: Veritabanına veri yazıldıktan sonra, Message Broker'a event fırlatılırken network koptuğunda veya Broker geçici olarak down olduğunda ne olacak? Sipariş veritabanında oluştu, ancak diğer servislerin bundan haberi yok. Sistemde veri tutarsızlığı (Data Inconsistency) meydana geldi.

Eğer önce Broker'a yazıp sonra veritabanına kaydetmeyi denerseniz ve veritabanı yazma işlemi başarısız olursa, bu sefer de var olmayan bir siparişin event'ini diğer servislere işletmiş (Phantom Event) olursunuz.

İki Aşamalı İşleme (Two-Phase Commit - 2PC) kullanmak bir seçenek gibi görünse de, modern dağıtık sistemlerde ve yüksek hacimli (high-throughput) yapılarda yarattığı locking (kilitleme) ve performans darboğazları nedeniyle tercih edilmez (CAP Teoremi).

Çözüm: Transactional Outbox Pattern

Transactional Outbox Pattern, "Aynı anda hem veritabanına yazıp hem de Kafka/RabbitMQ'ya nasıl güvenli event fırlatırım?" sorusunun en zarif yanıtıdır.

Sistem şu şekilde işler:

  1. Outbox Tablosu: İlgili mikroservisin veritabanında, asıl iş tablolarına (örneğin Orders) ek olarak bir de Outbox (veya Event_Logs) tablosu oluşturulur.

  2. Local Transaction: Yeni bir sipariş geldiğinde, sistem Orders tablosuna siparişi eklerken, fırlatılması gereken OrderCreated event datasını JSON payload olarak aynı SQL transaction blokajı içinde Outbox tablosuna INSERT eder.

  3. Atomicity Sağlanır: Veritabanı seviyesindeki transaction sayesinde, sipariş ya Orders ve Outbox tablosuna aynı anda başarıyla yazılır ya da tamamen Rollback olur. Dual-Write problemi RDBMS seviyesinde çözülmüş olur.

Örnek Bir SQL Akışı

BEGIN TRANSACTION;

-- Ana entity kaydı
INSERT INTO Orders (Id, CustomerId, TotalAmount, Status) 
VALUES ('ord-123', 'cust-55', 1500.00, 'Created');

-- Event payload'unun Outbox'a kaydı (Aynı Transaction içinde)
INSERT INTO Outbox (Id, EventType, AggregateId, Payload, CreatedAt, IsProcessed)
VALUES (NEWID(), 'OrderCreated', 'ord-123', '{"Id":"ord-123", "Amount":1500.00}', GETDATE(), 0);

COMMIT TRANSACTION;

Outbox Tablosunu Okumak ve Event'leri Fırlatmak

Veriyi Outbox tablosuna güvenle yazdık. Peki bu tabloyu dinleyip Message Broker'a kim iletecek? Bunun için iki temel mimari yaklaşım bulunur:

1. Polling Publisher (Worker Service): Arka planda çalışan bir Cron Job veya Worker Service, periyodik olarak (örneğin her 2 saniyede bir) Outbox tablosunu sorgular (SELECT * FROM Outbox WHERE IsProcessed = 0). Okuduğu kayıtları Kafka/RabbitMQ'ya publish eder ve başarılı olanların statüsünü IsProcessed = 1 olarak günceller.

  • Dezavantajı: Yoğun trafikli sistemlerde veritabanı üzerinde sürekli bir SELECT/UPDATE yükü (overhead) oluşturur.

2. Change Data Capture (CDC) - (Tavsiye Edilen): Veritabanının transaction loglarını (PostgreSQL için WAL, SQL Server için Transaction Log) okuyarak değişiklikleri yakalayan mimaridir. Debezium gibi araçlar kullanarak, Outbox tablosuna yapılan her INSERT işlemi, veritabanı motorunu hiç yormadan, doğrudan log dosyası üzerinden okunup Kafka'ya anında stream edilir. Yüksek ölçeklenebilirlik gerektiren mimarilerde endüstri standardı CDC kullanmaktır.

Mimari Uyarılar (Caveats & Best Practices)

  • Idempotency (Tekrarlanabilirlik): Outbox pattern kullanıldığında "At-Least-Once" (en az bir kere) teslimat garantisi sağlanır. Yani ağ gecikmeleri durumunda aynı event Broker'a iki kere gönderilebilir. Bu nedenle, Consumer (tüketici) servislerin mutlak suretle Idempotent olarak tasarlanması gerekir (Aynı event iki kez gelse bile sistemin durumunun bozulmaması).

  • Outbox Tablosunun Temizliği: Outbox tablosu zamanla aşırı şişebilir. İşlenmiş kayıtları (Processed) belirli aralıklarla (Nightly Jobs) arşiv tablosuna taşımak veya silmek, disk yönetimi ve sorgu performansı açısından kritiktir.

Sonuç: Büyük ölçekli ticari sistemlerde, middleware entegrasyonlarında ve mikroservis mimarilerinde veri tutarlılığını şansa bırakamayız. Transactional Outbox Pattern, veritabanının sunduğu klasik ACID garantilerini alıp, modern asenkron event-driven dünyaya güvenli bir köprü kurar. Kurumsal çapta bir mimari inşa ediyorsanız, bu pattern alet çantanızın demirbaşlarından biri olmalıdır.