Distributed Caching Stratejileri: Cache Aside, Write-Through ve Read-Through ile Tutarlılığı Sağlamak
Dağıtık önbellek (Distributed Cache) kullanmak, uygulamanızın performansını inanılmaz derecede artırabilir. Ancak, önbelleğin veritabanı veya diğer veri kaynaklarıyla tutarlı (consistent) kalması, doğru stratejiye bağlıdır. Yanlış bir strateji, "stale data" (güncel olmayan veri) veya "cache stampede" (önbellek çökmeleri) gibi sorunlara yol açabilir. Bu yazıda, en yaygın 5 önbellek stratejisini ve hangi senaryoda kullanılacağını ele alıyoruz.
1. Cache Aside (Lazy Loading - Tembel Yükleme)
Nasıl Çalışır?
-
Uygulama, önce cache'ten veriyi okumaya çalışır.
-
Cache'te veri varsa (cache hit), doğrudan kullanılır.
-
Cache'te veri yoksa (cache miss), uygulama veritabanından veriyi okur, cache'e yazar ve sonra kullanıcıya döner.
csharp
public async Task<Order> GetOrderAsync(int orderId)
{
var cacheKey = $"order:{orderId}";
// 1. Cache'ten oku
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
return JsonSerializer.Deserialize<Order>(cached);
// 2. Cache miss - Veritabanından oku
var order = await _dbContext.Orders.FindAsync(orderId);
if (order == null)
return null;
// 3. Cache'e yaz (TTL ile)
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(order),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });
return order;
}
Artıları:
-
Basitlik: Uygulaması kolaydır.
-
Esneklik: Veritabanı güncellemelerinden etkilenmez; sadece cache'in TTL'si (Time-to-Live) geçince yenilenir.
-
Kaynak Verimliliği: Sadece talep edilen veriler cache'e alınır.
Eksileri:
-
Stale Data Riski: Veritabanı değiştiğinde cache otomatik güncellenmez. TTL bitene kadar eski veri kalır.
-
Cache Stampede: Cache boşaldığında (cache miss), aynı anda gelen binlerce istek veritabanına çöker.
Ne Zaman Kullanılır?
-
Okuma ağırlıklı sistemler.
-
Verinin sık değişmediği, tutarlılığın saniyeler veya dakikalar mertebesinde tolere edilebildiği durumlar.
-
Örnek: Ürün detayları, blog yazıları.
2. Read-Through (Proxy Model)
Nasıl Çalışır?
Cache, doğrudan veritabanına bağlanabilen bir "proxy" gibi davranır. Uygulama cache'den okur; cache'de yoksa, cache mekanizması veritabanından otomatik olarak okur ve cache'e yazar.
(Bu strateji, özel bir cache library veya dağıtık cache ürünleri (ör. NCache, Redis özel implementasyonları) tarafından sağlanır.)
csharp
// Uygulama sadece cache'den okur, veritabanını bilmez.
public class ReadThroughCache : IDistributedCache
{
private readonly IDistributedCache _innerCache;
private readonly AppDbContext _dbContext;
public override async Task<byte[]> GetAsync(string key, CancellationToken token)
{
var value = await _innerCache.GetAsync(key, token);
if (value != null)
return value;
// Cache miss: Veritabanından oku ve cache'e yaz
var entityId = key.Split(':')[1];
var entity = await _dbContext.Orders.FindAsync(int.Parse(entityId));
if (entity != null)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(entity);
await _innerCache.SetAsync(key, bytes, new DistributedCacheEntryOptions { ... }, token);
return bytes;
}
return null;
}
}
Artıları:
-
Uygulama Katmanından Veritabanı Mantığını Soyutlar: Uygulama sadece cache ile konuşur.
-
Cache Stampede Azalır: Cache, veritabanına giden istekleri kontrol edebilir.
Eksileri:
-
Stale Data Riski: Yazma işlemleri hala doğrudan veritabanına yapılırsa cache güncellenmez.
-
Karmaşıklık: Cache katmanı daha akıllı hale gelir.
Ne Zaman Kullanılır?
-
Cache'li bir veri katmanı (data access layer) tasarlıyorsanız.
-
Uygulamanın veritabanı ayrıntılarını bilmesini istemediğinizde.
3. Write-Through (Proxy Model - Yazma)
Nasıl Çalışır?
Uygulama, veriyi önce cache'e yazar. Cache, bu veriyi hemen (senkron) veritabanına da yazar. Bu işlem tamamlanana kadar uygulama bloke olur.
csharp
public class WriteThroughCache
{
public async Task SetAsync(string key, Order value)
{
// 1. Önce cache'e yaz
await _cache.SetStringAsync(key, JsonSerializer.Serialize(value));
// 2. Sonra veritabanına yaz (senkron)
await _dbContext.Orders.AddAsync(value);
await _dbContext.SaveChangesAsync();
}
}
Artıları:
-
Yüksek Tutarlılık: Veri hem cache'de hem veritabanında anında güncellenir. Stale data riski neredeyse yoktur.
-
Basit Tüketim: Uygulama tek bir çağrı yapar.
Eksileri:
-
Performans Düşüşü: Her yazma işlemi, ağ gecikmesi (cache + DB) nedeniyle yavaşlar.
-
Veritabanı Yükü: Tüm yazmalar veritabanına gider.
Ne Zaman Kullanılır?
-
Tutarlılığın kritik olduğu ve yazma performansının ikincil öneme sahip olduğu sistemler.
-
Örnek: Finansal işlemler, rezervasyon sistemleri.
4. Write-Behind (Write-Back - Asenkron Yazma)
Nasıl Çalışır?
Uygulama, veriyi sadece cache'e yazar ve hemen yanıt döner (asenkron). Cache, bu veriyi daha sonra (periyodik olarak veya bir iş kuyruğu ile) veritabanına asenkron olarak yazar.
Artıları:
-
Yüksek Performans: Yazma işlemleri çok hızlıdır (sadece cache yazma maliyeti).
-
Yüksek Veri Toleransı: Kısa süreli veritabanı kesintileri tolere edilir (veriler cache'de saklanır).
Eksileri:
-
Veri Kaybı Riski: Cache çökerse veya yeniden başlatılırsa, yazılmamış veriler kalıcı olarak kaybolur.
-
Karmaşıklık: Veritabanına yazma işlemini yönetmek için ek mekanizmalar gerekir (kuyruk, retry, monitoring).
Ne Zaman Kullanılır?
-
Yüksek yazma trafiğinin olduğu sistemler (log toplama, olay kaydı).
-
Veri kaybının kabul edilebilir olduğu (veya veritabanı ile eventual consistency'nin yeterli olduğu) durumlar.
-
Örnek: Anlık görüntüleme (analytics), clickstream verileri.
5. Cache Aside + Write-Through Kombinasyonu (Refresh-Ahead)
Nasıl Çalışır?
Cache, popüler verileri TTL'den önce yeniler. Bu, "cache stampede" (önbellek çökmesi) sorununu azaltır.
csharp
// Cache Aside gibi, ancak TTL sonuna yaklaşırken arka planda yeniler.
public async Task<Order> GetOrderAsync(int orderId)
{
var cacheKey = $"order:{orderId}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
{
// TTL sonuna yaklaşıyorsa arka planda yenile
if (IsNearExpiration(cacheKey))
_ = RefreshCacheAsync(orderId, cacheKey); // Fire-and-forget
return JsonSerializer.Deserialize<Order>(cached);
}
// Cache miss -> normal yöntem
}
Artıları:
-
Cache Stampede'i Azaltır: Veriler sürekli yenilenir, böylece aynı anda veritabanına çökmeler önlenir.
-
Düşük Gecikme: Kullanıcılar neredeyse her zaman sıcak cache'ten yararlanır.
Eksileri:
-
Karmaşıklık: Zamanlama ve arka plan görev yönetimi ekler.
-
Veritabanı Yükü: Popüler veriler sürekli yenilenir.
Ne Zaman Kullanılır?
-
Çok yüksek trafikli sistemlerde cache stampede riskini azaltmak için.
-
Örnek: Ana sayfa ürün listeleri, anlık popüler veriler.
Hangi Stratejiyi Ne Zaman Kullanmalısınız?
| Strateji | Okuma Performansı | Yazma Performansı | Veri Tutarlılığı | Uygulama Karmaşıklığı | Kullanım Senaryosu |
|---|---|---|---|---|---|
| Cache Aside | 🟢 Yüksek | 🟢 Yüksek | 🟡 Orta (TTL riski) | 🟢 Düşük | Basit okuma ağırlıklı sistemler |
| Read-Through | 🟡 Orta | 🟢 Yüksek | 🟡 Orta | 🟡 Orta | Veri katmanı soyutlaması gereken sistemler |
| Write-Through | 🟢 Yüksek | 🔴 Düşük | 🟢 Çok Yüksek | 🟡 Orta | Tutarlılığın kritik olduğu sistemler |
| Write-Behind | 🟢 Yüksek | 🟢 Yüksek | 🟡 Düşük (veri kaybı riski) | 🔴 Yüksek | Yüksek yazma trafiği, eventual consistency |
| Refresh-Ahead | 🟢 Çok Yüksek | 🟢 Yüksek | 🟡 Orta | 🔴 Yüksek | Çok yüksek trafik, cache stampede riski |
Distributed Caching'de Ek İpuçları ve Tuzaklar
-
Tutarlılık (Consistency): Stale data'yı kabul edip etmediğinize karar verin. Eğer kabul edemiyorsanız, Write-Through veya Cache Aside + Invalidasyon (güncelleme sonrası cache'i silmek) kullanın.
-
Cache Invalidation (Cache Silme): Cache Aside ile veritabanı güncellendiğinde, ilgili cache key'ini silmeyi unutmayın. Bu, tutarlılığı ciddi oranda artırır.
-
Cache Stampede: Yüksek trafikte cache boşalmasını önlemek için Refresh-Ahead veya "lock" mekanizmaları (distributed lock) kullanın.
-
Serialization (Serileştirme): Önbelleğe alınan verilerin boyutunu küçük tutun.
System.Text.Jsonkullanarak verimli serileştirme yapın. Gereksiz alanları (örn.InternalNote)[JsonIgnore]ile işaretleyin.
Sonuç:
Dağıtık önbellek stratejileri, performans ve tutarlılık arasında bir denge kurmaktır. Cache Aside, çoğu senaryoda en basit ve etkili yöntemdir. Ancak, veri tutarlılığı çok kritikse Write-Through, yazma performansı öncelikliyse Write-Behind stratejilerini düşünün. Hangi stratejiyi seçerseniz seçin, cache'in nasıl ve ne zaman yenileneceğini net bir şekilde belirleyin ve uygulamanızın "stale data" ile nasıl başa çıkacağını planlayın.