Ana sayfa → Teknik
Dual Write Problem
Emir PostgreSQL’e yazıldı. Ardından Redis stream’e olay gönderilecekti ama tam o anda bağlantı koptu. Şimdi veritabanında var olan, streaming-engine’in ise hiç haberi olmayan bir emir var. Müşteri ekranında göremiyor, destek ekibi “sistemde görünüyor” diyor.
- İki ayrı sisteme atomik yazamazsın. Ne kadar dikkatli kod yazarsan yaz.
- Çözüm sırayı değiştirmek değil, ikinci yazmayı aynı veritabanına almak. Outbox tam olarak bu.
- Gelen tarafta da aynı sorun var ve cevabı inbox: önce yaz, sonra işle.
- CDC güçlü ama sözleşmen tablo şeman olur. En sağlamı outbox + CDC birleşimi.
Neden try/catch bunu çözmüyor?
İlk refleks genelde şu:
db.insert(emir); // 1. yazma
try {
redis.xadd("emirler", olay); // 2. yazma
} catch (Exception e) {
log.error("olay gonderilemedi", e);
// ve simdi ne olacak?
}
Buradaki sorun hatayı yakalamamak değil; yakaladıktan sonra yapabileceğin bir şey olmaması. Seçeneklerine bak:
- Emri geri sil? Belki köprü onu çoktan işledi. Sildiğinde daha büyük bir tutarsızlık yaratırsın.
- Tekrar dene? Süreç bu satırda çökerse tekrar deneme de ölür.
- Loga yaz ve geç? Yaptığın şey, tutarsızlığı görünmez kılmak oluyor.
Sırayı ters çevirmek de kurtarmıyor: önce olayı yayınlayıp sonra veritabanına yazarsan, bu sefer olmayan bir emir için olay yayınlamış oluyorsun ve streaming-engine hayalet bir emir gösteriyor.
| Sıra | Ne bozulur | Belirti |
|---|---|---|
| Önce DB, sonra olay | Olay kaybolur | Emir var, kimsenin haberi yok. Sessiz. |
| Önce olay, sonra DB | Emir kaybolur | Hayalet emir, tüketiciler var olmayan kayda bakıyor |
| İkisi de “başarılı” | Sıra bozulur | Olay, veritabanı işlemi kesinleşmeden tüketiciye ulaşır |
Üçüncü satır en az bilineni ve gerçekten başımıza geldi: olay yayınlandı, tüketici
hemen okudu ve o kaydı almak için veritabanına gitti — ama transaction
henüz kesinleşmemişti. Tüketici “kayıt yok” hatası aldı. Olay
COMMIT’ten önce yayınlanmıştı.
COMMIT’i yoksa, ikisine birden atomik
yazamazsın. Çözüm ikinci yazmayı aynı sisteme almaktan geçiyor.
Transactional outbox: event’i de veritabanına yaz
Fikir sürpriz derecede basit: olayı mesaj sistemine göndermek yerine, aynı veritabanında bir tabloya aynı transaction içinde yaz.
CREATE TABLE outbox (
id bigserial PRIMARY KEY, -- sirayi bu belirliyor
olay_tipi text NOT NULL, -- 'EmirOlusturuldu'
toplam_id text NOT NULL, -- order_id — ayni emrin olaylari
govde jsonb NOT NULL,
olusma timestamptz NOT NULL DEFAULT now(),
yayinlandi boolean NOT NULL DEFAULT false
);
CREATE INDEX ON outbox (yayinlandi, id) WHERE NOT yayinlandi;
-- Uygulamada:
BEGIN;
INSERT INTO emirler (id, sembol, lot, ...) VALUES (991, 'EURUSD', 1.0, ...);
INSERT INTO outbox (olay_tipi, toplam_id, govde)
VALUES ('EmirOlusturuldu', '991', '{"orderId":991,"symbol":"EURUSD",...}');
COMMIT; -- ← ya ikisi birden, ya hicbiri
Artık “emir var ama olay yok” durumu fiziksel olarak imkânsız. Ya ikisi de tabloda, ya hiçbiri.
Relay: outbox’u okuyup publish eden
-- Ayri bir surec, saniyede birkac kez:
BEGIN;
SELECT * FROM outbox
WHERE NOT yayinlandi
ORDER BY id -- ← sira garantisi burada
LIMIT 200
FOR UPDATE SKIP LOCKED; -- ← birden fazla relay calisabilsin
-- her biri icin: redis.xadd(...) / kafka.send(...)
UPDATE outbox SET yayinlandi = true WHERE id = ANY(:idler);
COMMIT;
Üç detay ve üçü de sonradan öğrenildi:
- Yayınlama, işaretlemeden önce olur. Yani yayınladıktan sonra çökersen olay iki kez gider. Bu kabul edilebilir — çünkü at-least-once zaten öyle çalışıyor ve tüketici tarafında dedup var (detay). Tersini yaparsan — önce işaretle, sonra yayınla — olayı kaybedersin, ki bu çok daha kötü.
-
Sıra
idüzerinden korunur. Aynı emrin olaylarıORDER BY idile sırayla gider. Birden fazla relay kopyası çalıştırıyorsan bu garanti zayıflar; sıra kritiksetoplam_id’ye göre bölümle (sharding). -
Temizlik şart. Yayınlanmış satırlar birikirse tablo devleşir ve
SELECT’ler yavaşlar. Günlük bir iş: 3 günden eski ve yayınlanmış satırları sil. Kısmi indeks (yukarıdakiWHERE NOT yayinlandi) sorguyu tablo boyutundan bağımsız tutuyor.
Yoklamayla (polling) çalışan bir relay, olayı en fazla yoklama aralığı kadar geciktirir. 100 ms’de bir yokluyorsan ortalama 50 ms gecikme eklemiş oluyorsun. Fiyat akışı için bu çok; emir olayları için genelde sorun değil.
Gecikmeyi sıfıra yaklaştırmak istiyorsan yoklama yerine
LISTEN/NOTIFY kullanabilirsin ya da bir sonraki bölümdeki CDC’ye
geçersin.
Inbox pattern: aynı sorunun consumer tarafındaki hali
Şimdi ters yön. Köprüden bir fill bildirimi geldi. Yapman gerekenler: kaydet, pozisyonu güncelle, müşteriye bildir, mutabakata yaz. Ortasında çökersen ne olacak?
Cevap yine aynı fikir: önce yaz, sonra işle.
CREATE TABLE inbox (
olay_id text PRIMARY KEY, -- ← dedup burada
govde jsonb NOT NULL,
alinma timestamptz NOT NULL DEFAULT now(),
islendi boolean NOT NULL DEFAULT false
);
-- 1. Adim: sadece kaydet ve ACK don. Hizli.
INSERT INTO inbox (olay_id, govde) VALUES (:fill_id, :govde)
ON CONFLICT (olay_id) DO NOTHING; -- ikinci kez geldiyse yok say
-- 2. Adim: ayri bir surec islesin
BEGIN;
SELECT * FROM inbox WHERE NOT islendi ORDER BY alinma
LIMIT 100 FOR UPDATE SKIP LOCKED;
-- ... asil isi yap ...
UPDATE inbox SET islendi = true WHERE olay_id = ANY(:idler);
COMMIT;
Üç problemi birden çözüyor:
- Tekrar: birincil anahtar sayesinde aynı olay iki kez girmiyor.
- Çökme: işlerken çökersen olay tabloda duruyor, yeniden işleniyor.
- Hız: göndericiye anında onay dönüyorsun; ağır iş arka planda. Köprü zaman aşımına (timeout) düşmüyor.
Change Data Capture: uygulamaya hiç dokunmadan
Üçüncü yol, veritabanının değişiklik günlüğünü (PostgreSQL’de WAL, MySQL’de binlog) okuyup oradan olay üretmek. Debezium bunu yapıyor.
Cazip tarafı açık: uygulama kodunda tek satır değişiklik yok. Pozisyon tablosuna yazılan her satır, otomatik olarak bir olaya dönüşüyor ve risk motoruna akıyor.
CDC’nin ürettiği olay, tablonun kendisi. Yani
pozisyonlar tablosuna yeni bir kolon eklediğinde ya da bir kolonu
yeniden adlandırdığında, o olayı dinleyen bütün servisler etkileniyor.
Sonuç: veritabanı şeman, farkında olmadan servisler arası bir sözleşmeye dönüşüyor. Ve o sözleşmeyi değiştirmek, bir migration yazmak kadar kolay göründüğü için insanlar farkında olmadan bozuyor.
Olayı outbox tablosuna yaz (sözleşmeyi sen belirle), o tabloyu CDC ile oku (yoklama gecikmesi olmasın). İki dünyanın da iyi yanını alıyorsun:
uygulama → BEGIN; emirler INSERT + outbox INSERT; COMMIT;
↓
Debezium (WAL okur)
↓
Kafka / Redis stream
↓
tuketiciler
Relay süreci yazmana da gerek kalmıyor; olay gecikmesi milisaniyelere iniyor ve olay şemasının kontrolü sende kalıyor. Debezium’un “outbox event router” diye hazır bir bileşeni de var.
Hangisini seçmeli?
| Outbox + yoklama | CDC (doğrudan tablo) | Outbox + CDC | |
|---|---|---|---|
| Kurulum | Kolay, ek altyapı yok | Orta (Debezium, Kafka Connect) | Orta |
| Kod değişikliği | Var | Yok | Var |
| Olay şeması | Sen belirlersin | Tablo yapısı | Sen belirlersin |
| Gecikme | Yoklama aralığı | Milisaniyeler | Milisaniyeler |
| Ne zaman | Başlangıç, tek servis | Eski sisteme dokunamıyorsan | Kalıcı çözüm |
Pratik tavsiye: outbox + yoklama ile başla. Kurulumu bir öğleden sonra, ek altyapı sıfır ve problemin %90’ını çözüyor. Gecikme gerçekten sorun olduğunda CDC eklersin — ve outbox tablosu zaten hazır olduğu için o geçiş sancısız oluyor.
Kontrol listesi AI agent görev listesi
- Kodda “DB’ye yaz, sonra mesaj gönder” deseni var mı?
- Olay, asıl kayıtla aynı transaction’da mı yazılıyor?
- Relay, yayınlamadan önce mi işaretliyor? (Yanlış — sonra işaretlemeli.)
- Outbox tablosunun temizlik işi var mı?
- Yayınlanmayı bekleyen en eski satırın yaşı izleniyor mu? (Relay ölürse tek sinyal bu.)
- Gelen olaylar önce inbox’a mı yazılıyor, yoksa doğrudan mı işleniyor?
- Inbox’ta olay kimliği birincil anahtar mı?
- CDC kullanıyorsan: tablo şeman servisler arası sözleşme haline mi geldi?
Sonuç
Çift yazma, mikroservise geçen ekiplerin neredeyse tamamının bir noktada çarptığı duvar. Ve çoğu ekip bunu bir hata sanıp daha dikkatli kod yazmaya çalışıyor. Oysa bu bir hata değil, iki ayrı sistemin ortak bir onay noktasının olmamasından doğan yapısal bir sonuç.
Çözümün tamamı tek cümlede: iki yazma yapmak yerine, tek yazma yapıp ikincisini ondan türet. Outbox da, inbox da, CDC de aynı fikrin farklı yerlerdeki uygulaması.