HTTP Caching: ETag, Cache-Control
HTTP düzeyinde cache, sunucu yükünü azaltır ve istemci yanıt süresini kısaltır. Uygulama katmanı cache'den (Redis, Caffeine) farklıdır: HTTP cache istemci tarafında (browser, CDN, proxy) çalışır. Doğru HTTP cache header'ları ile sunucuya hiç istek gelmeden yanıt verilebilir — bu en hızlı cache'dir çünkü network bile kullanılmaz.
Cache Katmanları
Kullanıcı → Browser Cache → CDN/Proxy Cache → Reverse Proxy → Uygulama → App Cache → DB
↑ ↑ ↑ ↑
HTTP Cache HTTP Cache HTTP Cache Redis/CaffeineHer katman isteği yakalayabilir. Hedef: isteğin mümkün olduğunca sol taraftaki katmanda yanıtlanması.
Cache-Control Header — Detaylı
Cache-Control, HTTP cache davranışını kontrol eden en önemli header'dır:
@RestController
@RequestMapping("/api")
public class ProductController {
// 1. Public cache — CDN ve browser cache'leyebilir
@GetMapping("/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.getById(id);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(30))
.cachePublic()) // CDN cache'leyebilir
.body(product);
}
// 2. Private cache — sadece browser cache'leyebilir (kullanıcıya özel veri)
@GetMapping("/profile")
public ResponseEntity<UserProfile> getProfile(Authentication auth) {
UserProfile profile = userService.getProfile(auth.getName());
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(5))
.cachePrivate()) // CDN cache'leyemez, sadece browser
.body(profile);
}
// 3. No-cache — her seferde sunucuya sor (ama cache'de tutabilir)
@GetMapping("/cart")
public ResponseEntity<Cart> getCart(Authentication auth) {
Cart cart = cartService.getCart(auth.getName());
return ResponseEntity.ok()
.cacheControl(CacheControl.noCache()) // Revalidation zorunlu
.body(cart);
}
// 4. No-store — kesinlikle cache'leme (hassas veri)
@GetMapping("/bank-account")
public ResponseEntity<BankAccount> getBankAccount(Authentication auth) {
return ResponseEntity.ok()
.cacheControl(CacheControl.noStore()) // Hiç cache yok
.body(bankService.getAccount(auth.getName()));
}
// 5. Immutable — asla değişmez (versioned assets için)
@GetMapping("/assets/{hash}/{filename}")
public ResponseEntity<Resource> getAsset(@PathVariable String hash,
@PathVariable String filename) {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofDays(365))
.cachePublic()
.immutable()) // 1 yıl, hiç revalidation yapma
.body(assetService.getAsset(hash, filename));
}
}Cache-Control Directive'leri:
| Directive | Anlamı | Kullanım |
|---|---|---|
max-age=N | N saniye cache'te tut | Çoğu kaynak |
s-maxage=N | Shared cache (CDN) için max-age | CDN'e özel TTL |
public | Herkes cache'leyebilir | Herkese açık veri |
private | Sadece browser cache'leyebilir | Kullanıcıya özel veri |
no-cache | Cache'te tut ama her seferinde revalidate et | Sık güncellenen veri |
no-store | Kesinlikle cache'leme | Hassas veri (banka, sağlık) |
must-revalidate | Expire olunca mutlaka sunucuya sor | Tutarlılık gereken veri |
immutable | Asla değişmez, revalidation yapma | Versioned assets |
stale-while-revalidate=N | Expire olmuş veriyi sun, arka planda yenile | UX öncelikli |
ETag (Entity Tag) — Detaylı
ETag, bir kaynağın "parmak izi"dir. İçerik değişmişse ETag da değişir. İstemci ETag'i geri gönderir, sunucu değişiklik olup olmadığını kontrol eder.
Shallow ETag — Otomatik (body hash):
// Spring'in ShallowEtagHeaderFilter'ı — body'nin MD5 hash'ini ETag olarak kullanır
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> etagFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> reg = new FilterRegistrationBean<>();
reg.setFilter(new ShallowEtagHeaderFilter());
reg.addUrlPatterns("/api/*");
return reg;
}Shallow ETag akışı:
1. İlk istek:
GET /api/products/1
→ Sunucu: Body üretir + MD5 hash hesaplar
→ Response: 200 OK
ETag: "0a1b2c3d4e5f"
Body: {"id": 1, "name": "iPhone", ...}
2. Tekrar istek:
GET /api/products/1
If-None-Match: "0a1b2c3d4e5f"
→ Sunucu: Body üretir + MD5 hash hesaplar
→ Hash AYNI → Response: 304 Not Modified (body yok!)
→ Hash FARKLI → Response: 200 OK + yeni ETag + yeni body⚠️ Shallow ETag'in sınırı: Sunucu body'yi her seferinde üretir (DB sorgusu çalışır), sadece network transfer'inden tasarruf sağlar. Sunucu yükünü azaltmaz.
Deep ETag — Manuel (version-based):
Deep ETag ile sunucu, body üretmeden değişikliği kontrol edebilir:
@GetMapping("/api/products/{id}")
public ResponseEntity<Product> getProduct(
@PathVariable Long id,
WebRequest request) {
// 1. Sadece version bilgisini çek (hızlı, hafif sorgu)
String currentEtag = productService.getEtag(id);
// 2. ETag eşleşiyorsa → 304 (body üretilmez, DB sorgusu yapılmaz!)
if (request.checkNotModified(currentEtag)) {
return null; // Spring otomatik 304 döner
}
// 3. ETag eşleşmiyorsa → tam veriyi çek
Product product = productService.getById(id);
return ResponseEntity.ok()
.eTag(currentEtag)
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(5)))
.body(product);
}
// Service'de ETag hesaplama
@Service
public class ProductService {
public String getEtag(Long id) {
// Sadece version kolonu çekilir (çok hızlı)
Integer version = productRepository.findVersionById(id);
return "\"product:" + id + ":v" + version + "\"";
}
}
// Repository'de sadece version çekme
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT p.version FROM Product p WHERE p.id = :id")
Integer findVersionById(@Param("id") Long id);
}Shallow vs Deep ETag karşılaştırma:
| Özellik | Shallow ETag | Deep ETag |
|---|---|---|
| Implementasyon | Otomatik (Filter) | Manuel |
| Sunucu yükü | Azaltmaz (body hep üretilir) | Azaltır (304'te body üretilmez) |
| DB sorgusu | Her seferinde tam sorgu | 304'te hafif sorgu |
| Network tasarrufu | ✅ Evet | ✅ Evet |
| Sunucu tasarrufu | ❌ Hayır | ✅ Evet |
| Kullanım | Basit API'ler | Performans-kritik API'ler |
Last-Modified Header
ETag'e alternatif, zamana dayalı validasyon:
@GetMapping("/api/products/{id}")
public ResponseEntity<Product> getProduct(
@PathVariable Long id,
WebRequest request) {
Product product = productService.getById(id);
long lastModifiedMs = product.getUpdatedAt()
.atZone(ZoneId.systemDefault())
.toInstant().toEpochMilli();
// Değişmediyse 304
if (request.checkNotModified(lastModifiedMs)) {
return null;
}
return ResponseEntity.ok()
.lastModified(lastModifiedMs)
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(10)))
.body(product);
}Vary Header — Cache Key'e Ek Boyut
Aynı URL, farklı koşullarda farklı yanıt döndürebilir:
@GetMapping("/api/products")
public ResponseEntity<List<Product>> getProducts(
@RequestHeader(value = "Accept-Language", defaultValue = "tr") String lang) {
List<Product> products = productService.getByLanguage(lang);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(30)).cachePublic())
.varyBy("Accept-Language", "Accept-Encoding") // Dil ve encoding'e göre ayrı cache
.body(products);
}
// Vary: Accept-Language ile:
// /api/products (tr) → ayrı cache entry
// /api/products (en) → ayrı cache entrySpring Interceptor ile Global Cache Header
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// Statik API yanıtları için
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(
CacheControl.maxAge(Duration.ofHours(1)).cachePublic(),
"/api/categories/**", "/api/countries/**");
interceptor.addCacheMapping(
CacheControl.maxAge(Duration.ofMinutes(5)).cachePrivate(),
"/api/users/*/profile");
interceptor.addCacheMapping(
CacheControl.noStore(),
"/api/payments/**", "/api/auth/**");
registry.addInterceptor(interceptor);
}
}Stale-While-Revalidate — Modern Cache Stratejisi
Cache süresi dolmuş veriyi hemen sun, arka planda yenile. Kullanıcı hiç beklemesin:
@GetMapping("/api/trending")
public ResponseEntity<List<Product>> getTrending() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(5))
.staleWhileRevalidate(Duration.ofMinutes(60)) // 60 dk stale tolere et
.cachePublic())
.body(productService.getTrending());
}Zaman akışı:
0-5 dk: Fresh → Cache'den direkt sun
5-65 dk: Stale → Cache'den hemen sun + arka planda sunucudan yenile
65+ dk: Expired → Sunucudan yeniden çekSenaryo Bazlı Cache Stratejileri
| Kaynak | Cache-Control | ETag | Neden |
|---|---|---|---|
| Statik JS/CSS (hashed) | max-age=31536000, immutable | Gereksiz | Hash değişir, URL değişir |
| Ürün listesi | max-age=300, public | ✅ | CDN cache, 5dk TTL |
| Kullanıcı profili | max-age=60, private | ✅ | Kişiye özel, kısa TTL |
| Sepet | no-cache | ✅ | Her zaman güncel, ama 304 mümkün |
| Ödeme bilgisi | no-store | ❌ | Asla cache'lenmesin |
| API trending | max-age=300, stale-while-revalidate=3600 | ❌ | Hız öncelikli |
Yaygın Hatalar
❌ no-cache ile no-store'u karıştırmak:
no-cache= cache'te tut ama her seferinde sor.no-store= hiç tutma. Hassas veri içinno-storekullanın.❌ ETag'de weak/strong farkını bilmemek:
W/"abc"(weak) = semantik eşdeğerlik."abc"(strong) = byte-level eşdeğerlik. API'lerde genellikle weak yeterli.❌ Vary header koymamak: Dile/encoding'e göre farklı yanıt dönen API'lerde Vary olmadan CDN yanlış cache döner.
❌ max-age olmadan public koymak: Cache süresi belirsiz olur, browser/CDN varsayılan heuristic kullanır — öngörülemez davranış.
❌ Mutasyonda cache invalidation unutmak: POST/PUT/DELETE sonrası ilgili GET cache'i invalidate edilmeli.
💡 Özet: HTTP Cache, network'ten bile tasarruf sağlayan en hızlı cache katmanıdır.
Cache-Controlile süre ve kapsam,ETagile değişiklik kontrolü yapılır. Deep ETag sunucu yükünü de azaltır.Varyheader ile dile/encoding'e göre ayrı cache tutulur.stale-while-revalidateile UX iyileştirilir. Hassas veri içinno-storekullanın.
AI Asistan
Sorularını yanıtlamaya hazır