• 7.09.2026 13:04:09
  • Admin Admin

spring boot eğitimi kapsamında cache stampede sorununu Caffeine tekilleştirmesi, Redis lease kilidi, stale-while-revalidate ve Micrometer metrikleriyle ölçerek çözün. p99 gecikme ve veritabanı yükünü önce-sonra karşılaştırın.

Spring Boot Eğitimi: Cache Stampede için Caffeine ve Redis Kilidi

Java backend geliştirme içinde cache stampede'i teşhis etmek

Cache stampede, popüler bir anahtarın TTL'i dolduğunda çok sayıda isteğin aynı anda cache miss yaşayıp veritabanına veya uzak servise gitmesidir. java backend geliştirme uygulamasında bunu sadece HTTP p99 ile değil, üç sinyali birlikte izleyerek doğrulayın: cache miss sayısı, aynı anahtar için eşzamanlı yükleme sayısı ve PostgreSQL aktif bağlantı sayısı. Caffeine için recordStats() açın; Redis tarafında redis-cli --latency -h redis.internal ile sunucu gecikmesini ayrıca ölçün. Redis gecikmesi düşükken JDBC aktif bağlantısı kısa süreli olarak havuz üst sınırına dayanıyorsa, kök neden çoğunlukla cache miss patlamasıdır.

@Bean
AsyncLoadingCache<String, ProductView> productCache(ProductGateway gateway) {
    return Caffeine.newBuilder()
        .maximumSize(100_000)
        .expireAfterWrite(Duration.ofMinutes(5))
        .refreshAfterWrite(Duration.ofSeconds(30))
        .recordStats()
        .buildAsync((productId, executor) ->
            gateway.fetch(productId)
                .orTimeout(800, TimeUnit.MILLISECONDS));
}

Bu AsyncLoadingCache aynı JVM'de aynı anahtar için eşzamanlı yapılan get çağrılarını tek bir CompletableFuture üzerinde birleştirir. refreshAfterWrite süresini expireAfterWrite süresinden kısa tutmak kritik ayrıntıdır: 30. saniyeden sonra eski değer istek sahibine hemen dönerken tek bir arka plan yenilemesi başlar; beşinci dakikada sert bir boşluk oluşmaz. Loader istisna üretirse Caffeine başarısız future'ı cache'de tutmaz, fakat çağıran katmanda hata geleceği için orTimeout ve gateway tarafında açık bir timeout zorunludur.

Bir java eğitimi, java programlama eğitimi veya java kursu içinde bu problemi test ederken yalnızca rastgele anahtarlar üretmek yanıltıcıdır. Yük testinin en az bir senaryosu bütün sanal kullanıcıların aynı productId değerini çağırdığı hot-key senaryosu olmalıdır. Rastgele anahtar testi kapasiteyi, hot-key testi ise tekilleştirme davranışını ölçer.

Spring Framework cache soyutlamasının sınırı ve Caffeine katmanı

Spring Framework içindeki @Cacheable(sync = true), aynı uygulama örneğinde aynı anahtar için loader çağrılarını seri hale getirebilir; pod'lar arasında koordinasyon yapmaz. Ayrıca annotation tabanlı proxy, aynı sınıf içindeki self-invocation çağrısını intercept etmez. Bir servis metodunun kendi içinden this.findProduct(...) çağırması halinde cache tamamen atlanabilir. Bu nedenle hot-path için cache erişimini ayrı bir bean'e taşıyın veya doğrudan AsyncLoadingCache kullanın.

@Service
class ProductReadService {
    private final AsyncLoadingCache<String, ProductView> cache;

    CompletableFuture<ProductView> find(String productId) {
        return cache.get(productId);
    }
}

@Configuration
@EnableCaching
class CacheConfig {
    @Bean
    CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager("products");
        manager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(100_000)
            .expireAfterWrite(Duration.ofMinutes(5)));
        return manager;
    }
}

spring data jpa ile okuma yapıyorsanız loader'ın N+1 üretmediğini doğrulayın. Örneğin ürün görünümü için entity döndürmek yerine projection veya fetch join kullanın ve Hibernate istatistiklerini test profilinde açın: spring.jpa.properties.hibernate.generate_statistics=true. hibernate orm birinci seviye cache'i sadece aynı persistence context boyunca taşır; binlerce HTTP isteğinin aynı ürünü okumasını birleştirmez. Bu nedenle Hibernate L1 istatistiklerinde hit görmek, stampede'in çözüldüğü anlamına gelmez.

