Idempotency Key Tasarımı: Güvenli Tekrarlanan İstekler
Dağıtık sistemlerde, ağ zaman aşımları, istemci retry'ları veya kullanıcıların çift tıklaması nedeniyle aynı istek birden fazla kez gönderilebilir. Ödeme, sipariş oluşturma, bakiye transferi gibi yan etkili işlemlerde bu, çift işlem (double charge, duplicate order) anlamına gelir. Idempotency Key, bu sorunu çözen, istemci tarafından üretilen benzersiz bir anahtardır; sunucu bu anahtar sayesinde isteğin daha önce işlenip işlenmediğini anlar.
1. Idempotency Key Nedir ve Nasıl Çalışır?
İstemci, her mantıksal işlem için benzersiz bir anahtar (genellikle UUID v4) üretir ve istekle birlikte gönderir. Sunucu bu anahtarı bir depoda arar:
-
Anahtar yoksa: İşlem gerçekleştirilir, sonuç (durum kodu + yanıt gövdesi) anahtarla birlikte saklanır.
-
Anahtar varsa (işlem tamamlanmış): İşlem tekrarlanmaz; saklanan yanıt aynen döner.
-
Anahtar varsa (işlem devam ediyor): Genellikle
409 Conflictdöner; istemci beklemelidir.
Kritik Kural: Anahtar, istek gövdesinden bağımsız olarak yalnızca o mantıksal işlemi temsil etmelidir. Aynı anahtar, farklı bir gövdeyle gelirse bu bir hata durumudur ve reddedilmelidir.
2. Tasarım Kararları: Scope, Format ve TTL
A. Scope (Kapsam) ve Benzersizlik
Anahtar, kiracı (tenant) ve endpoint bazında benzersiz olmalıdır. Aynı anahtarın farklı kullanıcılar veya farklı API'ler arasında çakışması, veri sızıntısına yol açar. Depolama anahtarı genellikle şu şekilde birleştirilir:
text
{userId}:{endpoint}:{idempotencyKey}
B. Anahtar Formatı
-
UUID v4: En yaygın ve önerilen yöntem. İstemci üretir; çakışma olasılığı ihmal edilebilir.
-
İstek Hash'i: Gövdenin hash'i. Ancak zaman damgası gibi değişkenler hash'i bozar; dikkatli kullanılmalıdır.
-
İşlem Kimliği: İş mantığında zaten var olan benzersiz bir kimlik (ör. sipariş numarası).
C. TTL (Yaşam Süresi)
Anahtarlar sonsuza kadar saklanmamalıdır. Tipik TTL 24-72 saat arasındadır. Ödeme gibi uzun vadeli işlemlerde daha uzun, oturum bazlı işlemlerde daha kısa tutulabilir.
3. En Kritik Konu: Atomiklik ve Eşzamanlı İstekler
Aynı anahtarla aynı anda iki istek gelirse ne olur? Bu, tasarımın en zorlu kısmıdır.
"First-Wins" (İlk Kazanır) Yaklaşımı:
İlk istek işleme alınırken anahtar "işlemde" (processing) olarak işaretlenir; aynı anahtarla gelen diğer istekler ya bekletilir ya da 409 Conflict alır.
Atomik İşlem Nasıl Sağlanır?
-
Veritabanı: Anahtar üzerinde
UNIQUEkısıtlaması ile tek bir transaction içinde işlem + kayıt yapılır. İkinci istekunique constraint violationalır. -
Redis:
SET key value NX PX ttlkomutu ile atomik olarak "yoksa yaz" işlemi yapılır.NXbaşarısız olursa, istek zaten işlemdedir.
Hata Durumları:
-
İşlem başarısız olduysa: Anahtar silinmeli veya "başarısız" olarak işaretlenmelidir; aksi halde istemci aynı anahtarla asla tekrar deneyemez.
-
İşlem başarılı olduysa: Yanıt saklanır ve tekrarlanan isteklerde döner.
4. Pratik Uygulama: .NET'te Idempotency Key
Örnek: Redis Tabanlı Idempotency Middleware
csharp
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("Idempotency-Key", out var key))
{
await _next(context);
return;
}
var cacheKey = $"{context.User.Identity?.Name}:{context.Request.Path}:{key}";
// Atomik "yoksa yaz" (NX)
var acquired = await _redis.StringSetAsync(cacheKey, "processing",
TimeSpan.FromHours(24), When.NotExists);
if (!acquired)
{
// Zaten işlenmiş veya işlemde
var cached = await _redis.StringGetAsync(cacheKey);
if (cached == "processing")
{
context.Response.StatusCode = 409;
return;
}
// Saklanan yanıtı döndür
context.Response.StatusCode = 200;
await context.Response.WriteAsync(cached);
return;
}
// İşlemi çalıştır ve yanıtı yakala
var originalBody = context.Response.Body;
using var memStream = new MemoryStream();
context.Response.Body = memStream;
await _next(context);
memStream.Position = 0;
var responseBody = await new StreamReader(memStream).ReadToEndAsync();
// Başarılıysa yanıtı sakla; değilse anahtarı sil
if (context.Response.StatusCode < 400)
await _redis.StringSetAsync(cacheKey, responseBody, TimeSpan.FromHours(24));
else
await _redis.KeyDeleteAsync(cacheKey);
memStream.Position = 0;
await memStream.CopyToAsync(originalBody);
}
Hazır Kütüphaneler: IdempotentAPI (NuGet), ASP.NET Core için hazır middleware ve Redis/SQL Server desteği sunar.
5. Ne Zaman Kullanılmalı?
| Uygun | Uygun Değil |
|---|---|
| POST ile kaynak oluşturma (sipariş, ödeme) | GET istekleri (zaten idempotent) |
| Finansal işlemler (bakiye transferi, iade) | PUT/DELETE (doğası gereği idempotent) |
| Harici API çağrıları (retry riski) | Kesinlikle idempotent olmayan işlemler |
| Kullanıcı çift tıklaması riski olan formlar | — |
Önemli: Idempotency Key, yalnızca yan etkili (non-idempotent) işlemlerde anlamlıdır. GET, PUT, DELETE gibi doğası gereği idempotent metotlarda kullanmak gereksiz karmaşıklık yaratır.
6. En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| Scope'u doğru belirleyin | (kullanıcı, endpoint, anahtar) bazında benzersizlik sağlayın. |
| Atomik "yoksa yaz" kullanın | Redis SET NX veya veritabanı UNIQUE kısıtlaması. |
| Hata durumunda anahtarı silin | Aksi halde istemci aynı anahtarla tekrar deneyemez. |
| TTL belirleyin | 24-72 saat; işlemin doğasına göre ayarlayın. |
| Anahtarı asla log'lamayın | Anahtar, hassas bir kimlik bilgisi gibi davranmalıdır. |
Sonuç
Idempotency Key, dağıtık sistemlerde güvenli retry'ların temelidir. Doğru tasarlandığında, ağ hataları ve kullanıcı hataları nedeniyle oluşan çift işlemleri engeller; yanlış tasarlandığında ise ya işlemleri kilitler ya da güvenlik açığı yaratır. Anahtarın scope'u, atomikliği, TTL'i ve hata durumları dikkatle ele alınmalıdır. Unutmayın: Idempotency Key bir "sihirli değnek" değil, bir sözleşmedir; istemci ve sunucu bu sözleşmeye aynı kurallarla uymalıdır.