Performans Mimarisi & Caching Stratejileri 5 dk okuma 13 okunma

Cache-Aside Pattern ile Veritabanı Yükünü Azaltma

Cache-Aside (lazy loading), veritabanı yükünü azaltmanın en yaygın ve en basit yoludur: uygulama önce cache'e bakar, bulamazsa veritabanından okur ve cache'e yazar. Ancak bu desen "bedava" değildir; tutarlılık (staleness), cache stampede ve invalidation sorunlarını doğru yönetmek gerekir. .NET'te IMemoryCache ve IDistributedCache ile uygulanır; en kritik kural okuma-yazma sırasının doğru kurulmasıdır.

Cache-Aside Pattern ile Veritabanı Yükünü Azaltma

Cache-Aside Pattern ile Veritabanı Yükünü Azaltma

Veritabanı, çoğu uygulamanın en pahalı ve en zor ölçeklenen bileşenidir. Aynı veri defalarca okunduğunda, her seferinde veritabanına gitmek gereksiz yük ve gecikme yaratır. Cache-Aside Pattern (Lazy Loading), bu sorunu çözmenin en yaygın ve en basit yoludur: veri talep edildiğinde önce cache'e bakılır, bulunamazsa veritabanından okunur ve cache'e yazılır.


1. Cache-Aside Nasıl Çalışır?

Desenin akışı üç adımdan oluşur:

  1. Uygulama cache'e sorar. Anahtar (key) ile veri aranır.

  2. Cache hit (bulundu): Veri doğrudan cache'ten döner; veritabanına hiç gidilmez.

  3. Cache miss (bulunamadı): Veri veritabanından okunur, cache'e yazılır ve istemciye döner.

Kritik Özellik: Cache, veritabanının bir "aynası" değil, isteğe bağlı (on-demand) doldurulan bir kopyasıdır. Yalnızca talep edilen veri cache'lenir; tüm veritabanı önceden yüklenmez.

csharp

public async Task<Product?> GetProductAsync(int id)
{
    string cacheKey = $"product:{id}";

    // 1. Cache'e sor
    if (_cache.TryGetValue(cacheKey, out Product? cached))
        return cached;

    // 2. Cache miss: veritabanından oku
    var product = await _dbContext.Products.FindAsync(id);
    if (product is null) return null;

    // 3. Cache'e yaz (TTL ile)
    _cache.Set(cacheKey, product, TimeSpan.FromMinutes(10));
    return product;
}

2. En Kritik Kural: Invalidation Sırası

Cache-Aside'ın en sık yanlış uygulanan kısmı, yazma (write) işlemlerinde cache'in nasıl güncelleneceğidir. İki yaygın yaklaşım vardır:

Yaklaşım Açıklama Risk
Cache'i güncelle Yazma sonrası cache'teki değer de güncellenir Eşzamanlı yazmalarda tutarsızlık
Cache'i sil (invalidate) Yazma sonrası cache'teki anahtar silinir; bir sonraki okuma yeniden doldurur ✅ Önerilen yaklaşım

Neden Güncelleme Yerine Silme? İki eşzamanlı yazma işlemi düşünün: A yazar, cache'i günceller; B yazar, cache'i günceller. Ağ gecikmesi nedeniyle güncellemeler ters sırayla uygulanırsa, cache'te eski veri kalabilir. Silme yaklaşımında ise bir sonraki okuma her zaman veritabanından taze veri çeker .

Kritik Sıra: Yazma işleminde önce veritabanı, sonra cache silinmelidir. Ters sıra (önce cache sil, sonra veritabanına yaz) bir yarış durumu yaratır: cache silindiğinde başka bir okuma veritabanından eski veriyi çekip cache'e yazabilir .

csharp

public async Task UpdateProductAsync(Product product)
{
    // 1. Önce veritabanını güncelle
    _dbContext.Products.Update(product);
    await _dbContext.SaveChangesAsync();

    // 2. Sonra cache'i sil (güncelleme değil!)
    _cache.Remove($"product:{product.Id}");
}

3. Cache Stampede: Sessiz Katil

Cache-Aside'ın en tehlikeli sorunu, cache stampede (thundering herd) olarak bilinir. Popüler bir anahtarın TTL'si dolduğunda, o anda gelen binlerce istek aynı anda cache miss yaşar ve hepsi veritabanına hücum eder. Bu, veritabanını anlık olarak çökertebilir.

Çözüm 1: Kilit (Lock) ile Tek Yükleme
Aynı anahtar için yalnızca bir isteğin veritabanına gitmesine izin verin; diğerleri bekler.

csharp

private static readonly SemaphoreSlim _lock = new(1, 1);

public async Task<Product?> GetProductSafeAsync(int id)
{
    string cacheKey = $"product:{id}";
    if (_cache.TryGetValue(cacheKey, out Product? cached))
        return cached;

    await _lock.WaitAsync();
    try
    {
        // Çift kontrol: kilit beklerken başka biri doldurmuş olabilir
        if (_cache.TryGetValue(cacheKey, out cached))
            return cached;

        var product = await _dbContext.Products.FindAsync(id);
        if (product is not null)
            _cache.Set(cacheKey, product, TimeSpan.FromMinutes(10));
        return product;
    }
    finally
    {
        _lock.Release();
    }
}

Çözüm 2: Kademeli TTL (Jitter)
Aynı anda dolan anahtarların TTL'sine rastgele bir sapma ekleyin: 10 dakika + random(0-60 saniye).

Çözüm 3: Erken Yenileme (Refresh-Ahead)
TTL dolmadan önce, arka planda cache'i yenileyin; kullanıcı hiç cache miss yaşamasın.


4. Tutarlılık ve TTL: Ne Kadar Eski Veri Kabul Edilebilir?

Cache-Aside, eventual consistency getirir: veritabanı güncellendiğinde cache'teki eski veri, TTL dolana veya invalidation yapılana kadar kalabilir.

TTL Seçimi:

Veri Türü Önerilen TTL Gerekçe
Ürün kataloğu, kategori listesi 10-60 dakika Sık değişmez, eski veri tolere edilebilir
Kullanıcı profili 5-15 dakika Orta sıklıkta değişir
Stok, fiyat, bakiye 30 saniye - 2 dakika Tutarlılık kritik; kısa TTL veya invalidation
Referans veriler (ülke, para birimi) Saatler - günler Neredeyse hiç değişmez

Kritik Kural: TTL, iş gereksinimlerine göre belirlenmelidir. "Ne kadar eski veri kabul edilebilir?" sorusunun cevabı, TTL'yi belirler. Finansal verilerde saniyeler, ürün kataloğunda dakikalar kabul edilebilir.


5. .NET'te Uygulama: IMemoryCache vs. IDistributedCache

Özellik IMemoryCache IDistributedCache
Kapsam Tek instance (in-process) Tüm instance'lar (Redis, SQL)
Hız En hızlı (bellek içi) Ağ gecikmesi (~1-5ms)
Kullanım Tek sunuculu uygulamalar Yatay ölçeklenmiş sistemler
Tutarlılık Instance başına farklı olabilir Merkezi, tutarlı

Yatay ölçeklenmiş sistemlerde IMemoryCache yetersizdir: her instance kendi cache'ini tutar, bir instance'ta güncellenen veri diğerlerinde eski kalır. Bu durumda Redis tabanlı IDistributedCache kullanılmalıdır.

csharp

// Redis ile distributed cache
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "MyApp:";
});

// Kullanım
public async Task<Product?> GetProductAsync(int id)
{
    string cacheKey = $"product:{id}";
    var cached = await _cache.GetStringAsync(cacheKey);
    if (cached is not null)
        return JsonSerializer.Deserialize<Product>(cached);

    var product = await _dbContext.Products.FindAsync(id);
    if (product is not null)
    {
        await _cache.SetStringAsync(cacheKey,
            JsonSerializer.Serialize(product),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
            });
    }
    return product;
}

6. Ne Zaman Cache-Aside Kullanılmalı?

Uygun Uygun Değil
Okuma yoğun, yazma seyrek iş yükleri Yazma yoğun iş yükleri
Aynı verinin sık okunması Her okumanın farklı veri getirdiği senaryolar
Eski verinin kısa süre tolere edilebildiği durumlar Güçlü tutarlılık gerektiren finansal işlemler
Veritabanı yükünü azaltmak isteyen sistemler Cache'in kendisinin darboğaz olacağı sistemler

7. En İyi Pratikler

Pratik Açıklama
Yazmada önce DB, sonra cache sil Ters sıra yarış durumu yaratır
TTL'i iş gereksinimine göre belirleyin "Ne kadar eski veri kabul edilebilir?" sorusuna yanıt verin
Stampede koruması ekleyin Kilit, jitter veya refresh-ahead kullanın
Yatay ölçeklemede distributed cache kullanın IMemoryCache instance başına farklılaşır
Cache key'lerini tutarlı adlandırın product:{id} gibi; versiyonlama için product:v2:{id}
Cache'i bir "hızlandırıcı" olarak görün Cache çökerse uygulama veritabanından çalışmaya devam etmelidir (graceful degradation)

Sonuç

Cache-Aside Pattern, veritabanı yükünü azaltmanın en yaygın ve en etkili yoludur. Okuma yoğun sistemlerde, doğru TTL ve invalidation stratejisiyle veritabanı yükünü önemli ölçüde düşürür ve yanıt sürelerini milisaniyelere indirir. Ancak bu desen "bedava" değildir: tutarlılık, stampede ve invalidation sırası gibi konular yanlış yönetilirse, faydadan çok zarar getirir. Unutmayın: Cache, veritabanının yerini almaz; onu rahatlatır. Cache çöktüğünde sistem çalışmaya devam etmelidir; cache'e bağımlı bir mimari, tek nokta arızası yaratır.

Tüm yazılar Soru ya da yorumun mu var? Yaz