İçeriğe geç
Çılgın Yazılım

Kurumsal Yazılım ve Entegrasyon kod örnekli

Idempotency Key: Aynı İsteği İki Kez Almanın Bedelini Sıfırlamak

Ağ koptuğunda istemci isteği tekrarlar. Sunucu bunu ikinci bir sipariş sanarsa müşteri iki kez ödeme yapar. Çözüm bir başlık.

Çılgın Yazılım 3 dk okuma

Idempotency Key: Aynı İsteği İki Kez Almanın Bedelini Sıfırlamak
İçindekiler
  1. Problem gerçekte ne?
  2. Sözleşme: istemci anahtarı üretir
  3. Sunucu tarafı: kayıt, kilit, yanıt
  4. Sık atlanan ayrıntılar
  5. Sonuç

Bir idempotency key, istemcinin ürettiği ve isteğe iliştirdiği benzersiz bir değerdir. Sunucu bu değeri gördüğünde şunu anlar: "Bu isteği daha önce işlediysem tekrar işleme, ilk sonucu geri ver." Kulağa küçük bir ayrıntı gibi gelir; oysa ödeme, sipariş ve stok düşme gibi işlemlerde çift kaydı önleyen tek gerçekçi mekanizma budur.

Tekrarlı isteklerin oluştuğu senaryolar
Sunucu, iki isteğin "aynı" olduğunu ancak istemci söylerse bilebilir.

Problem gerçekte ne?

İstemci bir POST isteği gönderdi, sunucu işledi, ama yanıt dönerken bağlantı koptu. İstemcinin elinde tek bir bilgi var: yanıt gelmedi. Bu durumda ne yaparsa yapsın hatalıdır — tekrar gönderirse çift kayıt riski, göndermezse kayıp işlem riski vardır. Belirsizliği ancak sunucu tarafında kaldırabilirsiniz.

GET, PUT ve DELETE tanım gereği idempotent'tir. Sorun neredeyse her zaman POST ile başlar; çünkü POST "yeni bir şey yarat" anlamına gelir ve her çağrı yeni bir kayıt üretmeyi meşru kılar.

Sözleşme: istemci anahtarı üretir

Anahtarı sunucu üretemez, çünkü tekrarlanan isteğin "aynı" olduğunu yalnızca istemci bilir. İstemci, kullanıcı işlemi başlattığı anda bir UUID üretir ve tüm yeniden denemelerde aynı değeri gönderir.

JavaScript
// Anahtar, ilk denemede üretilir ve yeniden denemelerde DEĞİŞMEZ
const anahtar = crypto.randomUUID();

async function siparisOlustur(govde, deneme = 0) {
  const yanit = await fetch('/api/v1/siparisler', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Idempotency-Key': anahtar,
    },
    body: JSON.stringify(govde),
  });

  // Geçici hatalarda üstel geri çekilme ile tekrar dene
  if (yanit.status >= 500 && deneme < 3) {
    await new Promise((r) => setTimeout(r, 2 ** deneme * 500));
    return siparisOlustur(govde, deneme + 1);
  }

  return yanit.json();
}

Sunucu tarafı: kayıt, kilit, yanıt

Sunucu tarafında bu kaydı tutarken eşzamanlı isteklerin çakışması kilitlenmeye yol açabilir; benzersiz indeks bu riski de sınırlar. Sunucunun işi üç adımdır. Anahtarı benzersiz bir sütunda saklar, aynı anahtarla ikinci istek geldiğinde ilk yanıtı geri döner ve iki isteğin aynı anda gelmesi ihtimaline karşı yarış durumunu veritabanı kısıtıyla kapatır.

