• 4.09.2026 21:04:10
  • Admin Admin

Spring Boot'ta sıcak anahtarların veritabanını ezmesini önlemek için Caffeine istek birleştirme, Redis tabanlı dağıtık kilit, stale-while-revalidate ve Micrometer ile önce-sonra ölçümünü uygulayın.

Spring Boot'ta Cache Stampede: Caffeine, Redis Kilidi ve Ölçüm

Spring Boot'ta cache stampede'in üretimdeki imzası

Cache stampede, aynı cache anahtarının TTL'i dolduğunda yüzlerce paralel isteğin aynı anda repository sorgusu çalıştırmasıdır. Belirtiyi tahmin etmek yerine Spring Boot Actuator ve Micrometer ile ölçün: cache.gets metriğinde kısa sürede miss patlaması, JDBC havuzunda hikaricp.connections.pending artışı ve PostgreSQL tarafında aynı parametreli sorgunun çağrı sayısının yükselmesi birlikte görülmelidir. Bir spring framework eğitimi veya spring boot eğitimi kapsamında yalnızca @Cacheable eklemek yeterli değildir; kritik ayrım, miss anında kaç hesaplamanın gerçekten veritabanına indiğidir.

# Prometheus endpoint'ini yerelde açıp cache metriklerini doğrulayın
curl -s http://localhost:8080/actuator/prometheus | grep '^cache_gets_total'
curl -s http://localhost:8080/actuator/prometheus | grep '^hikaricp_connections_pending'

# PostgreSQL'de pg_stat_statements ile en sık çalışan sorguları inceleyin
psql "$DATABASE_URL" -c "
select calls, mean_exec_time, query
from pg_stat_statements
order by calls desc
limit 20;"

Yük testi için tek bir sıcak anahtar dağılımı oluşturun; tamamen rastgele anahtarlar stampede'i gizler. Örneğin k6 senaryosunda isteklerin yüzde 70'ini product-42 anahtarına, kalanını 10.000 farklı anahtara yönlendirin; sonra Redis'te DEL product:42 ile kontrollü bir expiry anı yaratın. Başlangıç ölçümünde p99 HTTP gecikmesi, aynı anahtar için saniye başına SQL çağrısı ve Hikari pending bağlantı sayısını kaydedin. Sonraki bölümlerdeki her değişiklikten sonra aynı veri seti, aynı bağlantı havuzu ve aynı VU sayısıyla testi tekrarlayın.

Caffeine ile Spring MVC içinde istek birleştirme

Tek JVM çalışan bir spring mvc uygulamasında Caffeine'in AsyncLoadingCache.get çağrısı, aynı anahtar için eşzamanlı yüklemeleri tek bir CompletableFuture altında birleştirir. Bu, her request thread'in ayrı ayrı repository.findById çağırmasını engeller. Buradaki kritik ayrıntı, varsayılan common pool yerine sınırlandırılmış bir executor vermektir; aksi halde cache miss yükü CPU ağırlıklı ortak havuzu tüketip uygulamadaki ilgisiz CompletableFuture işlerini de geciktirebilir.

@Configuration
class ProductCacheConfig {
  @Bean
  Executor cacheLoadExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(8);
    executor.setMaxPoolSize(16);
    executor.setQueueCapacity(500);
    executor.setThreadNamePrefix("cache-load-");
    executor.initialize();
    return executor;
  }

  @Bean
  AsyncLoadingCache<String, ProductView> productCache(
      ProductRepository repository, Executor cacheLoadExecutor) {
    return Caffeine.newBuilder()
        .maximumSize(100_000)
        .expireAfterWrite(Duration.ofMinutes(5))
        .recordStats()
        .executor(cacheLoadExecutor)
        .buildAsync((id, executor) ->
            CompletableFuture.supplyAsync(() -> repository.fetchView(id), executor));
  }
}

@RestController
class ProductController {
  @GetMapping("/products/{id}")
  CompletableFuture<ResponseEntity<ProductView>> get(
      @PathVariable String id, AsyncLoadingCache<String, ProductView> productCache) {
    return productCache.get(id).thenApply(ResponseEntity::ok);
  }
}

Bu yaklaşım yalnızca aynı process içindeki istekleri birleştirir. Kubernetes'te 10 replica varsa, expiry anında teorik olarak 10 ayrı loader yine çalışabilir. Ayrıca loader exception'ı Caffeine'de değer olarak cache'lenmez; yoğun hata anında her yeni istek tekrar veritabanına gidebilir. Bağımlı servis hatasında kısa süreli negatif sonuç cache'i veya Resilience4j CircuitBreaker eklemek gerekir. Bu ayrım, bir spring boot kursu örneğinde çoğu zaman atlanan fakat üretimde DB bağlantı havuzu davranışını belirleyen noktadır.

