Mikroservis Ödeme Akışlarında Çift Çekim (Double-Charge) Sızıntılarını Önlemek: Kafka Outbox ve Redis Idempotency
Blog'a Dön

Mikroservis Ödeme Akışlarında Çift Çekim (Double-Charge) Sızıntılarını Önlemek: Kafka Outbox ve Redis Idempotency

Buğra Şıkel

Mikroservis Ödeme Akışlarında Çift Çekim (Double-Charge) Sızıntılarını Önlemek: Kafka Outbox ve Redis Idempotency

Giriş

2018 yılının e-ticaret kampanya döneminde, ödeme altyapımızın p99 yanıt süresi aniden 45 saniyelere fırladı. Ödeme ağ geçidinden (payment gateway) ardı ardına 504 Gateway Timeout yanıtları gelmeye başladığında, iOS ve Android istemcilerindeki retry (yeniden deneme) mekanizmaları agresif bir şekilde devreye girdi. Sistem, 45 dakika gibi kısa bir süre içinde tam 12.430 siparişte çift çekim (double-charge) vakası yarattı. İade operasyonlarının manuel iş yükü, müşteri şikayetleri ve chargeback cezaları toplamda 180.000$ seviyesindeydi.

Dağıtık sistemlerde “Ağ güvenilirdir” (The network is reliable) yanılgısına düşmenin bedeli her zaman doğrudan finansal tablolara yansır. Monolitik mimarilerde tek bir PostgreSQL veritabanı üzerinde ACID transaction başlatarak çözülebilen veri tutarlılığı problemleri, mikroservis sınırlarından içeri girildiğinde tamamen boyut değiştirir. İstemciden gelen bir API isteğinin tam olarak bir kez işlendiğini (exactly-once processing) garanti etmek ağ katmanında matematiksel olarak imkansızdır; TCP/IP tabanlı iletişim protokolleri yalnızca “en az bir kez” (at-least-once) teslimat stratejisine izin verir.

Bir finansal işlemin birden fazla kez tetiklenmesini engellemenin kalıcı yolu, iş mantığını idempotent (tekrarlanabilir ve yan etkisiz) bir yapıya kavuşturmak ve Kafka gibi mesaj brokerları ile veritabanı arasındaki veri senkronizasyonunu Outbox Pattern ile kurgulamaktır.

İçindekiler

  • Redis 7.2 ile Dağıtık Idempotency Yönetimi
  • PostgreSQL 15 ve Kafka ile Outbox Pattern Implementasyonu
  • Performans ve Trade-off Analizi
  • Production Önerileri
  • Sık Sorulan Sorular
  • Sonuç

Redis 7.2 ile Dağıtık Idempotency Yönetimi

Idempotency-Key Standardı ve İstemci Davranışı

Stripe’ın sektöre kazandırdığı en önemli standartlardan biri Idempotency-Key HTTP başlığıdır. İstemci (mobil uygulama veya web frontend), ödeme isteğini API Gateway’e göndermeden önce benzersiz bir UUIDv4 üretir. Sunucu, bu anahtarı kullanarak gelen isteğin daha önce işlenip işlenmediğini kontrol eder. Ağ kesintisi nedeniyle istemci HTTP 200 OK yanıtını alamazsa, aynı UUID ile isteği tekrar gönderir. Sunucu, aynı anahtarı gördüğünde DB’ye insert atmak veya 3. parti gateway’e istek atmak yerine, önbelleğe alınmış önceki sonucu döndürür.

Concurrency ve Race Condition Sızıntıları

Basit bir SELECT ve ardından INSERT veya Redis tarafında standart bir GET ve SET komutu kullanmak, eşzamanlı (concurrent) gelen iki isteğin aynı anda veritabanına ulaşmasına neden olur. İşlem süresi 800ms sürüyorsa ve istemci 200. milisaniyede timeout varsayımıyla retry tetiklediyse, iki uygulama thread’i de GET komutundan “veri yok” yanıtını alacak ve ödeme servisine iki ayrı talep gönderecektir.

