Caching Stratejileri: Uygulamanızı 10x Hızlandırmanın Sistematik Yolu
Amazon'un yaptığı bir araştırma var: sayfa yükleme süresi 100ms arttığında satışlar yüzde 1 düşüyor. Google'un araştırması ise 0.5 saniyelik gecikmenin arama trafiğini yüzde 20 azalttığını gösteriyor.
Hız iş sonuçlarını doğrudan etkiliyor. Ve hızlandırmanın en ekonomik yolu genellikle daha fazla sunucu değil, daha akıllı caching.
Cache Nedir ve Neden Gerekli?
Cache, sık erişilen verinin daha hızlı erişilebilir bir yerde saklanmasıdır. Orijinal kaynağa her seferinde gitmek yerine, yakında tutulan kopyadan servis yapılır.
Neden gerekli? Çünkü her katmanda hız farkı dramatiktir:
Bellek (RAM) erişimi: ~100 nanosaniye
SSD disk okuma: ~100 mikrosaniye (1.000x yavaş)
Ağ isteği (aynı bölge): ~1 milisaniye (10.000x yavaş)
Veritabanı sorgusu: ~10 milisaniye (100.000x yavaş)
Harici API isteği: ~100 milisaniye (1.000.000x yavaş)
Bir veritabanı sorgusunun sonucunu bellekte cache'lemek, o sorguyu her seferinde çalıştırmaktan 100.000 kat daha hızlı olabilir.
Katman 1: Browser Cache
En ucuz cache, kullanıcının tarayıcısında olan cache'dir. Sunucuya istek bile gitmez.
HTTP response header'ları ile kontrol edilir:
Cache-Control: max-age=86400, public ETag: "abc123def456" Last-Modified: Wed, 05 Mar 2025 10:00:00 GMT
Cache-Control direktifleri:
max-age=86400 — bu kaynağı 86400 saniye (1 gün) boyunca cache'le. Bu süre içinde tarayıcı sunucuya gitmez, doğrudan cache'den kullanır.
public — hem tarayıcı hem ara proxy'ler cache'leyebilir. Statik assets için ideal.
private — sadece kullanıcının tarayıcısı cache'leyebilir. Kişisel veriler için.
no-store — hiçbir şekilde cache'leme. Hassas finansal veriler için.
must-revalidate — cache süresi dolduğunda kullanmadan önce sunucudan doğrula.
ETag ile conditional request:
İlk istek:
Tarayıcı → GET /style.css
Sunucu ← 200 OK, ETag: "v2.1"
İkinci istek (cache süresi doldu):
Tarayıcı → GET /style.css, If-None-Match: "v2.1"
Sunucu ← 304 Not Modified (dosya değişmedi, tekrar indirme)
304 yanıtı body taşımaz — sadece header. Bant genişliği sıfır, gecikme minimum.
Versiyon stratejisi: Statik dosyaların URL'ine hash ekleyin. style.abc123.css gibi. max-age çok uzun tutabilirsiniz çünkü dosya değiştiğinde URL değişiyor, cache otomatik geçersiz oluyor.
Katman 2: CDN Cache
CDN (Content Delivery Network), içeriği kullanıcıya coğrafi olarak yakın sunucularda cache'ler. İstanbul'daki kullanıcı, ABD'deki origin sunucunuza gitmek yerine Frankfurt'taki CDN node'undan servis alır.
Etki dramatik: 200ms gecikme 10ms'ye düşebilir. Statik dosyalar, resimler, video — bunların hepsini CDN'den servis etmek hem hızlandırır hem origin sunucunuzun yükünü azaltır.
CDN cache'i nasıl kontrol edersiniz?
Cache-Control: public, max-age=31536000, s-maxage=86400
s-maxage sadece CDN ve proxy'leri etkiler. Tarayıcı max-age'e bakar, CDN s-maxage'e.
Surrogate-Control: max-age=3600 Cache-Tag: product-123, category-electronics
Cache-Tag ile belirli içerik gruplarını anında geçersiz kılabilirsiniz. Ürün güncellendi mi? product-123 tag'ini taşıyan tüm cache girişlerini CDN'den silin.
Katman 3: Application Cache (Redis)
Veritabanı sorgularının, harici API çağrılarının ve hesaplamalı işlemlerin sonuçlarını uygulama katmanında cache'lemek için Redis kullanılır.
Cache-Aside (Lazy Loading) — En Yaygın Pattern:
def get_product(product_id): cache_key = f"product:{product_id}" # 1. Önce cache'e bak cached = redis.get(cache_key) if cached: return json.loads(cached) # 2. Cache'de yoksa DB'den al product = db.query("SELECT * FROM products WHERE id = %s", product_id) # 3. Cache'e yaz (1 saat TTL) redis.setex(cache_key, 3600, json.dumps(product)) return product
Avantaj: sadece gerçekten istenen veri cache'lenir. Dezavantaj: ilk istek her zaman yavaş (cache miss).
Write-Through — Cache ve DB Her Zaman Senkron:
def update_product(product_id, data): # Hem DB hem cache'i güncelle db.execute("UPDATE products SET ... WHERE id = %s", product_id) redis.setex(f"product:{product_id}", 3600, json.dumps(data))
Cache her zaman güncel. Ama sık güncellenen veriler için gereksiz yazma işlemi.
Cache Warming — Soğuk Başlangıcı Önlemek:
Uygulama yeniden başladığında ya da yeni bir node eklendiğinde cache boştur. Bu dönemde tüm istekler DB'ye gider — cache stampede riski. Çözüm: kritik verileri önceden cache'e yüklemek.
def warm_cache(): popular_products = db.query( "SELECT * FROM products ORDER BY view_count DESC LIMIT 1000" ) for product in popular_products: redis.setex(f"product:{product.id}", 3600, json.dumps(product))
Cache Invalidation: En Zor Problem
Bilgisayar biliminin iki zor problemi vardır: cache invalidation ve şeylere isim vermek.
Cache'deki veri değişince ne olur? Eski veri servis edilmeye devam eder — stale data problemi.
TTL (Time To Live): En basit yaklaşım. Cache girişine süre ver, süre dolunca kendiliğinden geçersiz olsun. Ama veri güncellendikten sonra TTL dolana kadar eski veri servis edilir.
Event-Based Invalidation: Veri değiştiğinde ilgili cache girişlerini aktif olarak silin.
def update_user_profile(user_id, data): db.execute("UPDATE users SET ... WHERE id = %s", user_id) # İlgili tüm cache anahtarlarını temizle redis.delete(f"user:{user_id}") redis.delete(f"user:{user_id}:profile") redis.delete(f"user:{user_id}:orders")
Cache Versioning: Cache anahtarına versiyon ekleyin. Veri değiştiğinde versiyonu artırın.
cache_version = redis.get("cache:version:products") or 1 cache_key = f"product:{product_id}:v{cache_version}"
Tüm ürün cache'ini geçersiz kılmak istediğinizde versiyon numarasını artırmanız yeterli — eski anahtarlar artık hiç sorgulanmayacak, TTL dolunca silinecek.
Neleri Cache'lememeli?
Cache her problemi çözmez ve her veri cache'lenmeye uygun değildir.
Gerçek zamanlı güncel olması gereken veri cache'lenmez: borsa fiyatları, canlı konum bilgisi, anlık envanter sayıları. Kullanıcıya özel ve sık değişen veri cache'den fayda sağlamaz. Hesaplanması ucuz olan veri için cache'in overhead'i kazanımı geçebilir.
Cache Boyutu ve Tahliye Politikaları
Redis bellek dolduğunda yeni veri eklemek için eski veriyi silmek gerekir. Hangi veri silinir?
LRU (Least Recently Used) — en uzun süredir erişilmemiş veri silinir. Çoğu senaryo için iyi varsayılan.
LFU (Least Frequently Used) — en az erişilen veri silinir. Popüler verinin bellekte kalmasını garanti eder.
TTL tabanlı — süresi en yakın dolan veri önce silinir.
Redis konfigürasyonunda maxmemory-policy allkeys-lru ile belirlenebilir.
Caching doğru yapıldığında uygulamanız daha hızlı, veritabanınız daha az yüklü, altyapı maliyetiniz daha düşük olur. Yanlış yapıldığında stale data, cache stampede ve debug edilmesi zor tutarsızlıklar üretir. Fark, hangi veriyi nerede, ne kadar süreyle ve hangi invalidation stratejisiyle cache'lediğinizde.