in

Redis Cache Stratejileri: Cache-Aside, Write-Through, Evict

Önbellek, kurumsal uygulamada en hızlı performans kazancı ve en sık görülen 'eski veri' hatasının kaynağıdır. Cache-aside, read/write-through ve write-behind desenlerini karşılaştırıyor; Spring Cache ve Redis ile kurulumu, önbellek başına TTL, invalidation stratejilerini, stampede ve tutarlılık tuzaklarını ve Redis operasyon ayarlarını anlatıyoruz.

Uygulama, Redis önbelleği ve veritabanı arasındaki cache-aside okuma ve yazma akışı

Önbellek, kurumsal uygulamada en hızlı performans kazancıdır: fiyat listesi sorgusu 400 ms'den 2 ms'ye iner, veritabanı yükü yarıya düşer. Aynı önbellek, üretimdeki en sinir bozucu hata sınıfının da kaynağıdır: "fiyatı güncelledik ama ekranda eskisi görünüyor", "iki sunucu farklı stok gösteriyor", "Redis düştü, uygulama da düştü". Önbellek eklemek kolay, doğru geçersiz kılmak zordur.

Bu yazıda neyin önbelleğe alınacağını, cache-aside, read/write-through ve write-behind desenlerini, Spring Cache ile Redis kurulumunu ve önbellek başına TTL'i, invalidation stratejilerini, stampede ve tutarlılık tuzaklarını ve Redis'in operasyon ayarlarını anlatıyoruz.

Ne önbelleğe alınır, ne alınmaz?

Üç ölçüt birlikte sağlanıyorsa önbellek adayıdır: çok okunuyor, üretmesi pahalı (çok tablolu sorgu, dış API, hesaplama) ve bir süre eski olması kabul edilebilir. Fiyat listesi, ürün kataloğu, parametre tabloları, kur, yetki matrisi, oturum verisi tipik adaylardır. Alınmaması gerekenler: tek kullanıcıya özel ve gizli veri (önbellek anahtarında kullanıcı yoksa sızar), saniyede değişen veri (stok rezervasyonu), zaten hızlı olan sorgular (birincil anahtarla tek satır) ve doğruluğu kritik bakiye/limit değerleri.

Desenler

Desen

Nasıl çalışır

Tutarlılık

Karmaşıklık

Uygun

Cache-aside (lazy)

Uygulama önce önbelleğe bakar; yoksa DB'den okur ve önbelleğe yazar

TTL ve açık evict'e bağlı

Düşük

Varsayılan; okuma ağırlıklı her senaryo

Read-through

Önbellek katmanı eksikte DB'yi kendisi okur

Cache-aside ile aynı

Düşük (Spring Cache bunu soyutlar)

Cache-aside'ın anotasyonla uygulanmış hâli

Write-through

Yazma önce DB'ye sonra önbelleğe, aynı işlemde

Yüksek; önbellek her zaman güncel

Orta

Yazma sonrası hemen okunan veri

Write-behind

Yazma önbelleğe; DB'ye toplu ve gecikmeli

Düşük; önbellek düşerse veri kaybı

Yüksek

Sayaçlar, günlükler, kayıp tolere edilebilir yazmalar

Refresh-ahead

TTL dolmadan arka planda yenilenir

Yüksek

Orta

Az sayıda sıcak anahtar (ana sayfa, kur)

Kurumsal uygulamaların %90'ı için doğru cevap: cache-aside + yazmada açık evict + güvenlik ağı olarak TTL. Write-behind yalnızca veri kaybının kabul edildiği yerde.

Spring Cache ile Redis kurulumu

@Configuration
@EnableCaching
public class OnbellekAyari {

    @Bean
    RedisCacheManagerBuilderCustomizer onbellekler() {
        var json = RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());
        var varsayilan = RedisCacheConfiguration.defaultCacheConfig()
            .serializeValuesWith(json)
            .disableCachingNullValues()
            .prefixCacheNameWith("app:v3:");                       // sürüm öneki: şema değişince toplu geçersiz kılma
        return b -> b
            .cacheDefaults(varsayilan.entryTtl(Duration.ofMinutes(10)))
            .withCacheConfiguration("fiyatListesi", varsayilan.entryTtl(Duration.ofMinutes(30)))
            .withCacheConfiguration("kur",          varsayilan.entryTtl(Duration.ofMinutes(5)))
            .withCacheConfiguration("yetkiler",     varsayilan.entryTtl(Duration.ofMinutes(2)));
    }
}
@Service
public class FiyatServisi {

