İçeriğe geç

Spring Cache — @Cacheable Kimin Cevabını Hatırlıyor?

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

Önce şunu oku: AOP, Proxy ve Self-Invocation Tuzağı , Transaction Propagation ve Rollback Yayılımı

30 saniyede özet

@Cacheable bir sonucu bir anahtarla saklar ve bir dahakine metodu hiç çalıştırmadan onu verir. Anahtar, sonucu değiştiren her şeyi içermeli; birden çok sunucu varsa her birinin defteri ayrıdır.

Fiyatları güncelledin ve iki şikâyet geldi. Biri sepette eski fiyatı görmüş, diğeri dolar fiyatı yerine lira fiyatı. İkisi de aynı @Cacheable satırından çıktı.

Deftere sorunun tamamı mı yazılıyor, yarısı mı?
Adım adım oku
  1. Biri 7 numaralı ürünün fiyatını dolar olarak soruyor.
  2. Cache anahtarı yalnızca ürün numarasından oluşuyor; cevap deftere ürün 7 satırına yazılıyor.
  3. Lira olarak soran ikinci kişi de aynı satırı buluyor ve dolar fiyatını alıyor.
  4. Anahtar sonucu değiştiren her girdiyi içermeli: ürün ve para birimi birlikte. O zaman her soru kendi satırını bulur.
  1. Bayt: Fiyatları güncelledim ve iki şikâyet geldi!

  2. Sen: Ne şikâyeti?

  3. Bayt: Biri eski fiyatı görmüş, öteki dolar yerine lira. İkisi de aynı @Cacheable satırından!

  4. Bayt: Garsonun defterine ne yazıldığına bakalım: sorunun tamamı mı, yarısı mı?

Cache aslında bir defter

@EnableCaching açıkken Spring, @Cacheable metodunun önüne bir aracı (proxy) koyar. Aracı gelen soruya bir anahtar (key) verir ve deftere bakar: cevap yazılıysa metodu hiç çağırmaz, yazılı değilse çağırır ve cevabı deftere yazar.

Varsayılan anahtar metodun bütün parametreleridir. key = "#productId" yazdığın an bu güvence gider: anahtarda olmayan her parametre, farklı sorulara aynı cevabın dönmesine yol açar (dolar fiyatı sorana lira fiyatı gibi).

Aracı yalnızca dışarıdan gelen çağrıları gördüğü için this.find(id) gibi iç çağrılar deftere hiç uğramaz.

Hiç ayar yapmazsan· istersen atla

Projede Caffeine ya da Redis yoksa Boot, süresi ve boyut sınırı olmayan basit bir ConcurrentHashMap kurar. Deneme için yeter, canlı sistemde bellek dolana kadar büyür.

Kafam karıştı, daha basit anlat

Cache bir not defteridir: aynı soru gelince cevabı yeniden hesaplamaz, deftere bakar. Sorunun bir kısmını not almayı unutursan, farklı sorulara aynı cevabı verir.

Hızlı kontrolBaşlangıç

Spring Boot uygulamasında @EnableCaching açtın, ama classpath'te Caffeine ya da Redis yok. @Cacheable değerleri nerede ve ne kadar süre tutulur?

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

@Cacheable(value = "prices", key = "#productId") ile işaretli getPrice(long productId, String currency) metodu var. Ne olur?

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

Defter nasıl güncellenir?

İki yol var. 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 → girdiyi belirli bir süre sonra kendiliğinden siler; o süre boyunca eski veri görülebilir. @CacheEvict ise güncelleme anında girdiyi siler, sonraki okuma veritabanına gider.

İki pod, her birinde yerel Caffeine cache. Fiyat güncellemesi @CacheEvict taşıyor ve istek Pod A'ya düştü. Pod B ne gösterir? Cevabı göster

Eski fiyatı, TTL dolana kadar. Yerel cache her JVM’de ayrı bir kopyadır. Evict yalnızca çağrının düştüğü pod’un kopyasını siler; Spring diğer pod’lara haber vermez.

Time
Süre: girdiye verilen ömür.
To
...boyunca
Live
Yaşa: süre dolunca girdi kendiliğinden silinir.
Kafam karıştı, daha basit anlat

Her sunucunun kendi defteri var. Birinin defterinden bir satırı silmek, ötekilerin defterini değiştirmez. Onlar eski cevabı süre dolana kadar vermeye devam eder.

Hızlı kontrolOrta

Üç pod, her birinde Caffeine ile yerel cache. Fiyat güncelleme uç noktası @CacheEvict taşıyor ve istek Pod 1'e düştü. Kullanıcılar ne görür?

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

@CacheEvict ile @CachePut arasındaki fark nedir?

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

Kendin gör

Cache kimin fiyatını hatırlıyor?

Tohum 548711
  • Pod A cache

    boş

  • Pod B cache

    boş

Oynat ya da adımla.

Hız
Adım 0

Şu an ne oldu?

Her pod kendi cache'i (Caffeine)

İki pod aynı fiyatı okuyor, sonra yönetici fiyatı değiştiriyor. Cache değişikliği duyacak mı?