Bu durumu engellemek için Redis üzerinde atomik bir durum makinesi (state machine) tasarlanmalıdır. İsteğin üç durumu olabilir: STARTED, PROCESSING ve COMPLETED. Redis 7.2 üzerinde Lua script kullanarak bu kontrolü tek bir network round-trip ile atomik hale getirebiliriz.

-- Redis Idempotency Lua Script (Atomic Check-and-Set)
local key = KEYS[1]
local ttl = ARGV[1]
local current_state = redis.call('GET', key)

if current_state == 'COMPLETED' then
    -- İşlem daha önce başarıyla tamamlanmış, DB'den sonucu dön
    return {err = 'ALREADY_PROCESSED'}
elseif current_state == 'PROCESSING' then
    -- Başka bir thread şu an bu işlemi yürütüyor, 409 Conflict dön
    return {err = 'CONCURRENT_REQUEST'}
else
    -- Anahtar yok, lock işlemini biz üstleniyoruz
    redis.call('SET', key, 'PROCESSING', 'EX', ttl)
    return 'OK'
end

Uygulama katmanı bu script’ten OK yanıtı alırsa ödeme API’sine çağrı yapar. İşlem bittiğinde Redis state değerini COMPLETED olarak günceller ve üretilen ödeme referans kodunu (veya HTTP 200 payload’unu) bu anahtar altında saklar.

PostgreSQL 15 ve Kafka ile Outbox Pattern Implementasyonu

Dual-Write Probleminin Anatomisi

Mikroservis ortamında siparişi veritabanına kaydetmek ve ardından fatura/kargo servislerini haberdar etmek için Kafka’ya asenkron mesaj göndermek en sık rastlanan mimari zafiyettir. Aşağıdaki adımları inceleyelim:

  1. BEGIN TRANSACTION
  2. INSERT INTO payments (id, amount) VALUES (1, 100);
  3. COMMIT
  4. kafkaProducer.send("payment_completed", event);

Veritabanı commit işlemi başarılı olduktan sonra, 4. adımda Kafka kümesinde ağ gecikmesi yaşanırsa ve producer timeout fırlatırsa ne olur? Ödeme veritabanında oluşmuştur ancak ekosistemin geri kalanının bu ödemeden haberi yoktur. Müşteriden para çekilmiş, ancak sepet onaylanmamıştır. Sırayı tam tersine çevirip önce Kafka’ya yazıp sonra veritabanına commit atmak ise “hayalet işlem” (phantom payload) yaratır.

Mimari Kural: Dağıtık sistemlerde bir servisin dış dünyaya gönderdiği her olay (event), kendi yerel veritabanındaki ana iş kuralıyla aynı transaction sınırları içinde persist edilmelidir.

Transactional Outbox Tablosu ve Şema Tasarımı

Bu problemi kökünden çözmek için Outbox Pattern kullanılır. İş mantığı verileri ve Kafka’ya gönderilecek mesaj aynı PostgreSQL transaction’ı içinde kaydedilir. PostgreSQL 15 üzerinde aşağıdaki şema kurgulanır:

