İçeriğe geç

Cache Stratejileri — Defter Ne Zaman Yalan Söyler?

Orta 10 dk Çok sık karşılaşılır

Önce şunu oku: Spring Cache — @Cacheable Kimin Cevabını Hatırlıyor?

30 saniyede özet

Cache, uzaktaki veritabanının yanında duran hızlı bir defterdir. Okurken önce deftere bakılır, yazarken defterdeki not güncellenmez, silinir. Yine de eski bir not araya sızabilir; TTL onun ne kadar yaşayacağını sınırlar.

Mahalle bakkalı her fiyat sorusunda arkadaki depoya yürümez. Tezgâhta bir defter tutar: bir kez baktığı fiyatı oraya yazar, sonra defterden söyler.

Defter hızlıdır, ama asıl bilgi depodadır.
Adım adım oku
  1. Müşteri çayın fiyatını sorar; defterde yok, bakkal depoya gider ve fiyatı deftere yazar.
  2. Sonraki müşteriye fiyat defterden, hemen söylenir.
  3. Depoda fiyat değişince defterdeki not silinir.
  4. Yeni soru defterde bir şey bulamaz, depodan yeni fiyatı getirir ve deftere yazar.
  1. Bayt: Müşteri kasada şikâyet ediyor: ürün sayfası bir fiyat söylüyor, sepet başka!

  2. Sen: Fiyatı değiştirince cache'i de güncellemiyor muyduk?

  3. Bayt: Güncelliyoruz. Demek ki bir yerde sıra karışıyor...

  4. Bayt: Defter ne zaman yalan söyler, önce onu anlayalım.

Cache-aside: önce deftere bak

cache-asideUygulamanın önce cache'e baktığı, bulamazsa veritabanından okuyup cache'e koyduğu düzen. Cache veritabanının yanında durur; düşerse uygulama veritabanından devam eder.Sözlükte gör → düzeninde uygulama önce cache’e bakar. Bulursa (hit) hemen döner; bulamazsa (miss) veritabanından okur, cache’e koyar ve döner.

oku: cache.get(key) ──bulundu──→ döndür
└─yok──→ db.read(key) → cache.set(key, değer, TTL) → döndür

Cache her şeye değmez. Çok okunan, az değişen ve biraz eski görünmesi dert olmayan veri iyi adaydır: ürün sayfası, kategori listesi, döviz kuru ekranı. Ödemede kullanılacak fiyat ise veritabanından okunur.

Kafam karıştı, daha basit anlat

Önce deftere bak. Yoksa depoya git, fiyatı deftere yaz. Defter hızlıdır ama bazen geride kalır.

Hızlı kontrolBaşlangıç

Cache-aside düzeninde cache'i kim doldurur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Yazınca defter ne olacak?

Veri değişince cache’e ne olacağına dair birkaç düzen var. Write-through: her yazma hem veritabanına hem cache’e aynı anda gider. write-behindYazmanın önce cache'e yapılıp veritabanına sonradan toplu aktarıldığı düzen. Yazma hızlanır; cache düşerse henüz aktarılmamış değişiklikler kaybolabilir.Sözlükte gör →: yazma önce cache’e gider, veritabanına sonra toplu yazılır; hızlıdır ama cache düşerse yazma kaybolabilir.

Cache-aside kullanan çoğu sistem ise yazarken ya cache’teki değeri günceller ya da girdiyi siler. İkisi aynı görünür.

Fiyat 100. Ayşe 120 yazıyor, Mert hemen ardından 130. İkisi de commit'ten sonra cache.set yapıyor, ama Ayşe'ninki ağda gecikip en son varıyor. Ekran ne gösterir? Cevabı göster

120, veritabanında 130 varken. Cache’e yazmalar veritabanına yazmaların tersi sırayla vardı ve son gelen eski değeri taşıyordu. Fiyat bir daha değişmezse cache hep yanlış kalır.

Girdiyi silmek bu yarışı kapatır. İki silme hangi sırayla gelirse gelsin sonuç aynıdır: girdi yoktur ve sonraki okuma veritabanından taze değeri getirir.

Kafam karıştı, daha basit anlat

Deftere yeni fiyatı yazmak yerine eski notu sil. Silmek hangi sırayla gelirse gelsin aynı sonucu verir.

Hızlı kontrolOrta

Write-behind (write-back) düzeninin en büyük riski hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Veri değişince cache'teki değeri güncellemek yerine girdiyi silmek neden daha güvenli?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Satır satır: yavaş okuyan

Eski fiyat cache'e nasıl geri girer?

PriceReads.java
1Price find(String sku) {
şu an çalışan satır Price cached = cache.get(sku);
3 if (cached != null) return cached;
4
5 Price fresh = db.findPrice(sku);
6 cache.set(sku, fresh, TTL);
7 return fresh;
8}
9
10void change(String sku, Price price) {
11 db.updatePrice(sku, price); // commits
12 cache.delete(sku);
13}