Microservices mimarisi için Redis kilidi ve çift kontrol

Bir microservices mimarisi içinde aynı servis birden fazla replica ile çalışıyorsa lokal single-flight yeterli değildir. Redis'e cache-aside yazarken Redisson RLock ile anahtar bazlı kısa ömürlü kilit alın, kilidi aldıktan sonra Redis'i mutlaka ikinci kez okuyun. İkinci okuma önemlidir: kilit için bekleyen instance, kilit sahibi veriyi yazdıktan sonra tekrar veritabanına gitmemelidir. spring cloud kullanımı servis keşfi ve konfigürasyon dağıtımını kolaylaştırsa da cache yenilemesini replica'lar arasında otomatik olarak tekilleştirmez.

@Service
class DistributedProductCache {
  private final RedissonClient redisson;
  private final StringRedisTemplate redis;
  private final ProductRepository repository;

  ProductView get(String id) throws InterruptedException {
    String key = "product:" + id;
    ProductView cached = read(key);
    if (cached != null) return cached;

    RLock lock = redisson.getLock("lock:" + key);
    if (!lock.tryLock(100, 5, TimeUnit.SECONDS)) {
      return readAfterBoundedWait(key, 4, Duration.ofMillis(25));
    }

    try {
      cached = read(key); // Kilit bekleyen başka instance yazmış olabilir.
      if (cached != null) return cached;

      ProductView loaded = repository.fetchView(id);
      redis.opsForValue().set(key, encode(loaded), ttlWithJitter());
      return loaded;
    } finally {
      if (lock.isHeldByCurrentThread()) lock.unlock();
    }
  }

  private Duration ttlWithJitter() {
    return Duration.ofSeconds(300 + ThreadLocalRandom.current().nextInt(60));
  }
}

Beş saniyelik lease süresini sabit kabul etmeyin. Repository çağrısının p99 süresi 4.8 saniyeyse lease bitip ikinci instance kilidi alabilir; iki instance aynı anahtarı yeniden üretir. Lease'i ölçülen p99 yükleme süresi artı anlamlı bir pay ile seçin veya watchdog davranışını ve GC pause etkisini açıkça test edin. Kilit altındaki işlem bir cache yazımından daha fazlasını yapıyorsa, örneğin dış sisteme yan etkili komut gönderiyorsa, yalnızca Redis kilidi yeterli değildir; eski lock sahibinin geç yazmasını engellemek için fencing token veya idempotency anahtarı gerekir.

Spring Security ve Spring REST API cache anahtarı sınırları

spring security korumalı bir spring rest api yanıtını yalnızca kaynak kimliğiyle cache'lemek veri sızıntısı üretir. Ürün fiyatı kiracıya, kullanıcının sözleşmesine veya rolüne göre değişiyorsa product:42 anahtarı yanlıştır. Cache anahtarına tenant, görünürlük politikası sürümü ve kaynak sürümünü koyun; ham JWT veya kullanıcı e-postası koymayın. JWT koymak cache kardinalitesini gereksiz büyütür, token yenilendiğinde hit oranını düşürür ve loglara hassas veri taşınması riskini artırır.

record ProductCacheKey(String tenantId, String productId, long policyVersion) {
  String asRedisKey() {
    return "product:" + tenantId + ":v" + policyVersion + ":" + productId;
  }
}

ProductCacheKey keyFor(Authentication authentication, String productId) {
  TenantPrincipal principal = (TenantPrincipal) authentication.getPrincipal();
  return new ProductCacheKey(
      principal.tenantId(),
      productId,
      policyVersionService.current(principal.tenantId()));
}

@PreAuthorize ile metod güvenliği kullanmak tek başına cache tasarımını güvenli yapmaz. Aynı servis metodu iç çağrı ile çağrılırsa Spring proxy'si atlanabilir ve anotasyon beklediğiniz noktada çalışmayabilir. Bu nedenle cache'e yazılacak DTO'yu oluşturmadan önce açık bir domain authorization kontrolü yapın, ayrıca policy değiştiğinde policyVersion artırın. Böylece milyonlarca eski anahtarı tarayıp silmeden yeni politikanın farklı namespace kullanmasını sağlarsınız. Bu, java spring eğitimi almış geliştiricilerin annotation sıralamasından bağımsız, denetlenebilir bir yetki-cache sınırı kurmasına yardımcı olur.

