Distributed Caching Stratejileri

Dağıtık önbellek kullanımında tutarlılığı sağlamak için cache aside, write-through, read-through gibi stratejilerin farkları ve ne zaman kullanılacağı açıklanır.

Distributed Caching Stratejileri

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?

  1. Uygulama, önce cache'ten veriyi okumaya çalışır.

  2. Cache'te veri varsa (cache hit), doğrudan kullanılır.

  3. 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

  1. 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.

  2. 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.

  3. Cache Stampede: Yüksek trafikte cache boşalmasını önlemek için Refresh-Ahead veya "lock" mekanizmaları (distributed lock) kullanın.

  4. Serialization (Serileştirme): Önbelleğe alınan verilerin boyutunu küçük tutun. System.Text.Json kullanarak 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.

Tüm yazılar

İlgili Yazılar