Debug

Adım 1/6

okuyan Girdinin süresi az önce dolmuş. Cache boş: miss.

cache
= yok
db
= 100
Java 21UTF-8LF2:1

Sol/sağ ok tuşlarıyla da gezebilirsin.

Bu yarış nadirdir: okuyanın veritabanı okuması ile cache’e yazması arasındaki boşluk, yazanın bütün işinden uzun sürmelidir. Nadir ama imkânsız değil.

Bu yüzden her girdiye bir TTLTime to live: bir cache girdisinin ne kadar süre geçerli sayılacağı. Süre dolunca girdi silinir ve sonraki okuma kaynağa gider; o süre boyunca eski veri görülebilir.Sözlükte gör → verilir. TTL yarışı önlemez; eski değerin en fazla ne kadar yaşayacağını söyler.

Hızlı kontrolOrta

Olaylar bu sırayla gerçekleşiyor. Ne yazdırılır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
TwoWriters.java
1Map<String, Integer> cache = new HashMap<>(Map.of("tea", 100));
2int db = 100;
3
4db = 120; // writer A commits
5db = 130; // writer B commits
6cache.put("tea", 130); // B updates the cache
7cache.put("tea", 120); // A's delayed update arrives
8
9System.out.println(cache.get("tea") + " / " + db);
Java 21UTF-8LF

Yazan commit'ten sonra cache'i siliyor. Yine de nadiren eski fiyat cache'e geri giriyor. Bu açığı sınırlamanın en basit yolu hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Başlangıçta fiyat hem veritabanında hem cache’te 100. Yazan fiyatı değiştirirken aynı anda başka bir şey oluyor.

Cache ne zaman yalan söyler?

Tohum 1

Başlangıç: veritabanında da cache'te de fiyat 100.

Oynat ya da adımla.

Hız
Adım 0

Şu an ne oldu?

Fiyat değişiyor, cache yanında duruyor

Veritabanında 100 yazıyor. Yazan fiyatı değiştirecek; cache sonunda hangi fiyatı gösterecek?

Görevler0/3

  • Cache'in veritabanından farklı bir fiyatı süresiz göstermesini sağlaaçık

    İpucu

    TTL kapalıyken iki yazanın cache'e değer koymasına izin ver.

  • İki yazan yarışırken cache doğru fiyatı göstersinaçık

    İpucu

    Yazan cache'e değer koymasın.

  • Yavaş okuyanın cache'e koyduğu eski fiyat kendiliğinden temizlensinaçık

    İpucu

    Hiçbir yazma stratejisi bunu önlemez. Girdiye bir ömür ver.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varsayılanı oynat. İki yazan, ikisi de cache’e değer koyuyor: cache 120, veritabanı 130. TTL yok, yanlışlık süresiz.
  2. Yazarken “DB’ye yaz, sonra cache’ten sil” seç. Aynı yarış, ama sonraki okuma 130’u getiriyor.
  3. “Yavaş bir okuyan araya giriyor” seç. Üç stratejinin üçünde de eski fiyat geri giriyor. TTL’i aç ve farkı gör.
Hızlı kontrolOrta

Deploy sonrası cache ısıtıldı: 50.000 girdi aynı dakikada, hepsi 10 dakikalık TTL'le yazıldı. Tam 10 dakika sonra veritabanı yükü sıçrıyor. En uygun düzeltme hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Tuzaklar

Hepsi aynı anda bitiyor. Deploy sonrası ısıtılan binlerce girdi aynı TTL’le yazılırsa aynı saniyede biter ve hepsi birden veritabanına gider. Süreye rastgele küçük bir pay eklemek bitişleri zamana yayar.

Çok okunan tek girdi bitiyor. O anda yüzlerce istek aynı sorguyu çalıştırır: cache stampedeSık okunan bir cache girdisi silindiği anda çok sayıda isteğin aynı değeri aynı anda kaynaktan hesaplamaya çalışması. Veritabanına ani bir yük bindirir.Sözlükte gör →. Çözüm, girdiyi tek bir isteğin yüklemesi ve diğerlerinin onu beklemesidir; Spring Cache dersindeki sync = true bunun bir örneği.

Olmayan anahtarlar. Var olmayan bir ürün hiç cache’e girmez ve her soru veritabanına gider. “Yok” cevabı da kısa bir süreyle saklanır.

Kafam karıştı, daha basit anlat

Notlar aynı anda silinmesin, çok sorulan notu tek kişi yenilesin, “böyle bir ürün yok” da deftere yazılsın.

Hızlı kontrolOrta

Her özellik hangi düzene ait?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Cache-aside