Görevler0/3

  • Dolar isteyene lira fiyatı döndüraçık

    İpucu

    Key'den bir parametre eksik olsun.

  • Evict çalışsın, ama bir pod yine eski fiyatı görsünaçık

    İpucu

    Cache nerede duruyor?

  • Her okuma doğru fiyatı görsünaçık

    İpucu

    Doğru key, ortak cache, güncellemede evict.

Olay günlüğü (0)

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

  1. Varsayılanla oynat. Evict yok: güncellemeden sonra iki pod da eski fiyatta.
  2. “@CacheEvict” seç. Pod A düzeldi, Pod B hâlâ eski.
  3. Cache’i “Ortak cache (Redis)” yap. İki pod da yeni fiyatı görüyor.
  4. Key’i #productId yap. Dolar isteyen lira fiyatı aldı.
Hızlı kontrolOrta

Ürün sayfası her istekte veritabanına gidiyor; cache hiç dolmuyor gibi. @EnableCaching açık. Hatalı satır hangisi?

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

Hatalı satıra dokun, sonra kontrol et.

ProductService.java
Java 21UTF-8LF

Her veriyi en uygun cache yaklaşımına göre ayır.

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

Sınıflandırılmamış

Yerel cache, TTL yeterli

Biraz eskime sorun değil, çok sık okunuyor.

    Ortak cache + evict

    Değişince her pod hemen görmeli.

      Cache'leme

      Her okuma güncel olmalı ya da kişiye özel ve seyrek.

        Tuzaklar

        Kayıttan önce silmek. @Transactional metottaki evict, değişiklik veritabanına kaydedilmeden önce çalışır. Arada gelen bir okuma eski değeri yeniden deftere yazar; cache manager’ı transaction-aware yaparak silmeyi kayıttan sonraya al.

        Değiştirilebilir değerler. Yerel cache nesnenin kendisini döndürür. Dönen nesneyi değiştiren kod, cache’i de değiştirir; cache’lenen tipleri record yap.

        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 → (izdiham). Çok sorulan bir cevap defterden silindiği anda yüzlerce istek aynı sorguyu aynı anda çalıştırır. @Cacheable(sync = true) bunu tek bir sunucu içinde tek sorguya indirir.

        Karar anında cache. Ödeme anındaki stok ya da bakiye kontrolü cache’ten okunmaz. Cache’i, biraz eskimesi zarar vermeyen okumalara sakla.

        Hızlı kontrolİleri

        Popüler bir ürünün cache girdisinin TTL'i doldu ve aynı anda 500 istek geldi. Veritabanı CPU'su sıçradı. Bu neyin belirtisi ve Spring'de ilk yardım nedir?

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

        Aşağıdaki örnek bir bankanın döviz kuru servisinden ve dersteki kararları birlikte uyguluyor: ortak cache, sonucu değiştiren her parametreyi içeren key, commit’ten sonra evict, stampede koruması ve değiştirilemez değerler.

        Derinleş · Beş pod'lu döviz kuru servisi: uçtan uca 6 dosya · ~118 satır · ilk okumada atlayabilirsin
        Proje dosyaları

        src/main/java/com/bank/fx/ CacheConfig.java Redis'te cache başına TTL; kurlar kısa yaşar. transactionAware: put ve evict commit'ten sonra çalışır. Redis düşerse servis cache'siz devam eder.

        src/main/java/com/bank/fx/CacheConfig.java
        @Configuration
        @EnableCaching
        class CacheConfig implements CachingConfigurer {
        @Bean
        RedisCacheManager cacheManager(RedisConnectionFactory connections) {
        var json = RedisSerializationContext.SerializationPair
        .fromSerializer(new GenericJackson2JsonRedisSerializer());
        var defaults = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(10))
        .disableCachingNullValues()
        .serializeValuesWith(json);
        return RedisCacheManager.builder(connections)
        .cacheDefaults(defaults)
        // A displayed rate must never be far behind the treasury desk,
        // even if an evict is ever missed.
        .withCacheConfiguration("fxRates", defaults.entryTtl(Duration.ofSeconds(15)))
        // Defer put/evict until the surrounding transaction commits.
        .transactionAware()
        .build();
        }
        // Redis down: log it and fall through to the database instead of failing the request.
        @Override
        public CacheErrorHandler errorHandler() {
        return new LoggingCacheErrorHandler();
        }
        }

        src/main/java/com/bank/fx/ FxRateBoard.java Okuma yolu. Key döviz çiftini ve kanalı içeriyor (mobil ile şubenin makası farklı); sync=true aynı key'i tek thread'e hesaplatır.

        src/main/java/com/bank/fx/FxRateBoard.java
        @Service
        class FxRateBoard {
        private final FxRateRepository rates;
        FxRateBoard(FxRateRepository rates) {
        this.rates = rates;
        }
        // Channel is in the key because the spread differs: forget it and mobile
        // customers see the branch's rate, or the other way round.
        @Cacheable(cacheNames = "fxRates", key = "#pair + ':' + #channel", sync = true)
        public FxRateView rateOf(CurrencyPair pair, Channel channel) {
        FxRate rate = rates.findCurrent(pair).orElseThrow(() -> new RateNotFoundException(pair));
        return FxRateView.of(rate, channel);
        }
        }

        src/main/java/com/bank/fx/ TreasuryRateService.java Yazma yolu: hazine masası kuru günceller. Evict commit'ten sonra gider; arada okuyan eski kuru yeniden cache'leyemez.

        src/main/java/com/bank/fx/TreasuryRateService.java
        @Service
        class TreasuryRateService {
        private final FxRateRepository rates;
        TreasuryRateService(FxRateRepository rates) {
        this.rates = rates;
        }
        // Transaction-aware cache manager: these evicts run after COMMIT, not when the
        // method returns, so a concurrent reader cannot re-cache the old rate.
        @Transactional
        @Caching(evict = {
        @CacheEvict(cacheNames = "fxRates", key = "#pair + ':MOBILE'"),
        @CacheEvict(cacheNames = "fxRates", key = "#pair + ':BRANCH'")})
        public void publish(CurrencyPair pair, BigDecimal mid) {
        rates.findCurrent(pair).orElseThrow().retire(); // history is kept, never overwritten
        rates.save(FxRate.published(pair, mid));
        }
        }

        src/main/java/com/bank/fx/ FxRateView.java Cache'lenen değer: değiştirilemez bir record, JSON olarak Redis'e yazılır.

        src/main/java/com/bank/fx/FxRateView.java
        // Immutable on purpose: a local cache would hand the same instance to every caller.
        public record FxRateView(String pair, String channel, BigDecimal buy, BigDecimal sell, Instant publishedAt) {
        static FxRateView of(FxRate rate, Channel channel) {
        BigDecimal spread = channel.spread(); // e.g. wider at the branch
        return new FxRateView(rate.getPair().code(), channel.name(),
        rate.getMid().subtract(spread).setScale(4, RoundingMode.DOWN),
        rate.getMid().add(spread).setScale(4, RoundingMode.UP),
        rate.getPublishedAt());
        }
        }

        src/main/java/com/bank/fx/ FxController.java Ekranda gösterilen kur cache'ten gelir; işlem yapılacak kur ise süreli bir teklifle kilitlenir.

        src/main/java/com/bank/fx/FxController.java
        @RestController
        @RequestMapping("/fx")
        class FxController {
        private final FxRateBoard board;
        private final FxQuotes quotes;
        FxController(FxRateBoard board, FxQuotes quotes) {
        this.board = board;
        this.quotes = quotes;
        }
        // The rate board: a few seconds stale is fine, so it comes from the cache.
        @GetMapping("/rates/{pair}")
        FxRateView rate(@PathVariable CurrencyPair pair, @RequestParam Channel channel) {
        return board.rateOf(pair, channel); // a separate bean: the call goes through the proxy
        }
        // Before buying, the customer gets a quote locked for a short time. The exchange
        // executes at the quote's rate, never at whatever the cache holds by then.
        @PostMapping("/quotes")
        FxQuote quote(@RequestBody @Valid QuoteRequest body) {
        return quotes.lock(body.pair(), body.channel(), body.amount());
        }
        }

        src/main/resources/ application.yml Cache ve Redis ayarları. Metrikler için cache adları baştan tanımlı.

        src/main/resources/application.yml
        spring:
        cache:
        type: redis
        cache-names: fxRates # created up front, so they appear in metrics immediately
        data:
        redis:
        host: redis
        port: 6379
        timeout: 200ms # a slow cache must not become a slow rate board
        management:
        endpoints:
        web:
        exposure:
        include: health,metrics,caches

        Kendini sına

        Şimşek turu1/5

        @Cacheable, cevap defterde varsa metodu hiç çalıştırmaz.

        Soru 1/2İleri

        @Transactional updatePrice metodu @CacheEvict taşıyor. Nadiren, güncellemeden sonra cache'te yine eski fiyat kalıyor ve TTL dolana kadar düzelmiyor. Neden?

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

        Aklında kalacak üç şey

        1. 1 Cache anahtarı, metodun sonucunu değiştiren her şeyi içermelidir. Varsayılan anahtar bütün parametreleri kullanır; elle yazılan anahtar bu güvenceyi kaldırır.
        2. 2 Yerel cache her sunucuda ayrı bir kopyadır. @CacheEvict yalnızca çağrının düştüğü sunucunun kopyasını siler.
        3. 3 TTL, verinin en fazla ne kadar eski olabileceğini söyler. Değişikliğin hemen görünmesi gerekiyorsa evict gerekir ve evict commit'ten sonra çalışmalıdır.
        Sonraki kapı Aynı ayar üç farklı yerde yazıyor. Hangisi kazanır? Konfigürasyon ve Profiller — Bu Değer Nereden Geldi? · 8 dk

        5 kart sonraki derste seni bekliyor

        0/5 kart bu dersten toplandı

        Bu dersin üstüne kurulanlar

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