    @Cacheable(cacheNames = "fiyatListesi", key = "#kiraci + ':' + #listeId", sync = true)   // sync: aynı örnekte tek hesaplama
    public FiyatListesi getir(String kiraci, Long listeId) {
        return repo.yukle(kiraci, listeId);                       // pahalı sorgu
    }

    @Transactional
    @CacheEvict(cacheNames = "fiyatListesi", key = "#kiraci + ':' + #liste.id()")
    public void guncelle(String kiraci, FiyatListesi liste) {
        repo.kaydet(kiraci, liste);
    }
}

Üç ayrıntı sonucu belirler. Serileştirme: JDK serileştirme sınıf değişince bozulur; JSON kullanın ve önbelleğe koyduğunuz nesneleri entity değil DTO/record yapın. Anahtar tasarımı: anahtar, sonucu belirleyen her parametreyi içerir (kiracı, dil, kullanıcı rolü); eksik parametre veri sızıntısı ya da yanlış sonuçtur. Sürüm öneki: önbellek şeması değişince eski değerleri tek tek silmek yerine önek v3 → v4 yapılır; eskiler TTL ile ölür.

Invalidation: dört strateji, birlikte

  1. TTL — güvenlik ağı. Her anahtarın süresi dolar; en kötü durumda veri en fazla TTL kadar eskidir. TTL'i iş toleransına göre seçin: kur 5 dk, katalog 30 dk, parametreler 2 saat. Toplu süre dolumunu önlemek için TTL'e küçük rastgele sapma (jitter) ekleyin.

  2. Yazmada açık evict. Veriyi değiştiren her yol ilgili anahtarı siler (@CacheEvict). Kritik ayrıntı: evict transaction commit'ten sonra yapılmalı; commit'ten önce silinirse başka bir istek eski veriyi okuyup önbelleği yeniden doldurur. Spring Cache'in @CacheEvict'i varsayılan olarak metot dönüşünde çalışır; transaction'lı servislerde TransactionAwareCacheManagerProxy ya da TransactionSynchronization.afterCommit ile commit sonrasına alınır.

  3. Olay tabanlı geçersiz kılma. Veri başka bir sistemden (ERP, başka modül) değişiyorsa evict oradan gelen olayla tetiklenir: Kafka'daki UrunGuncellendi olayı → ilgili anahtarlar silinir. Outbox ile olayın kaybolmaması garanti edilir.

  4. Ad alanı sürümü. "Tüm fiyat listesi önbelleğini boşalt" için binlerce anahtarı KEYS/SCAN ile silmek yerine ad alanı sürüm numarası artırılır; yeni okumalar yeni ad alanına gider.

Tutarlılık ve stampede tuzakları

  • Cache-aside yarışı: A isteği DB'den eski değeri okur, B isteği günceller ve evict eder, A eski değeri önbelleğe yazar → TTL boyunca eski veri. Çözüm: commit sonrası evict + kısa TTL; sıfır tolerans gereken veride önbellek kullanmayın.

  • Stampede (dogpile): Sıcak anahtarın süresi dolar, bin istek aynı anda DB'ye gider. Tek örnekte sync = true; çoklu örnekte Redis kilidi (SET anahtar:lock NX EX 5) alan tek örnek hesaplar, diğerleri kısa süre bekler ya da eski değeri kullanır (stale-while-revalidate).

  • Null önbellekleme: Var olmayan ürün için DB'ye her seferinde gitmek (cache penetration) DB'yi yorar; kısa TTL ile "yok" değerini önbelleklemek çözer, ama disableCachingNullValues ile çelişir — bilinçli seçin.

  • Yerel + Redis (iki katman): Caffeine önbelleği Redis gidiş-dönüşünü de kaldırır; ama diğer örneklerin yerel önbelleğini geçersiz kılmak için Redis pub/sub ile evict mesajı yayınlanmalıdır. Bu olmadan iki sunucu farklı veri gösterir.