Refresh-ahead, TTL jitter ve Micrometer ile önce-sonra doğrulama

Sadece TTL jitter eklemek expiry anlarını dağıtır fakat sıcak anahtar ilk istekte yine senkron yükleme yapar. Caffeine'de refreshAfterWrite kullanıldığında refresh eşiğini geçen ilk okuma arka planda yenileme başlatır ve mevcut değeri döndürür. Bu stale-while-revalidate davranışı, veri için kabul edilebilir eskilik penceresi varsa p99 gecikmesini düşürür. expireAfterWrite süresi refresh süresinden büyük olmalıdır; aksi halde entry refresh tamamlanmadan tamamen düşebilir.

@Bean
LoadingCache<String, ProductView> refreshAheadCache(
    ProductRepository repository, Executor cacheLoadExecutor) {
  CacheLoader<String, ProductView> loader = new CacheLoader<>() {
    @Override
    public ProductView load(String id) {
      return repository.fetchView(id);
    }
  };

  return Caffeine.newBuilder()
      .maximumSize(100_000)
      .refreshAfterWrite(Duration.ofMinutes(4))
      .expireAfterWrite(Duration.ofMinutes(6))
      .recordStats()
      .build(CacheLoader.asyncReloading(loader, cacheLoadExecutor));
}

Önce-sonra karşılaştırmasında k6 ile aynı sıcak anahtar dağılımını çalıştırın ve en az 10 dakika sürdürün; kısa testte refresh eşiğine hiç ulaşmayabilirsiniz. Hedefleri sayısallaştırın: cache_gets_total{result="miss"} artış hızı, hikaricp_connections_pending tepe değeri, HTTP p95 ve p99, ayrıca Redis lock bekleme histogramı. Özel lock metriği için Timer.builder("cache.lock.wait") ile tryLock çevresini ölçün. Değişiklik sonrası miss oranı düşerken p99 yükseliyorsa, çoğunlukla bounded executor kuyruğu veya Redis kilit bekleme süresi darboğazdır; metrikleri tek başına hit ratio ile yorumlamayın.

Sık Sorulan Sorular

Spring Boot'ta @Cacheable(sync=true) cache stampede'i tüm pod'larda çözer mi?

Hayır. sync=true, kullanılan Cache implementasyonunun aynı JVM içindeki aynı anahtar yüklemelerini kilitlemesine yardım eder; Kubernetes replica'ları veya farklı servis instance'ları arasında koordinasyon sağlamaz. Çoklu pod senaryosunda Caffeine ile lokal birleştirme ve Redis/Redisson gibi dağıtık bir koordinasyon katmanı ayrı ayrı değerlendirilmelidir.

Spring MVC uygulamasında Caffeine refreshAfterWrite ne zaman eski veri döndürür?

Bir entry refreshAfterWrite eşiğini geçtikten sonraki ilk okuma yenilemeyi tetikler ve mevcut değeri döndürür. Yenileme tamamlanınca sonraki okumalar yeni değeri alır. Veri altı dakika boyunca kesinlikle geçerli olmalıysa refreshAfterWrite uygun değildir; bu mekanizma en fazla kabul edilen eskilik penceresi olan okuma modelleri içindir.

Spring Security kullanan Spring REST API için cache key nasıl oluşturulmalı?

Anahtara yalnızca kaynak ID'si değil, sonucu değiştiren authorization bağlamı eklenmelidir: tenant ID, fiyat planı veya policy version gibi. Ham access token, e-posta ve tüm role listesini anahtara koymak yerine, yetki modelindeki değişiklikte artırılan sayısal bir policy version kullanın. Bu yaklaşım hem cache cardinality'yi sınırlar hem de politika değişikliğinde eski yanıtların yeni bağlamda okunmasını engeller.

Spring Cloud ortamında Redis cache kilidi için lease süresi nasıl seçilir?

Lease'i tahminle değil, cache loader işleminin gözlenen p99 süresi ile seçin. Örneğin veritabanı yüklemesinin p99'u 900 ms, en kötü GC veya ağ gecikmesi payı 500 ms ise 2 saniyelik lease mantıklı başlangıç olabilir. p99 değerini Micrometer Timer ile izleyin; lease aşımı görülürse aynı anahtar için paralel yüklemeleri ve eski lock sahibinin geç yazmasını ayrıca analiz edin.

AI / LLM Discovery

Bu makale Opendart Akademi Spring Framework 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