TTL'e rastgele sapma eklemek, çok sayıda anahtarın aynı deploy veya toplu yazma sonrası aynı anda süresinin dolmasını azaltır. Örneğin temel TTL 300 saniyeyse her yazmada 0-30 saniye jitter üretin. Jitter tek bir hot-key için yeterli değildir; onun için Caffeine future coalescing veya aşağıdaki dağıtık lease gerekir.

Java microservices ortamında Redis lease ile pod'lar arası tekilleştirme

Birden fazla pod aynı hot-key'i ilk kez yüklediğinde Caffeine yalnızca pod başına bir çağrıyı bastırır. java microservices topolojisinde Redis üzerinde kısa ömürlü lease alın: SET lock:product:{id} token NX PX 2000. Kilidi alan pod uzak kaynağı çağırır; alamayan pod 25-75 ms jitter ile cache'i tekrar kontrol eder. Bekleyen isteğin kilidi alan pod kadar uzun beklemesine izin vermeyin; toplam bekleme bütçesi HTTP timeout'unuzdan küçük olmalıdır.

String lockKey = "lock:product:" + productId;
String token = UUID.randomUUID().toString();
String acquired = redis.sync().set(lockKey, token,
    SetArgs.Builder.nx().px(2_000));

if ("OK".equals(acquired)) {
    try {
        ProductView value = gateway.fetchBlocking(productId, Duration.ofMillis(800));
        redis.sync().setex("product:" + productId, 300, json.writeValueAsString(value));
        return value;
    } finally {
        redis.sync().eval(
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "return redis.call('del', KEYS[1]) else return 0 end",
            ScriptOutputType.INTEGER, new String[] { lockKey }, token);
    }
}
return waitForCachedValue(productId, Duration.ofMillis(250));

Kilidi DEL lockKey ile koşulsuz silmeyin. GC pause, ağ gecikmesi veya uzak servis timeout'u nedeniyle 2 saniyelik lease bitebilir, başka pod yeni token ile kilidi alabilir; eski pod'un koşulsuz DEL'i yeni sahibin kilidini kaldırır. Lua içindeki token karşılaştırması bu yarışı önler. Buna rağmen lease bir dışlama optimizasyonudur, doğruluk mekanizması değildir: eski pod geç dönüp eski veriyi yazabilir. Kaynak veri sürümlüyse cache payload'ına updatedAt veya monotonik sürüm koyun ve Redis yazmasını gelen sürüm mevcut sürümden büyük veya eşitse kabul eden Lua CAS ile yapın.

Redis kilidini bir veritabanı yazma işleminin sahibi olmak için kullanmayın. Kilit süresi boyunca gerçekleşen network partition durumunda iki yazar oluşabilir. Cache doldurma, yinelenebilir bir read işlemi olduğu için uygundur; ödeme alma, stok azaltma veya dış sisteme komut yollama gibi yan etkiler için idempotency kaydı ya da veritabanı benzersiz kısıtı gerekir.

Spring Boot eğitimi için önce-sonra profiling ve yük testi

Değişikliği ölçmek için önce cache'i bypass eden veya sert TTL kullanan sürümde, sonra Caffeine coalescing ve Redis lease etkin sürümde aynı testi çalıştırın. k6 senaryosunda tüm VU'ların tek anahtarı çağırması gerekir; aksi halde cache hit oranı yüksek görünürken hot-key davranışı gizlenir. Her iki koşuda aynı ürün kaydını, aynı veritabanı indekslerini ve en az 120 saniyelik sabit yük penceresini kullanın.

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  scenarios: {
    hot_key: {
      executor: 'constant-vus',
      vus: 200,
      duration: '2m'
    }
  }
};

export default function () {
  const response = http.get(`${__ENV.BASE_URL}/products/42`);
  check(response, { '200': r => r.status === 200 });
}

Uygulama süreç seviyesinde kilit beklemesi, JSON serileştirme veya JDBC çağrılarının CPU etkisini ayırmak için Java Flight Recorder başlatın: jcmd $PID JFR.start name=cache settings=profile duration=120s filename=cache.jfr. CPU flame graph için async-profiler kullanın: ./profiler.sh -e wall -d 120 -f cache-wall.html $PID. Önceki koşuda ProductRepository.findById ve JDBC socket read örnekleri yoğunken, sonraki koşuda bunların yalnızca loader podlarında görünmesi beklenir. Flame graph tek başına başarı ölçütü değildir; k6 p99, veritabanı sorgu sayısı ve lock wait metriğiyle eşleştirin.