Okuyan doldurur

    Write-through

    Yazma ikisine birlikte gider

      Write-behind

      Önce cache, veritabanı sonra

        Derinleş · Ürün fiyatı: okuma, yazma ve commit sonrası silme 3 dosya · ~78 satır · ilk okumada atlayabilirsin
        Proje dosyaları

        src/main/java/com/shop/price/ PriceCache.java Okuma yolu: cache-aside. TTL'e yüzde ona kadar rastgele pay eklenir; olmayan ürünler de kısa bir süre 'yok' olarak saklanır.

        src/main/java/com/shop/price/PriceCache.java
        @Component
        class PriceCache {
        private static final Duration BASE_TTL = Duration.ofMinutes(10);
        private static final Duration MISSING_TTL = Duration.ofSeconds(30);
        private static final String MISSING = "-";
        private final StringRedisTemplate redis;
        private final PriceRepository prices;
        PriceCache(StringRedisTemplate redis, PriceRepository prices) {
        this.redis = redis;
        this.prices = prices;
        }
        Optional<BigDecimal> find(String sku) {
        String key = key(sku);
        String cached = redis.opsForValue().get(key);
        if (cached != null) {
        return MISSING.equals(cached) ? Optional.empty() : Optional.of(new BigDecimal(cached));
        }
        Optional<BigDecimal> fresh = prices.findPriceBySku(sku);
        // "No such product" is remembered briefly too, so unknown SKUs do not all reach the database.
        redis.opsForValue().set(key,
        fresh.map(BigDecimal::toPlainString).orElse(MISSING),
        fresh.isPresent() ? withJitter(BASE_TTL) : MISSING_TTL);
        return fresh;
        }
        void evict(String sku) {
        redis.delete(key(sku));
        }
        private static String key(String sku) {
        return "price:" + sku;
        }
        // Up to 10% extra, so entries written together do not all expire together.
        private static Duration withJitter(Duration base) {
        return base.plusMillis(ThreadLocalRandom.current().nextLong(base.toMillis() / 10 + 1));
        }
        }

        src/main/java/com/shop/price/ PriceService.java Yazma yolu: fiyat transaction içinde değişir ve bir olay yayınlanır. Cache burada silinmez; commit olmadan silinirse araya giren okuyan eski fiyatı geri koyar.

        src/main/java/com/shop/price/PriceService.java
        @Service
        class PriceService {
        private final PriceRepository prices;
        private final ApplicationEventPublisher events;
        PriceService(PriceRepository prices, ApplicationEventPublisher events) {
        this.prices = prices;
        this.events = events;
        }
        @Transactional
        public void change(String sku, BigDecimal price) {
        prices.updatePrice(sku, price);
        events.publishEvent(new PriceChanged(sku));
        }
        }
        record PriceChanged(String sku) {
        }

        src/main/java/com/shop/price/ PriceChangedListener.java Silme yalnızca commit'ten sonra çalışır. Bu andan sonra veritabanından okuyan herkes yeni fiyatı görür.

        src/main/java/com/shop/price/PriceChangedListener.java
        @Component
        class PriceChangedListener {
        private final PriceCache cache;
        PriceChangedListener(PriceCache cache) {
        this.cache = cache;
        }
        // Runs only after the commit: from here on the database can no longer hand out the old price.
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        void on(PriceChanged event) {
        cache.evict(event.sku());
        }
        }

        Kendini sına

        Önce hızlı bir ısınma: puan yok, kayıt yok. Sonra asıl sorular.

        Şimşek turu1/5

        Cache-aside'da cache'i okuyan taraf doldurur.

        Soru 1/3İleri

        Bu senaryoda hangi yolu seçersin?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        SenaryoBir e-ticaret sitesinde ürün sayfaları, fiyat değişikliklerinden binlerce kat fazla okunuyor. Kampanya saatlerinde veritabanı zorlanıyor. Ürün sayfasında birkaç saniyelik eski fiyat kabul edilebilir, ama ödeme adımında müşteri kesinlikle güncel fiyatı ödemeli.

        Aklında kalacak üç şey

        1. 1 Cache-aside'da okuyan önce cache'e bakar, yoksa veritabanından okuyup cache'e koyar. Cache'in bilgisi hep veritabanının bir kopyasıdır ve geride kalabilir.
        2. 2 Yazarken cache'teki değeri güncellemek, iki yazan yarışınca eski değeri kalıcı bırakabilir. Commit'ten sonra girdiyi silmek bu yarışı kapatır.
        3. 3 Hiçbir yazma stratejisi yavaş bir okuyanın eski değeri geri koymasını tamamen önlemez. TTL güvenlik ağıdır ve girdiler aynı anda bitmesin diye süreye rastgele bir pay eklenir.
        Sonraki kapı Üç sunucudan biri yarı hızda çalışıyor ama ayakta. Trafik ona gitmeye devam eder mi? Yük Dengeleme — Trafik Artınca Kapıya Kim Bakar? · 10 dk

        4 kart sonraki derste seni bekliyor

        0/4 kart bu dersten toplandı

        Bu dersin üstüne kurulanlar

        Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.