Redis'i önbellek olarak işletmek

  • Bellek politikası: maxmemory tanımlı ve maxmemory-policy allkeys-lru (ya da allkeys-lfu); önbellek Redis'i dolunca en az kullanılanı atmalı, hata vermemeli.

  • Kalıcılık: Saf önbellek için RDB/AOF kapalı olabilir; ama oturum gibi kaybı can yakan veri aynı Redis'te tutulmamalı — ayrı örnek.

  • Zaman aşımı ve fallback: Redis'e bağlantı ve komut zaman aşımı kısa (50–200 ms); Redis erişilemezse istek DB'den devam etmeli, hata vermemeli (CacheErrorHandler ile hataları yut, logla, metrik at). "Redis düştü, uygulama düştü" bu ayar eksikliğidir.

  • Anahtar hijyeni: uygulama:sürüm:önbellek:anahtar biçimi; büyük değerler (1 MB+) sıkıştırılmalı ya da parçalanmalı; sıcak anahtar (herkesin okuduğu tek anahtar) tek Redis düğümünü yorar, yerel katman gerektirir.

  • Metrikler: Micrometer cache.gets (hit/miss) ile isabet oranı; %80'in altındaki isabet, ya TTL'in kısa ya anahtarın yanlış olduğunu söyler. Redis tarafında bellek, evicted_keys ve gecikme.

Sahada sık yapılan hatalar

  1. Entity'yi önbelleğe koymak: Lazy ilişkiler serileştirmede patlar ya da tüm grafı sürükler; DTO kullanın.

  2. TTL'siz anahtar: Evict unutulduğunda sonsuza kadar eski veri; her anahtarın TTL'i olmalı.

  3. Anahtarda kiracı/dil eksikliği: A kiracısı B'nin fiyatını görür.

  4. Commit öncesi evict: Yarış, eski veriyi geri koyar.

  5. KEYS * ile toplu silme: Üretim Redis'ini kilitler; sürümlü ad alanı kullanın.

  6. Önbelleği doğruluk mekanizması sanmak: Önbellek bir iyimserliktir; iş kuralı doğrulaması her zaman DB'den yapılır (limit kontrolü, stok düşümü).

Sık sorulan sorular

Hibernate ikinci seviye önbelleği mi, Spring Cache mi?

İkisi farklı seviyedir: L2 önbellek entity/collection düzeyinde ve Hibernate tarafından yönetilir; Spring Cache servis metodu sonuçlarını (DTO, hesaplama) önbelleğe alır. Çoğu uygulama için Spring Cache daha öngörülebilirdir; L2 yalnızca ölçümle kanıtlanmış darboğazlarda.

Önbellek ısıtma (warm-up) gerekli mi?

Dağıtım sonrası soğuk önbellek DB'ye ani yük bindiriyorsa evet: başlangıçta en sıcak birkaç yüz anahtar arka planda doldurulur. Çoğu uygulamada kademeli dağıtım (rolling) bunu gereksiz kılar.

Redis Cluster ne zaman gerekir?

Tek örneğin belleği ya da işlem hızı yetmediğinde; yüksek erişilebilirlik için önce Sentinel/replika, veri hacmi için sonra Cluster. Çoğu kurumsal önbellek tek yönetilen örnekle yıllarca çalışır.

Sonuç

Doğru önbellek: cache-aside, sonucu belirleyen her parametreyi içeren sürümlü anahtar, önbellek başına TTL, commit sonrası evict, dış değişikliklerde olay tabanlı geçersiz kılma, stampede koruması ve Redis düştüğünde DB'ye düşen bir fallback. Bu altı kural, önbelleğin kazancını "eski veri" hatasına dönüştürmeden korur.

Uygulamanızın performans darboğazlarını ölçüp doğru önbellek katmanını kurmak için bakım ve destek ile web ve e-ticaret geliştirme hizmetlerimize göz atabilirsiniz.