SQL
CREATE TABLE idempotency_kayitlari (
  id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  anahtar       VARCHAR(64)  NOT NULL,
  istek_ozeti   CHAR(64)     NOT NULL,        -- gövdenin SHA-256'sı
  durum         ENUM('isleniyor','tamam') NOT NULL DEFAULT 'isleniyor',
  yanit_kodu    SMALLINT     NULL,
  yanit_govde   JSON         NULL,
  olusturuldu   DATETIME     NOT NULL,
  UNIQUE KEY uq_anahtar (anahtar)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Benzersiz indeks burada bir performans aracı değil, doğruluk aracıdır: iki isteğin aynı milisaniyede gelmesi hâlinde ikincisini veritabanı reddeder. Uygulama katmanında "önce sorgula, sonra yaz" yaklaşımı bu yarışı kapatmaz.

PHP
public function store()
{
    $anahtar = trim((string) $this->request->getHeaderLine('Idempotency-Key'));
    if ($anahtar === '') {
        return $this->response->setStatusCode(400)
            ->setJSON(['hata' => 'Idempotency-Key başlığı zorunludur.']);
    }

    $govde  = $this->request->getJSON(true) ?? [];
    $ozet   = hash('sha256', json_encode($govde, JSON_UNESCAPED_UNICODE));
    $tablo  = db_connect()->table('idempotency_kayitlari');

    $mevcut = $tablo->where('anahtar', $anahtar)->get()->getRowArray();

    if ($mevcut) {
        // Aynı anahtar, FARKLI gövde: istemci hatası, sessizce kabul etme
        if ($mevcut['istek_ozeti'] !== $ozet) {
            return $this->response->setStatusCode(422)
                ->setJSON(['hata' => 'Bu anahtar farklı bir istek gövdesiyle kullanılmış.']);
        }

        if ($mevcut['durum'] === 'isleniyor') {
            return $this->response->setStatusCode(409)
                ->setJSON(['hata' => 'İstek hâlâ işleniyor, kısa süre sonra tekrar deneyin.']);
        }

        return $this->response->setStatusCode((int) $mevcut['yanit_kodu'])
            ->setJSON(json_decode($mevcut['yanit_govde'], true));
    }

    $tablo->insert([
        'anahtar'     => $anahtar,
        'istek_ozeti' => $ozet,
        'durum'       => 'isleniyor',
        'olusturuldu' => date('Y-m-d H:i:s'),
    ]);

    $sonuc = $this->siparisServisi->olustur($govde);

    $tablo->where('anahtar', $anahtar)->update([
        'durum'      => 'tamam',
        'yanit_kodu' => 201,
        'yanit_govde'=> json_encode($sonuc, JSON_UNESCAPED_UNICODE),
    ]);

    return $this->response->setStatusCode(201)->setJSON($sonuc);
}

Yükleme ve indirme uçlarında aynı tekrar sorunu görülür; güvenli dosya yükleme rehberimiz bu ucu adım adım kuruyor.

Sık atlanan ayrıntılar

Gövde özeti neden gerekli? Aynı anahtarla farklı bir gövde gelirse bu bir tekrar değil, istemci hatasıdır. Sessizce eski yanıtı dönmek, hatayı gizleyip çok daha sonra ortaya çıkarır.

Kayıtlar ne kadar saklanmalı? Genellikle 24 saat yeterlidir; bu süre yeniden deneme pencerelerinin çok üzerindedir. Eski kayıtları düzenli temizlemezseniz tablo gereksiz büyür.

Yeniden deneme politikasıyla birlikte düşünün. Idempotency anahtarı olmadan yapılan otomatik yeniden denemeler, sorunu çözmek yerine çoğaltır — bu ilişkiyi üçüncü parti API dayanıklılığı yazımızda ayrıntılı ele almıştık. Uçları tasarlarken webhook uçlarındaki yaygın hatalar da benzer bir tekrar sorununa işaret eder.

HTTP tarafındaki resmî tanım ve semantik için MDN idempotency tanımı kısa ve nettir.

Sonuç

Tekrarlı istek bir istisna değil, dağıtık sistemlerin normalidir: zaman aşımı, otomatik yeniden deneme ve çift tıklama her gün olur. Idempotency anahtarı bu üç durumu tek bir mekanizmayla çözer — istemci anahtarı üretir, sunucu benzersiz indeksle yarışı kapatır ve ilk yanıtı saklayıp tekrar sunar. Uygulaması bir tablo ve elli satır koddur; getirisi ise çift ödeme, çift sipariş ve mutabakat kâbuslarının tamamen ortadan kalkmasıdır. Para veya stok hareketi yaratan bir sonraki POST ucunuzu yazarken, bu başlığı sözleşmenin zorunlu parçası yapın.

Beğeniniz benzer içerikleri öne çıkarmamıza yardımcı olur.

Sık sorulan sorular

Idempotency anahtarını sunucu üretemez mi?

Üretemez, çünkü sunucu iki isteğin aynı niyeti taşıdığını bilemez. İki farklı kullanıcının aynı anda aynı ürünü sipariş etmesi ile tek kullanıcının aynı isteği iki kez göndermesi sunucuya birebir aynı görünür. Ayrımı yalnızca istemci yapabilir.

Anahtar kayıtlarını ne kadar süre saklamalıyım?

Pratikte 24 saat çoğu senaryo için yeterlidir; yeniden deneme pencereleri genelde dakikalarla sınırlıdır. Finansal mutabakat gereksinimi varsa süreyi uzatabilirsiniz, ancak o zaman tabloyu bölümlemeyi (partitioning) ve düzenli arşivlemeyi planlayın.

İşlem hâlâ sürerken ikinci istek gelirse ne dönmeliyim?

409 Conflict ile "işleniyor, kısa süre sonra tekrar deneyin" mesajı en güvenli davranıştır. İkinci isteği beklemeye almak sunucu kaynaklarını kilitler; sessizce ikinci bir işlem başlatmak ise korumanın tamamen amacını ortadan kaldırır.

GET isteklerinde de idempotency anahtarı kullanmalı mıyım?

Hayır. GET zaten tanım gereği idempotent olmalıdır ve yan etki üretmemelidir. Anahtar yalnızca durum değiştiren isteklerde — özellikle POST ve bazı PATCH kullanımlarında — anlamlıdır.

Yazan

Çılgın Yazılım

Ben Evren Çılgın; yazılım geliştirme, web teknolojileri ve sistem tasarımı alanlarında uzmanlaşmış bir geliştiriciyim. Uzun yıllardır hem frontend hem de backend tarafında üretken, ölçeklenebilir ve kullanıcı dostu çözümler üretiyorum.

Hakkımızda Bize ulaşın

Bu konuda bir projeniz mi var?

İhtiyacınızı birkaç cümleyle anlatın; uygun yaklaşımı birlikte belirleyelim.

Projenizi Anlatın

İlgili yazılar

Yorumlar

Henüz yorum yok. Sorunuzu ya da deneyiminizi ilk siz yazın.

Yorum yazın

Yorumunuz onaylandıktan sonra yayınlanır. Ekibimiz gerekirse konuyla ilgili bir yanıt da paylaşır.

Yayınlanmaz; yalnızca gerektiğinde size ulaşmak için.

En az 10 karakter.

Tüm yazılar