CREATE TABLE payment_outbox (
    id UUID PRIMARY KEY,
    aggregate_type VARCHAR(255) NOT NULL,
    aggregate_id VARCHAR(255) NOT NULL,
    event_type VARCHAR(255) NOT NULL,
    payload JSONB NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

Ödeme servisi, ana tabloya yazarken aynı bloğun içinde outbox tablosuna da yazar:

@Transactional(isolation = Isolation.READ_COMMITTED)
public void processPayment(PaymentRequest request, String idempotencyKey) {
    // 1. Ödemeyi DB'ye kaydet
    Payment payment = paymentRepository.save(new Payment(request));
    
    // 2. Outbox event oluştur
    OutboxEvent event = new OutboxEvent(
        UUID.randomUUID(),
        "Payment",
        payment.getId().toString(),
        "PaymentCompleted",
        objectMapper.writeValueAsString(payment)
    );
    outboxRepository.save(event);
}

Debezium 2.4 ile Log-Based Change Data Capture (CDC)

Outbox tablosuna yazılan kayıtları Kafka’ya taşımak için uygulamadan bağımsız bir CDC altyapısı kurulur. PostgreSQL konfigürasyonunda wal_level = logical ve max_replication_slots = 5 ayarları aktif edilir. Debezium, pgoutput eklentisi üzerinden bu mantıksal replikasyon slotuna bağlanır ve outbox tablosundaki her yeni satırı milisaniyeler içinde okuyarak Kafka topic’ine aktarır.

Performans ve Trade-off Analizi

Mimari kararlar alırken sistemin throughput değerlerine göre doğru okuma stratejisini seçmek veritabanı sağlığı için kritik öneme sahiptir.

Karşılaştırma Kriteri Polling (SELECT FOR UPDATE SKIP LOCKED) Debezium (WAL Tailing)
Mesaj Gecikmesi (Latency) Zamanlanmış göreve bağlı (Örn: 500ms – 2000ms) Replikasyon hızında (3ms – 8ms)
PostgreSQL Yükü Yüksek IOPS, lock tablosunda büyüme Sıfıra yakın (Doğrudan disk/WAL okuması)
Kurulum ve Bakım Kod seviyesinde (Basit Cron/Worker) Ayrı cluster gereksinimi (Kafka Connect)
Kullanım Senaryosu Dakikada < 2000 işlem, düşük trafik Saniyede > 5000 işlem, kritik event gecikmesi

Kendi production ortamımızda PostgreSQL 14’ten 15’e geçiş sürecinde, outbox tablosunda polling yöntemini bırakıp Debezium’a geçiş yaptık. 10 milyon satırlık aktif outbox tablosunda polling yöntemi saniyede 400 IOPS ek yük yaratıyor ve SKIP LOCKED kullanılmasına rağmen saniyede 4.000 işlem eşiğinde lock contention (kilit çakışması) başlatıyordu. Debezium ve WAL tailing mimarisine geçişin ardından EXPLAIN ANALYZE çıktılarında işlemci yükü sıfırlandı ve veritabanı sunucusunun toplam CPU kullanımında %32 düşüş sağlandı.

Production Önerileri

Sistemin yüksek trafik altında çökmesini engellemek için aşağıdaki konfigürasyonları kontrol listesi olarak değerlendirebilirsiniz:

  • Idempotency TTL Süreleri: Redis üzerindeki Idempotency-Key kayıtlarını kalıcı olarak bırakmayın. E-ticaret akışlarında 24 veya 48 saatlik TTL (Time-To-Live) süresi yeterlidir. 48 saatten eski bir retry talebi, sepet mantığı gereği 400 Bad Request ile reddedilmelidir.
  • Redis Bellek Yönetimi: Redis sunucusunda maxmemory-policy allkeys-lru ayarından kaçının. Bunun yerine volatile-lru tercih edin, böylece out-of-memory anında sadece süresi dolmaya yakın anahtarlar tahliye edilir. Anahtarlara mutlak surette EX (expire) değeri atayın.
  • Kafka Isolation Level: Consumer tarafında mesajları okurken, yalnızca tamamen commit edilmiş mesajları almak için Kafka consumer ayarlarında isolation.level=read_committed yapılandırmasını kullanın.
  • Consumer Tarafında İdempotency: Kafka’nın enable.idempotence=true ayarı sadece producer seviyesinde (Network katmanı) koruma sağlar. Mesajı dinleyen downstream servislerin (örneğin Fatura servisi) rebalance durumlarında aynı mesajı iki kez okuma ihtimaline karşı kendi yerel veritabanlarında `event_id` üzerinden Unique Constraint kontrolü yapması zorunludur.

Sık Sorulan Sorular

İstemci (Client) Idempotency Key üretmezse ne yapılmalı?

Eğer API istemcisi (örneğin eski bir versiyona sahip mobil uygulama) HTTP başlığında anahtar göndermiyorsa, API Gateway veya backend katmanında gelen isteğin SHA-256 hash’ini (user_id, amount, cart_id kombinasyonu) alarak sentetik bir anahtar oluşturabilirsiniz. En güvenli kural ise anahtar eksikliğinde doğrudan 400 Bad Request dönerek istemci ekibini bu standarda zorlamaktır.

Redis Cluster çökerse ödeme akışı durmalı mı?

Sistem tasarımı burada net bir yol ayrımına girer. Fail-open stratejisi izlerseniz ve ödemeleri doğrudan veritabanına geçirirseniz, olası bir retry sağanağında binlerce çift çekim vakası oluşur. Finansal sistemlerde standart, Fail-closed (Kapalı devre hata) uygulamak ve Redis ayağa kalkana kadar HTTP 503 dönerek işlemleri geçici olarak durdurmaktır. Redis Sentinel yapılandırması kesinti süresini 5-10 saniye aralığında tutacaktır.

Outbox tablosu sürekli büyüyecek mi? Nasıl temizlenmeli?

Debezium veriyi Kafka’ya başarıyla aktardıktan sonra outbox satırının veritabanında tutulmasına gerek yoktur. Sürekli büyüyen bir outbox tablosu, PostgreSQL autovacuum sürecini bloke eder ve tablo şişmesine (bloat) neden olur. Veritabanında PostgreSQL’in Native Partitioning özelliğini kullanarak tabloyu günlük bölümlere (partition) ayırabilir ve 3 günden eski partition’ları DROP TABLE komutu ile saniyeler içinde silebilirsiniz. Bu yöntem, DELETE operasyonlarının yaratacağı disk IOPS yükünden kurtarır.

Sonuç

Çift çekim vakaları, dağıtık mikroservis mimarilerinde yalnızca bir yazılım hatası değil, müşteri güvenini yok eden ve yasal chargeback yaptırımlarına neden olan ciddi bir operasyonel krizdir. Ağ katmanının %100 güvenilir olamayacağı gerçeğiyle yüzleştiğinizde, sistem tasarımınızı “hata kesinlikle olacak” önkoşuluyla kurgulamak mühendislik birikiminin bir gereğidir.

Redis 7.2 destekli Idempotency-Key kontrol mekanizması istemci kaynaklı tekrarları API kapısında eritirken; PostgreSQL 15 ve Kafka ikilisiyle inşa edilen Outbox Pattern, veritabanı ile asenkron olay kuyruğu arasındaki atomik bütünlüğü garanti eder. Mimarinize bu katmanları entegre ettiğinizde, sistem sadece hatalara karşı dirençli (resilient) olmakla kalmaz, aynı zamanda olay güdümlü mimarinin (event-driven architecture) sağladığı ölçeklenebilirlik avantajıyla üretim ortamında yüzbinlerce anlık işlemi sıfır veri kaybı garantisiyle yönetebilir.

Bunları da beğenebilirsiniz

TensorRT 8.6 Explicit Quantization: Production Modellerinde INT8 Hassasiyet Kayıplarını İzole Etme
10 Mayıs 2026

TensorRT 8.6 Explicit Quantization: Production Modellerinde INT8 Hassasiyet Kayıplarını İzole Etme

TensorRT 8.6’da ONNX GraphSurgeon ve Polygraphy kullanarak Explicit Quantization ile INT8 aktivasyon sapmalarını nasıl izole edeceğinizi teknik detaylarıyla inceleyin.

Devamını Oku
Dağıtık Sistemlerde Context Propagation Hataları: İzlenebilirlik Kayıplarını Teşhis Etme
26 Nisan 2026

Dağıtık Sistemlerde Context Propagation Hataları: İzlenebilirlik Kayıplarını Teşhis Etme

Heterojen mikroservis ağlarında context propagation hatalarını teşhis etme stratejilerini öğrenin ve üretim ortamındaki izlenebilirlik açıklarını kapatın.

Devamını Oku
Autoencoder vs. CNN: Görüntü Tabanlı Anomali Tespitinde Hangisi?
14 Ocak 2026

Autoencoder vs. CNN: Görüntü Tabanlı Anomali Tespitinde Hangisi?

Endüstriyel otomasyon, kalite kontrol ve güvenlik sistemleri gibi birçok alanda görüntü tabanlı anomali tespiti kritik bir rol oynamaktadır. Ancak bu karmaşık problemi bir web uygulamasına…

Devamını Oku
AI Asistan