Spring Boot uygulamalarında cache stampede sorununu ölçerek teşhis edin; JVM içi tek uçuş, Redis dağıtık kilidi, TTL jitter ve Micrometer metrikleriyle origin yükünü kontrollü biçimde azaltın.
Spring Boot'ta Cache Stampede: Redis Kilidi, TTL Jitter ve Ölçüm
Spring Boot eğitimi kapsamında cache stampede'i doğru teşhis etmek
Cache stampede, bir anahtarın TTL'i bittiği anda çok sayıda isteğin aynı pahalı origin çağrısını başlatmasıdır. Örneğin bir Spring REST API, 200 ms süren bir ürün fiyatı sorgusunu Redis'ten okuyorsa ve aynı anahtar için saniyede 800 istek geliyorsa, expiration anında JDBC havuzuna yüzlerce paralel sorgu binebilir. Bir spring boot eğitimi veya java spring eğitimi içinde bunu sadece cache miss oranıyla değerlendirmek eksiktir; aynı cache key için eşzamanlı origin load sayısını ayrıca ölçmek gerekir.
private final ConcurrentHashMap<String, AtomicInteger> inFlight = new ConcurrentHashMap<>();
public ProductView loadFromOrigin(String key) {
int concurrent = inFlight
.computeIfAbsent(key, ignored -> new AtomicInteger())
.incrementAndGet();
try {
concurrentOriginLoads.record(concurrent);
return productRepository.findViewBySku(key)
.orElseThrow(() -> new NoSuchElementException(key));
} finally {
inFlight.get(key).decrementAndGet();
}
}Bu sayaç üretim kodunda sınırsız key cardinality yaratmamalıdır. Key'i Micrometer tag'i olarak eklemek yerine yalnızca sayısal concurrency değerini DistributionSummary'ye yazın. Aynı anda `cache.origin.load` Timer'ını, `cache.result` için sadece `hit`, `miss`, `wait`, `fallback` gibi sonlu değerli tag'leri kaydedin. Bir spring framework eğitimi içeriğinde kritik ayrıntı şudur: hit ratio yüzde 95 olsa bile expiration saniyesindeki 400 paralel miss, yüzde ortalamalarının gizlediği asıl arıza modudur.
Spring MVC ve @Cacheable(sync = true) sınırı
Tek JVM çalışan bir spring mvc serviste `@Cacheable(sync = true)`, aynı anahtar için cache loader'ın tek iş parçacığında çalışmasına yardım eder. Bu davranış Cache abstraction seviyesindedir ve kullanılan Cache implementasyonunun desteğine bağlıdır; Caffeine ile anlamlıdır. Ancak microservices mimarisi içinde her pod kendi JVM'ine sahip olduğundan, 12 pod aynı anda expire olan anahtar için yine 12 origin çağrısı başlatabilir. Bu nedenle `sync = true`, Redis gibi ortak cache önünde dağıtık kilidin yerine geçmez.
@Configuration
@EnableCaching
class CacheConfig {
@Bean
CaffeineCacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager("product-l1");
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(20_000)
.expireAfterWrite(Duration.ofSeconds(20))
.recordStats());
return manager;
}
}
@Cacheable(cacheNames = "product-l1", key = "#sku", sync = true)
public ProductView findProduct(String sku) {
return productClient.fetch(sku);
}Caffeine'de `maximumSize` seçimini heap tahminiyle yapın: 20.000 nesne x ortalama 3 KB serileştirilmiş/veri modeli maliyeti yaklaşık 60 MB demek değildir; Java object graph, String ve collection overhead'i nedeniyle JFR `Object Count` ve `Old Object Sample` kayıtlarıyla gerçek retained size'ı kontrol edin. `sync = true` ile loader içinde başka bir cacheable metot çağırıp aynı anahtara dönen reentrant bir zincir kurmak, lock beklemeleri ve beklenmeyen gecikmelere yol açabilir; loader'ı doğrudan repository veya HTTP client çağrısıyla sınırlı tutun.
Spring Cloud ortamında Redis kilidi ve token ile güvenli bırakma
Birden fazla instance kullanan spring cloud dağıtımında, cache key için kısa ömürlü Redis kilidi alın, kilidi alan süreç cache'i ikinci kez kontrol etsin, sonra origin'den yüklesin. İkinci kontrol zorunludur: kilit beklerken başka instance değeri yazmış olabilir. Kilidi `DEL` ile koşulsuz silmeyin; lease süresi dolduktan sonra başka süreç aynı lock key'i almışsa eski süreç yeni sahibin kilidini siler. Token karşılaştırmalı Lua script'i bu yarışı engeller.
private static final DefaultRedisScript<Long> RELEASE_LOCK = new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end", Long.class);
public ProductView getOrLoad(String sku) {
String cacheKey = "product:v3:" + sku;
String lockKey = "lock:" + cacheKey;
String token = UUID.randomUUID().toString();
ProductView cached = read(cacheKey);
if (cached != null) return cached;
Boolean acquired = redis.opsForValue().setIfAbsent(
lockKey, token, Duration.ofSeconds(3));
if (!Boolean.TRUE.equals(acquired)) return waitForCache(cacheKey);
try {
cached = read(cacheKey); // lock altinda ikinci kontrol
if (cached != null) return cached;
ProductView loaded = productRepository.findViewBySku(sku)
.orElseThrow(() -> new NoSuchElementException(sku));
redis.opsForValue().set(cacheKey, write(loaded), ttlWithJitter());
return loaded;
} finally {
redis.execute(RELEASE_LOCK, List.of(lockKey), token);
}
}Lease süresini rastgele bir sabit olarak seçmeyin. JFR veya OpenTelemetry trace'lerinden origin load süresinin p99.9 değerini çıkarın ve lease'i bunun üzerine ağ gecikmesi payı ekleyerek belirleyin. Örneğin p99.9 850 ms ise 3 saniye makul bir başlangıç olabilir; ama 5 saniye süren downstream kesintisinde yine çift yükleme oluşabilir. Cache doldurma işlemi idempotent olduğundan bu genellikle doğruluk hatası değildir, fakat veritabanını korumak için kilidi alamayan istekleri 40-80 ms sınırında bekletip ardından Resilience4j Bulkhead veya CircuitBreaker fallback'ine yönlendirin.
TTL jitter, negatif cache ve Spring Security cache anahtarları
Aynı deployment veya toplu warm-up sırasında tüm anahtarlara 300 saniye TTL vermek, 300. saniyede senkron expiration dalgası üretir. TTL'e kontrollü jitter eklemek expiration'ları zamana yayar. Jitter aralığı TTL'in çok büyük yüzdesi olursa freshness sözleşmesi bozulur; 300 saniyelik veri için 0-30 saniye aralığı, iş kuralı izin veriyorsa ölçülebilir bir başlangıçtır.
private Duration ttlWithJitter() {
long baseSeconds = 300;
long jitterSeconds = ThreadLocalRandom.current().nextLong(0, 31);
return Duration.ofSeconds(baseSeconds + jitterSeconds);
}
private ProductView waitForCache(String cacheKey) {
for (int i = 0; i < 8; i++) {
LockSupport.parkNanos(Duration.ofMillis(10).toNanos());
ProductView value = read(cacheKey);
if (value != null) return value;
}
throw new CacheLoadBusyException(cacheKey);
}Bulunamayan SKU'lar için 15-30 saniyelik negatif cache, botların veya hatalı istemcilerin aynı olmayan kaydı sürekli veritabanından istemesini önler. Buna karşılık spring security ile kullanıcıya, role veya tenant'a göre değişen sonuçlarda negatif cache dahil tüm key'lere yetki bağlamını ekleyin. Sadece `#sku` anahtarıyla cache'lenen bir fiyat görünümü, yetkili kullanıcıya ait alanların başka kullanıcıya sızmasına neden olabilir. Spring Security `Authentication` bilgisinden tenant id çıkarıp controller yerine service katmanında anahtar üretin; HTTP header'ını doğrudan key yapmak cardinality ve cache poisoning riskini artırır.
Spring REST API için önce-sonra profil ve yük testi
Değişikliği doğrulamak için aynı veri seti, aynı pod sayısı ve aynı connection pool boyutuyla iki k6 koşusu çalıştırın: ilkinde ortak expiration'a sahip anahtarlarla mevcut davranış, ikincisinde jitter ve Redis tek uçuş mekanizması. Test başlangıcında cache'i `redis-cli --scan --pattern 'product:v3:*' | xargs -r redis-cli DEL` komutuyla temizleyin; ardından 60 saniyelik sabit yük altında özellikle ilk miss penceresini karşılaştırın.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 200,
duration: '60s',
thresholds: {
http_req_duration: ['p(95)<250'],
http_req_failed: ['rate<0.01']
}
};
export default function () {
const r = http.get('http://localhost:8080/api/products/HOT-SKU');
check(r, { 'status 200': (x) => x.status === 200 });
sleep(0.02);
}Önce-sonra tablosuna en az dört ölçüm koyun: `http_req_duration` p95/p99, `cache.origin.load` çağrı sayısı, HikariCP `hikaricp.connections.pending` zirvesi ve Redis lock bekleme sayısı. CPU nedenini doğrulamak için yük sırasında `./profiler.sh -d 60 -e wall -f before.html $PID` ve değişiklikten sonra aynı komutla `after.html` üretin. Flame graph'ta repository sürücüsü, JSON serileştirme veya lock polling'in toplam wall-clock payını karşılaştırın. Yalnızca p95 düşüp `fallback` sayısı artıyorsa, sistem origin'i korurken istemcilere hata döndürüyor olabilir; bu sonucu başarı saymak yerine fallback politikasını yeniden ayarlayın.
Spring boot kursu örneklerinde sık yapılan hata, sadece lokal makinede tek instance ile benchmark almaktır. Dağıtık kilit kodu için en az üç replica ile Kubernetes veya Docker Compose ortamı kurun ve Redis'i ayrı container olarak çalıştırın. Böylece spring cloud servis keşfi, pod ölçekleme ve ağ gecikmesi altında `sync = true` ile Redis lock'ın farklı davranışlarını gerçekçi şekilde görebilirsiniz.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Java Spring Boot ReactJS FullStack Eğitimi (Yıldız Teknik Üniversitesi SEM)
Sık Sorulan Sorular
Spring Boot'ta @Cacheable sync true Redis cache stampede sorununu çözer mi?
Hayır, `sync = true` aynı JVM içindeki aynı key yüklemelerini serialize eder. Birden fazla pod varsa her pod ayrı lock tutar. Ortak Redis cache kullanan bir microservices mimarisi için token'lı `SET key value NX PX` kilidi, ikinci cache kontrolü ve sınırlı bekleme stratejisi ekleyin.
Spring REST API cache TTL jitter değeri nasıl belirlenir?
Önce veri freshness sözleşmesinden taban TTL'i belirleyin. Ardından jitter'i taban TTL'in yaklaşık yüzde 5-10'u ile başlatın ve Prometheus'ta expiration sonrası `cache.origin.load` burst'ünü izleyin. Örneğin 300 saniye TTL için 0-30 saniye jitter, tüm key'lerin aynı anda düşmesini engellerken en kötü freshness yaşını 330 saniyeye taşır.
Spring Security kullanan bir serviste cache key'e kullanıcı bilgisi eklenmeli mi?
Yanıt kullanıcı, tenant, rol veya izin kapsamına göre değişiyorsa evet. Key'i `tenantId + ':' + sku + ':' + pricePolicyVersion` gibi sonlu ve doğrulanmış alanlardan üretin. JWT'nin tamamını veya Authorization header'ını key yapmak yüksek cardinality, token sızıntısı ve gereksiz cache parçalanması üretir.
Spring framework eğitimi ve spring boot eğitimi sırasında cache performansı nasıl ölçülür?
Micrometer ile origin load Timer'ı, lock wait Counter'ı ve HikariCP pending connection metriğini yayınlayın. Aynı k6 senaryosunu cache temizlenmiş halde önce ve sonra çalıştırın; ayrıca async-profiler wall-clock flame graph'larında JDBC, Redis client ve bekleme maliyetlerinin payını karşılaştırın.
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.