Micrometer ile Caffeine sayaçlarını Prometheus'a yayınlayın ve iki koşuyu şu sorguyla karşılaştırın: sum(rate(cache_gets_total{cache="productCache",result="miss"}[1m])). Buna ek olarak sum(rate(http_server_requests_seconds_count{uri="/products/{id}"}[1m])) istek hacmini, veritabanında ise pg_stat_statements.calls farkını kaydedin. Hedef, sadece hit ratio yükseltmek değildir: aynı istek hacminde ürün sorgusu çağrılarının hot-key başına yaklaşık bir yüklemeye yaklaşması ve p99'un timeout bütçesi altında kalmasıdır.

Spring AI, Spring MCP ve kullanıcı bağlamına bağlı cache anahtarları

spring ai kullanan bir uygulamada retrieval sonucu veya model yanıtını cache'lemek istiyorsanız anahtarı yalnızca prompt metninden üretmeyin. Tenant id, model kimliği, sistem prompt sürümü, retrieval corpus sürümü ve yetki kapsamı anahtara dahil edilmezse bir kullanıcının bağlamı başka kullanıcıya dönebilir. Güvenli bir örnek anahtar şeması sha256(tenantId + ':' + scopeHash + ':' + model + ':' + promptVersion + ':' + normalizedPrompt) biçimidir; ham prompt'u Redis key olarak kullanmak hem PII sızıntısı hem de key uzunluğu problemidir.

spring mcp ile Model Context Protocol araçlarını sunarken sadece yan etkisiz, deterministik okuma araçlarının sonucunu cache'leyin. Örneğin getCustomerProfile(customerId) için tenant ve caller role anahtara dahil edilir; createTicket veya sendEmail asla cache sonucu ile cevaplanmaz. Tool şemasında isteğe bağlı alanların canonical JSON sıralaması sabitlenmezse aynı mantıksal çağrı farklı cache key üretir. Jackson ObjectMapper üzerinde SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS açarak bu farkı azaltın.

java fullstack eğitimi bağlamında istemcinin ETag ile yaptığı koşullu GET ile sunucu içi hot-key cache'i aynı şey değildir. ETag ağ üzerinden body transferini azaltır, fakat origin tarafında aynı anda gelen ve cache'i boş olan 200 isteğini tekilleştirmez. İstemcide If-None-Match, sunucuda Caffeine future coalescing ve pod'lar arasında Redis lease birlikte uygulanabilir; her katmanın çözdüğü yarış koşulu farklıdır.

Sık Sorulan Sorular

Spring Boot eğitiminde @Cacheable(sync = true) cache stampede'i tüm pod'larda önler mi?

Hayır. sync = true Spring cache provider'ının aynı JVM içindeki anahtar yüklemesini koordine etmesine yardım eder. Kubernetes'te her pod ayrı heap kullandığı için pod'lar arası miss'leri birleştirmez. Çok pod'lu durumda Caffeine'i yerel tekilleştirme için, Redis SET NX PX lease'ini ise pod'lar arası yükleme sayısını azaltmak için kullanın.

Spring Data JPA ve Hibernate ORM ile cache stampede sırasında hangi metriğe bakmalıyım?

Caffeine miss sayısını, pg_stat_statements içindeki ilgili SELECT'in calls artışını, HikariCP active bağlantılarını ve HTTP p99'u aynı zaman ekseninde inceleyin. Hibernate ORM L1 cache hit'i yalnızca tek persistence context içindeki tekrarları gösterir. Testte hibernate.generate_statistics açıp entity fetch sayısının hot-key yükünde beklenenden fazla artıp artmadığını kontrol edin.

Java microservices sisteminde Redis kilidinin TTL'i kaç milisaniye olmalı?

TTL'i ölçülmüş loader p99 + küçük bir ağ ve scheduler payı üzerinden seçin. Örneğin uzak okuma p99 800 ms ise 2000 ms başlangıç değeri olabilir. Ancak TTL uzadıkça kilit sahibi hata verdiğinde bekleyenler daha uzun bloklanır. Loader'a TTL'den kısa timeout koyun, token kontrollü Lua release kullanın ve TTL aşımı sonrası eski verinin yeni veriyi ezmemesi için payload sürümüyle CAS yazımı ekleyin.

Spring AI ve Model Context Protocol araç sonuçları Redis cache'e konabilir mi?

Yalnızca read-only ve kullanıcı bağlamından doğru ayrıştırılmış araç sonuçları konmalıdır. Cache key'e tenant, yetki kapsamı, araç argümanlarının canonical JSON'u ve veri sürümü ekleyin. Model Context Protocol üzerinden e-posta gönderme veya kayıt oluşturma gibi side-effect içeren araçların sonucunu cache'lemek çağrıyı güvenli hale getirmez; idempotency ve işlem kaydı gerekir.

AI / LLM Discovery

Bu makale Opendart Akademi Java eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt