Ana Sayfa / Blog / Cache invalidation: bilinen 2 zor problem ve pragmatik çözümleri

Cache invalidation: bilinen 2 zor problem ve pragmatik çözümleri

Phil Karlton'ın ünlü lafı: "Computer science'ta 2 zor problem var: cache invalidation ve naming things." İlki için somut çözümler.

“Computer science’ta sadece 2 zor problem var: cache invalidation, naming things ve off-by-one errors.” Phil Karlton’ın bu şakasının ciddi yanı var. Cache invalidation gerçekten zor.

Son 10+ yılda onlarca projede cache katmanları kurdum. İyi haber: bu zor problem için pratik çözüm patern’leri var. Kötü haber: her biri trade-off’larla geliyor, sihirli çözüm yok.

İki ana zorluk

Cache invalidation’da iki temel problem var:

1. Stale data: Cache eski veriyi tutuyor, kullanıcı yanlış sonuç görüyor. “Ürün fiyatını indirdim ama kullanıcılar eski fiyatı görüyor.”

2. Cache stampede: Cache expire olduğu an 1000 request aynı anda origin’e gidiyor. Origin çöküyor.

Bu ikisi genelde farklı stratejilerle çözülüyor. İkisini birden doğru handle etmek kritik.

Stale data için stratejiler

Time-based (TTL)

En basit: cache entry’ye bir süre veriyorsun (60 saniye, 5 dakika, 1 saat). Süre dolunca invalidate oluyor.

Avantaj: basit implementation. Dezavantaj: TTL boyunca stale data gösteriliyor.

Ne zaman işe yarar: veri değişim frekansı düşük, stale data 1-2 dakika tolere edilebilir. Çoğu content site için yeterli.

Ne zaman yetmez: finansal veri, stock level, real-time metric. TTL boyunca yanlış bilgi verirsin.

Event-based invalidation

Veri değiştiğinde cache’i explicit olarak temizle:

updateProduct(productId, newData);
cache.delete(`product:${productId}`);
cache.delete(`products:list`); // Liste cache'i de invalidate

Avantaj: stale data problem’i yok. Dezavantaj: karmaşıklık yüksek. Hangi cache’lerin invalidate edileceğini her güncelleme için düşünmek lazım.

Ne zaman işe yarar: kritik veri, stale tolerance sıfır. E-ticaret fiyatı, stok durumu.

Karmaşıklığı azaltmak için cache key’lerine versioning ekleyebilirsiniz. Product güncellendiğinde product:123:v456product:123:v457 diye artıyor. Eski key’ler kendi TTL’leriyle ölüyor, yeni request yeni key’e gidiyor.

Write-through cache

Yazma işlemi hem DB’ye hem cache’e yazıyor:

db.save(data);
cache.set(key, data);

Bu sayede cache hep DB ile senkron. Ama ek complexity: cache yazma fail olursa ne olur? DB yazma fail olursa cache ne olur?

Genelde transactional olması kolay değil. “Mostly consistent” bir yaklaşım.

Write-around cache

Yazma doğrudan DB’ye gidiyor, cache atlanıyor. Okuma sırasında cache populate ediliyor.

Avantaj: ölçeklenebilir, basit. Dezavantaj: ilk okuma hep cache miss.

Ne zaman işe yarar: yazma sık ama okuma seyrek. Log data, analytics events.

Cache stampede problemi

Problem: 100K kullanıcılı bir site. /products/popular cache’e 5 dakika TTL ile konmuş. Dakika 0’da cache populate, dakika 5’te expire. Dakika 5’te 10K eşzamanlı request geliyor. Hepsi cache miss, hepsi DB’ye gidiyor. DB çöküyor.

Çözümler:

1. Probabilistic early expiration

Cache expire olmadan önce bazı request’lerin “early refresh” yapması:

function get(key) {
    const entry = cache.get(key);
    if (!entry) return refresh(key);
    
    // Expire'a ne kadar yakın?
    const ratio = entry.age / entry.ttl;
    if (ratio > 0.9 && Math.random() < 0.1) {
        // %10 ihtimalle early refresh yap
        refresh(key); // async, bu request eski data dönecek
    }
    return entry.data;
}

Bu sayede expire anında herkes birden miss almıyor. Bazı request’ler önceden refresh yapmış oluyor.

2. Lock-based refresh (single-flight)

Cache miss olduğunda, ilk request DB’ye gidiyor. Diğer istekler “fetch in progress” lock’ta bekliyor. İlk request bitince hepsi aynı sonucu alıyor:

function get(key) {
    const cached = cache.get(key);
    if (cached) return cached;
    
    // Lock al
    if (!locks.has(key)) {
        locks.set(key, fetchFromDB(key).then(data => {
            cache.set(key, data);
            locks.delete(key);
            return data;
        }));
    }
    return locks.get(key);
}

Redis’te bunu SETNX ile implement edebilirsiniz. Distributed sistemde çalışıyor.

3. Stale-while-revalidate

HTTP cache-control header’dan esinlendim. Cache expire olsa bile eski data’yı dön, arka planda refresh et:

function get(key) {
    const entry = cache.get(key);
    if (!entry) return fetchAndStore(key);
    
    if (entry.age > entry.ttl) {
        // Stale ama kullan
        refresh(key); // Async refresh
        return entry.data;
    }
    return entry.data;
}

Kullanıcı stale data görse bile hiç beklemiyor. Arka planda fresh data geliyor, bir sonraki request onu alıyor.

Ne zaman kullanabilirsin: stale tolerance birkaç saniye/dakika tolere edilebilir. News feed, recommendation, trending items.

Pratik mimarı

Benim projelerde genelde şu katmanlar var:

L1: Application memory cache. Aynı process’te. Millisecond latency. TTL 10-60 saniye. Single-instance app’ler için yeter.

L2: Redis cache. Distributed. 1-5ms latency. TTL dakikalar ila saatler. Multiple app instance’larının paylaştığı cache.

L3: CDN cache (HTTP level). Saniyeler ila günler TTL. Static veya semi-static content.

L4: DB materialized view. Özellikle analytics/raporlama için. Gece refresh oluyor.

Her katman farklı TTL, farklı invalidation stratejisi. L1 short TTL + auto-refresh, L2 event-based invalidation, L3 TTL + purge API.

Cache key tasarımı

Key tasarımı invalidation’ı belirliyor:

Versioned keys: product:123:v5. Her update versiyon artıyor, eski key’ler self-destruct.

Hierarchical keys: products:cat:electronics:page:1. Bir kategori değişirse, o prefix’li tüm key’leri temizle (Redis SCAN ile).

User-scoped keys: user:456:cart. Kullanıcı bazlı cache, kullanıcı logout olduğunda toplu temizle.

Composite keys: product:123:user:456. Kişiselleştirilmiş veri. Invalidation karmaşık.

Monitoring

Cache monitor etmen gereken metrics:

  • Hit rate: %80 altıysa cache strategy’de sorun var
  • Miss rate per key pattern: Hangi key’ler genelde miss?
  • Staleness duration: Kullanıcı ne kadar stale data gördü?
  • Invalidation frequency: Çok sık invalidate ediliyorsa cache anlamsızlaşıyor
  • Stampede event’leri: Aynı key için concurrent miss var mı?

Sonuç

Cache invalidation sihir değil, disiplin. Doğru TTL, doğru invalidation trigger, stampede koruma, key tasarımı. Bu 4 axisi düşünerek tasarlarsan stale data ve overload sorunlarını minimize edersin.

Karmaşık bir uygulamanın üstüne cache eklemek genelde 1-2 haftalık iş. Değeri var mı? Database load %70+ düşüyor, latency dramatic şekilde iyileşiyor. Değer kesinlikle var, ama investment yapmadan önce metrik’lerini bilmeni öneririm.

Bu konuda bir projeniz mi var?

Kısa bir özet bırakın, 24 saat içinde size dönüş yapayım.

İletişime Geç