İçeriğe atla

2025-12-19

Caching Stratejileri: Yerel Bellekten Distributed Sistemlere

In-memory uygulama cache'lerinden distributed Redis cluster'lara ve CDN edge caching'e kadar çok katmanlı caching stratejilerini uygulamaya yönelik kapsamlı bir rehber. Cache-aside ve write-through pattern'leri ne zaman kullanılır, ElastiCache ile MemoryDB arasında nasıl seçim yapılır ve production'da cache stampede nasıl önlenir öğrenin.

Etkili caching çok katmanlı bir problemdir: en hızlı katman süreç içi bir LRU, sonraki uzaktaki bir cache (Redis ya da Memcached), onun üstünde edge’de bir CDN’dir ve her katmanın farklı geçersiz kılma semantiği, tutarlılık garantisi ve hata biçimi vardır. %15 hit rate’e sahip bir Redis cluster’ı, yanlış katman için yanlış iş yapıyor demektir; Redis’in o iş için yanlış araç olması gerekmez. Popüler bir anahtar expire olduğunda oluşan thundering herd de aslında bir cache problemi değil, tek katmanlı herhangi bir cache’in maruz kalacağı bir sürü-koruma (stampede) problemidir.

Bu rehber, çalışan bir cache stratejisinin ardındaki teknik kararları ele alır. Çok katmanlı hiyerarşiyi (süreç içi, uzak, CDN), cache-aside ve write-through karşılaştırmasını, ElastiCache ve MemoryDB arasındaki seçimi, dağıtık ölçek için consistent hashing’i ve caching’i net bir kayba çeviren anti-pattern’leri (thundering herd, cache stampede, stale invalidation) kapsar.

Cache Pattern’lerini Anlamak

Cache pattern’leri sadece akademik konseptler değil. Cache-aside ile write-through arasındaki fark, stale data şikayetleri mi yoksa yavaş write performance mı alacağını belirleyebilir. Her pattern’in production’da gerçekte ne yaptığını görelim.

Cache-Aside (Lazy Loading)

Uygulama hem cache’i hem database’i doğrudan yönetir. Okumada önce cache’i kontrol et. Miss durumunda database’den çek ve cache’i doldur. En yaygın pattern bu çünkü basit ve verimli.

class UserRepository {
  private redis: Redis;
  private db: Database;

  async getUser(id: string): Promise<User> {
    // Önce cache'i kontrol et
    const cached = await this.redis.get(`user:${id}`);
    if (cached) {
      return JSON.parse(cached);
    }

    // Cache miss - database'den çek
    const user = await this.db.users.findById(id);

    // Cache'e TTL ile kaydet
    await this.redis.set(
      `user:${id}`,
      JSON.stringify(user),
      'EX',
      3600 // 1 saat
    );

    return user;
  }
}

Cache-aside ne zaman kullanılır:

  • Tüm datanın sık erişilmediği read-heavy workload’lar
  • Hafif staleness tolere edilebilen data
  • Sadece gerçekten kullanılan şeyleri cache’lemek istiyorsan

Trade-off’lar:

  • İlk request cache miss latency yaşar
  • Popüler expire olan key’lerde cache stampede riski (bunu düzelteceğiz)
  • Verimli memory kullanımı çünkü sadece erişilen data cache’lenir

Write-Through Pattern

Her yazma hem cache’e hem database’e gider. Cache database ile senkronize kalır ve okuyucular her zaman cache’den fresh data alır.

class UserRepository {
  async updateUser(id: string, data: Partial<User>): Promise<User> {
    // Önce database'i güncelle
    const user = await this.db.users.update(id, data);

    // Hemen cache'i güncelle
    await this.redis.set(
      `user:${id}`,
      JSON.stringify(user),
      'EX',
      3600
    );

    return user;
  }

  async getUser(id: string): Promise<User> {
    // Cache'i kontrol et (yeni güncellenen user'lar için her zaman orada olmalı)
    const cached = await this.redis.get(`user:${id}`);
    if (cached) {
      return JSON.parse(cached);
    }

    // Cache miss için cache-aside fallback
    const user = await this.db.users.findById(id);
    await this.redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
    return user;
  }
}

Write-through ne zaman kullanılır:

  • Cache ve database arasında strong consistency gereklilikleri
  • Write operasyonları sık
  • Read-heavy workload’lar her zaman fresh cache’den faydalanır

Trade-off’lar:

  • Write latency artar (hem cache hem database güncellemeli)
  • Hiç okunmayacak data’yı cache’ler
  • Daha yüksek cache hit rate’leri çünkü cache her zaman dolu

Write-Behind (Write-Back) Pattern

Yazmalar hemen cache’e gider, sonra asenkron olarak database’e yazılır. Mükemmel write performance sağlar ama karmaşıklık ve potansiyel data loss riski getirir.

class AnalyticsRepository {
  async trackEvent(event: Event): Promise<void> {
    // Hemen cache'e yaz (hızlı response)
    await this.redis.lpush(
      'analytics:queue',
      JSON.stringify(event)
    );

    // Background worker queue'yu asenkron işler
  }

  // Ayrı background worker
  async processQueue(): Promise<void> {
    while (true) {
      // Queue'dan event'leri batch işle
      const events = await this.redis.lrange('analytics:queue', 0, 99);

      if (events.length > 0) {
        // Database'e batch insert
        await this.db.analytics.batchInsert(
          events.map(e => JSON.parse(e))
        );

        // İşlenen event'leri sil
        await this.redis.ltrim('analytics:queue', 100, -1);
      }

      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
}

Write-behind ne zaman kullanılır:

  • Write-heavy workload’lar (analytics, log’lar, metrikler)
  • Cache failure durumunda potansiyel data loss tolere edilebilir
  • Database write performance bottleneck

Trade-off’lar:

  • Cache persistence öncesi fail olursa data loss riski
  • Daha karmaşık implementasyon ve monitoring
  • Batching sayesinde mükemmel write performance

Cache Stampede’i Önlemek

Cache stampede (thundering herd) popüler bir cache key expire olduğunda ve yüzlerce ya da binlerce request eşzamanlı onu yeniden oluşturmaya çalıştığında olur. Database connection pool’un tükenir ve her şey cascade eder.

Nasıl önlenir:

Probabilistic Early Expiration

Cache expire olmasını beklemek yerine, kalan TTL’e göre probabilistik olarak expiration’dan önce refresh et. Bu refresh yükünü yayar.

async function getWithProbabilisticRefresh<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number,
  beta: number = 1.0
): Promise<T> {
  const result = await redis.get(key);

  if (result) {
    const data = JSON.parse(result);
    const now = Date.now();
    const timeUntilExpiry = (data.expiresAt - now) / 1000;

    // Probabilistic early refresh
    // Expiry yaklaştıkça refresh olasılığı artar
    const shouldRefresh =
      timeUntilExpiry / ttl < Math.random() * beta;

    if (shouldRefresh) {
      // Background'da refresh et, blocking olmadan
      this.backgroundRefresh(key, fetcher, ttl);
    }

    return data.value;
  }

  // Cache miss - stampede önlemek için lock kullan
  return this.getWithLock(key, fetcher, ttl);
}

Distributed Locking

Cache miss olduğunda, data’yı kimin yeniden oluşturacağını koordine etmek için Redis kullan. Diğer request’ler kısaca bekler ve retry yapar.

async function getWithLock<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number
): Promise<T> {
  const lockKey = `lock:${key}`;

  // Lock almaya çalış (10 saniye timeout)
  const lockAcquired = await redis.set(
    lockKey,
    '1',
    'NX', // Sadece yoksa set et
    'EX',
    10
  );

  if (lockAcquired) {
    try {
      // Lock'u aldık - data'yı çek
      const value = await fetcher();

      const data = {
        value,
        expiresAt: Date.now() + ttl * 1000,
      };

      await redis.set(
        key,
        JSON.stringify(data),
        'EX',
        ttl
      );

      return value;
    } finally {
      // Her zaman lock'u serbest bırak
      await redis.del(lockKey);
    }
  } else {
    // Başka request fetch ediyor - bekle ve retry yap
    await new Promise(resolve => setTimeout(resolve, 100));
    return getWithProbabilisticRefresh(key, fetcher, ttl);
  }
}

Request Coalescing

Aynı in-flight request’leri uygulama seviyesinde deduplicate et. Aynı cache key için 100 request gelirse, sadece biri gerçekten data çeker.

class CacheManager {
  private inflightRequests = new Map<string, Promise<any>>();

  async get<T>(
    key: string,
    fetcher: () => Promise<T>
  ): Promise<T> {
    // Önce cache'i kontrol et
    const cached = await redis.get(key);
    if (cached) return JSON.parse(cached);

    // Request zaten in-flight mi kontrol et
    const existing = this.inflightRequests.get(key);
    if (existing) {
      // Mevcut request'e piggyback yap
      return existing;
    }

    // Yeni request oluştur
    const promise = fetcher()
      .then(async value => {
        await redis.set(
          key,
          JSON.stringify(value),
          'EX',
          300
        );
        this.inflightRequests.delete(key);
        return value;
      })
      .catch(error => {
        this.inflightRequests.delete(key);
        throw error;
      });

    this.inflightRequests.set(key, promise);
    return promise;
  }
}

AWS Caching Servisleri: Ne Zaman Hangisi

AWS ElastiCache, MemoryDB ve DAX sunuyor. Birbirlerinin yerine kullanılamazlar - her biri farklı use case’lere hizmet eder.

ElastiCache for Redis

En iyisi:

  • Birden fazla uygulama sunucusu arasında session management
  • Genel amaçlı caching katmanı (cache-aside pattern)
  • Pub/sub messaging pattern’leri
  • Leaderboard’lar, rate limiting, real-time analytics

Teknik özellikler:

  • Latency: Sub-milisaniye
  • Persistence: Opsiyonel snapshot’lar (real-time değil)
  • Consistency: Eventual
  • Fiyatlandırma: cache.r6g.large için ~0.206/saat(13.07GB)=nodebas\cına 0.206/saat (13.07 GB) = node başına ~150/ay
import Redis from 'ioredis';

const redis = new Redis.Cluster(
  [
    {
      host: 'redis-cluster.xxx.cache.amazonaws.com',
      port: 6379,
    },
  ],
  {
    redisOptions: {
      password: process.env.REDIS_PASSWORD,
      tls: {},
    },
    clusterRetryStrategy: times =>
      Math.min(100 * times, 3000),
    enableReadyCheck: true,
    maxRetriesPerRequest: 3,
  }
);

MemoryDB for Redis

En iyisi:

  • Microservice’ler için primary database (sadece cache değil)
  • Durability gerektiren real-time analytics
  • Redis hızı + ACID garantileri gereken mission-critical uygulamalar
  • Finansal transaction’lar, inventory management

Teknik özellikler:

  • Latency: Sub-milisaniye read’ler, tek haneli milisaniye write’lar
  • Persistence: Transaction log ile tam durable persistence
  • Consistency: Strong (senkron replication)
  • Multi-AZ: Sıfır data loss ile otomatik failover
  • Fiyatlandırma: db.r6g.large için ~0.406/saat= 0.406/saat = ~293/ay (ElastiCache’in 1.5x’i)

MemoryDB’yi ElastiCache yerine ne zaman seçmeli:

  • Redis’i primary database olarak kullanman gerekiyor (sadece cache değil)
  • Hiç data loss tolere edemezsin
  • Strong consistency garantileri gerekli
  • Ayrı database + cache mimarisini ortadan kaldırmak istiyorsun

DynamoDB Accelerator (DAX)

En iyisi:

  • Sadece DynamoDB’ye özel hızlandırma
  • Read-heavy DynamoDB workload’ları (gaming leaderboard’ları)
  • Eventually consistent read’ler kabul edilebilir
  • Scale’de mikrosaniye latency gerekli

Teknik özellikler:

  • Latency: Cache’li read’ler için mikrosaniye
  • Entegrasyon: Native DynamoDB API uyumluluğu
  • Consistency: Sadece eventually consistent read’ler
  • Fiyatlandırma: dax.r4.large için ~$0.40/saat

Önemli sınırlamalar:

  • Sadece DynamoDB ile çalışır (genel amaçlı değil)
  • Query/scan cache’i get/batch-get cache’inden ayrı
  • Strongly consistent read desteği yok
  • Conditional update’leri cache’leyemez

Karar Matrisi

Evet

Hayir

Cache

Primary DB

Evet

Hayir

Evet

Hayir

Caching Gerekli mi?

Sadece DynamoDB?

DAX

Primary DB mi Cache mi?

Data Loss OK?

MemoryDB

ElastiCache

Pub/Sub Gerekli?

ElastiCache Redis

ElastiCache Redis veya Memcached

Distributed Cache’ler için Consistent Hashing

Birden fazla cache node’un varsa, hangi node’un hangi key’i sakladığına nasıl karar verirsin? Basit modulo hashing (hash(key) % N) node’lar değiştiğinde büyük redistribution’a neden olur:

  • Server ekle: ~%50 key taşınır
  • Server kaldır: ~%50 key taşınır

Consistent hashing redistribution’ı ~1/N key’e minimize eder.

İmplementasyon

import crypto from 'crypto';

class ConsistentHash {
  private ring: Map<number, string> = new Map();
  private sortedKeys: number[] = [];
  private virtualNodes: number = 150;

  private hash(key: string): number {
    return parseInt(
      crypto
        .createHash('md5')
        .update(key)
        .digest('hex')
        .substring(0, 8),
      16
    );
  }

  addServer(server: string): void {
    // Eşit dağılım için virtual node'lar oluştur
    for (let i = 0; i < this.virtualNodes; i++) {
      const hash = this.hash(`${server}:vnode:${i}`);
      this.ring.set(hash, server);
      this.sortedKeys.push(hash);
    }
    this.sortedKeys.sort((a, b) => a - b);
  }

  removeServer(server: string): void {
    for (let i = 0; i < this.virtualNodes; i++) {
      const hash = this.hash(`${server}:vnode:${i}`);
      this.ring.delete(hash);
      const index = this.sortedKeys.indexOf(hash);
      if (index > -1) {
        this.sortedKeys.splice(index, 1);
      }
    }
  }

  getServer(key: string): string | undefined {
    if (this.sortedKeys.length === 0) return undefined;

    const hash = this.hash(key);

    // Ring'de bir sonraki server için binary search
    let idx = this.sortedKeys.findIndex(k => k >= hash);
    if (idx === -1) idx = 0; // Wrap around

    const serverHash = this.sortedKeys[idx];
    return this.ring.get(serverHash);
  }
}

// Kullanım
const hashRing = new ConsistentHash();
hashRing.addServer('cache-node-1');
hashRing.addServer('cache-node-2');
hashRing.addServer('cache-node-3');

const server = hashRing.getServer('user:12345');
// Döner: 'cache-node-2'

Virtual Node’lar Neden Önemli

Virtual node’lar olmadan basit consistent hashing dengesiz dağılım oluşturabilir. Virtual node’lar (vnode’lar) bunu çözer:

  • Her fiziksel node ring üzerinde dağılmış 100-200 virtual node alır
  • Daha uniform data dağılımı
  • Node ekleme/çıkarma sırasında daha smooth load balancing
  • Server’ları kapasiteye göre ağırlıklandırabilirsin (daha fazla vnode = daha fazla data)
// Kapasiteye göre ağırlıklandır
const optimalVnodes = Math.ceil(
  150 * (serverCapacity / averageCapacity)
);

// Yüksek kapasiteli server daha fazla data alır
hashRing.addServer('high-capacity', 225); // 1.5x
hashRing.addServer('low-capacity', 75); // 0.5x

Multi-Tier Caching Mimarisi

Gerçek performance cache’leri stratejik olarak katmanlamaktan gelir. İşte pratik üç katmanlı bir mimari:

L1: In-Process Memory Cache

  • Boyut: Instance başına 50-100 MB
  • TTL: 30-60 saniye
  • Amaç: Hot data için ultra-hızlı erişim
  • Teknoloji: LRU cache

L2: Distributed Redis Cache

  • Boyut: 10-100 GB cluster
  • TTL: 5-60 dakika
  • Amaç: Instance’lar arası paylaşımlı cache
  • Teknoloji: ElastiCache Redis cluster

L3: CDN Edge Cache

  • Boyut: Sınırsız (CloudFront)
  • TTL: 1 saat - 1 yıl
  • Amaç: Global edge dağıtımı
  • Teknoloji: CloudFront

İmplementasyon

import LRU from 'lru-cache';

class MultiTierCache {
  private l1Cache: LRU<string, any>;
  private l2Cache: Redis;

  constructor() {
    this.l1Cache = new LRU({
      max: 500, // Max item
      maxSize: 50 * 1024 * 1024, // 50 MB
      sizeCalculation: (value) => {
        return JSON.stringify(value).length;
      },
      ttl: 1000 * 60, // 1 dakika
    });
  }

  async get<T>(
    key: string,
    fetcher: () => Promise<T>
  ): Promise<T> {
    // L1: In-memory cache'i kontrol et
    if (this.l1Cache.has(key)) {
      return this.l1Cache.get(key);
    }

    // L2: Redis'i kontrol et
    const l2Result = await this.l2Cache.get(key);
    if (l2Result) {
      const value = JSON.parse(l2Result);
      // L1'i doldur
      this.l1Cache.set(key, value);
      return value;
    }

    // Cache miss - origin'den çek
    const value = await fetcher();

    // Tüm cache katmanlarını doldur
    this.l1Cache.set(key, value);
    await this.l2Cache.set(
      key,
      JSON.stringify(value),
      'EX',
      3600
    );

    return value;
  }

  async invalidate(key: string): Promise<void> {
    // Tüm katmanları invalidate et
    this.l1Cache.delete(key);
    await this.l2Cache.del(key);
  }
}

CloudFront Caching Stratejileri

CDN caching uygulama caching’den farklı. İçeriği global olarak uzun TTL’lerle dağıtıyorsun, bu da invalidation stratejisinin önemli olduğu anlamına gelir.

Cache Behavior Configuration

Farklı içerik türleri farklı cache policy’leri gerektirir:

import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as cdk from 'aws-cdk-lib';

// Static asset'ler (image'lar, CSS, JS)
const staticBehavior = {
  pathPattern: '/static/*',
  cachePolicy: new cloudfront.CachePolicy(
    this,
    'StaticCachePolicy',
    {
      minTtl: cdk.Duration.seconds(0),
      defaultTtl: cdk.Duration.hours(24),
      maxTtl: cdk.Duration.days(365),
      enableAcceptEncodingGzip: true,
      enableAcceptEncodingBrotli: true,
      queryStringBehavior:
        cloudfront.CacheQueryStringBehavior.none(),
      headerBehavior:
        cloudfront.CacheHeaderBehavior.none(),
      cookieBehavior:
        cloudfront.CacheCookieBehavior.none(),
    }
  ),
};

// API response'ları (kısa ömürlü)
const apiCacheBehavior = {
  pathPattern: '/api/public/*',
  cachePolicy: new cloudfront.CachePolicy(
    this,
    'ApiCachePolicy',
    {
      minTtl: cdk.Duration.seconds(0),
      defaultTtl: cdk.Duration.seconds(60),
      maxTtl: cdk.Duration.minutes(5),
      queryStringBehavior:
        cloudfront.CacheQueryStringBehavior.all(),
      headerBehavior:
        cloudfront.CacheHeaderBehavior.allowList(
          'Authorization'
        ),
    }
  ),
};

// Dynamic içerik (cache yok)
const dynamicBehavior = {
  pathPattern: '/api/user/*',
  cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
};

Invalidation Stratejisi

CloudFront invalidation maliyetleri toplanır (ilk 1.000/ay’dan sonra path başına $0.005). Bunun yerine versiyonlu URL’ler kullan:

// Kötü: Invalidation gerektirir
const assetUrl = '/static/app.js';
await cloudfront.createInvalidation({
  DistributionId: 'E1234567890',
  InvalidationBatch: {
    CallerReference: Date.now().toString(),
    Paths: {
      Quantity: 1,
      Items: ['/static/app.js'],
    },
  },
});

// İyi: Versiyonlu URL (invalidation gerekmez)
const buildHash = process.env.BUILD_HASH;
const assetUrl = `/static/app.${buildHash}.js`;
// Yeni versiyon = yeni URL = otomatik cache busting

React Query ile Client-Side Caching

Frontend caching genellikle gözden kaçırılır ama kullanıcı deneyimi için kritik. React Query (TanStack Query) stale-while-revalidate pattern ile sofistike client-side caching sağlar.

import {
  useQuery,
  useMutation,
  useQueryClient,
} from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) {
  const queryClient = useQueryClient();

  // Caching ve stale-while-revalidate ile query
  const { data: user, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000, // 5 dakika fresh
    gcTime: 30 * 60 * 1000, // Cache'de 30 dakika tut
    refetchOnWindowFocus: true,
    refetchOnReconnect: true,
  });

  // Optimistic update'lerle mutation
  const updateMutation = useMutation({
    mutationFn: (data: Partial<User>) =>
      updateUser(userId, data),

    onMutate: async newData => {
      // Giden refetch'leri iptal et
      await queryClient.cancelQueries({
        queryKey: ['user', userId],
      });

      // Önceki değeri snapshot'la
      const previous = queryClient.getQueryData([
        'user',
        userId,
      ]);

      // Cache'i optimistically güncelle
      queryClient.setQueryData(
        ['user', userId],
        (old: any) => ({
          ...old,
          ...newData,
        })
      );

      return { previous };
    },

    onError: (err, variables, context) => {
      // Hata durumunda rollback yap
      queryClient.setQueryData(
        ['user', userId],
        context?.previous
      );
    },

    onSettled: () => {
      // Mutation'dan sonra refetch
      queryClient.invalidateQueries({
        queryKey: ['user', userId],
      });
    },
  });

  return (
    <div>
      {isLoading ? 'Yükleniyor...' : user?.name}
      <button
        onClick={() =>
          updateMutation.mutate({ name: 'Yeni İsim' })
        }
      >
        Güncelle
      </button>
    </div>
  );
}

Daha İyi UX için Prefetching

Kullanıcılar ihtiyaç duymadan önce data’yı prefetch et, anında navigasyon için:

function UserList() {
  const queryClient = useQueryClient();

  const { data: users } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });

  // Hover'da prefetch
  const handleUserHover = (userId: string) => {
    queryClient.prefetchQuery({
      queryKey: ['user', userId],
      queryFn: () => fetchUser(userId),
    });
  };

  return (
    <ul>
      {users?.map(user => (
        <li
          key={user.id}
          onMouseEnter={() => handleUserHover(user.id)}
        >
          <Link to={`/user/${user.id}`}>
            {user.name}
          </Link>
        </li>
      ))}
    </ul>
  );
}

Cache Monitoring ve Optimization

Ölçmediğin şeyi optimize edemezsin. İşte kritik metrikler:

Ana Metrikler

1. Hit Rate

class CacheMetrics {
  private hits = 0;
  private misses = 0;

  recordHit(): void {
    this.hits++;
  }

  recordMiss(): void {
    this.misses++;
  }

  getHitRate(): number {
    const total = this.hits + this.misses;
    return total === 0 ? 0 : (this.hits / total) * 100;
  }
}

Hedef: Workload’a göre %85-95

  • %80’in altında: Cache key tasarımını, TTL ayarlarını incele
  • Formül: (hit'ler / (hit'ler + miss'ler)) * 100

2. Latency Percentile’ları

  • P50: Redis için ~1-2ms
  • P99: <10ms olmalı
  • P99.9: >50ms ise alert

3. Memory Kullanımı

  • Hedef: %70-80 kullanım
  • Alert: >%90 (eviction riski)

4. Eviction Rate

  • Yüksek eviction = daha fazla memory ya da daha kısa TTL’ler gerekli

Monitoring İmplementasyonu

import { CloudWatch } from 'aws-sdk';

class CacheMonitor {
  private cloudwatch: CloudWatch;

  async trackMetrics(
    cacheKey: string,
    hit: boolean,
    latency: number
  ): Promise<void> {
    await this.cloudwatch
      .putMetricData({
        Namespace: 'CustomCache',
        MetricData: [
          {
            MetricName: 'CacheHitRate',
            Value: hit ? 1 : 0,
            Unit: 'Count',
            Dimensions: [
              { Name: 'CacheLayer', Value: 'Redis' },
            ],
          },
          {
            MetricName: 'CacheLatency',
            Value: latency,
            Unit: 'Milliseconds',
            Dimensions: [
              { Name: 'CacheLayer', Value: 'Redis' },
            ],
          },
        ],
      })
      .promise();
  }

  async getCacheHitRate(
    period: number = 300
  ): Promise<number> {
    const result = await this.cloudwatch
      .getMetricStatistics({
        Namespace: 'CustomCache',
        MetricName: 'CacheHitRate',
        StartTime: new Date(Date.now() - period * 1000),
        EndTime: new Date(),
        Period: period,
        Statistics: ['Average'],
        Dimensions: [
          { Name: 'CacheLayer', Value: 'Redis' },
        ],
      })
      .promise();

    return result.Datapoints?.[0]?.Average ?? 0;
  }
}

Yaygın Hatalar ve Dersler

1. Dynamic Data’yı Aşırı Cache’lemek

User’a özel data’yı uzun TTL ile cache’lemek kullanıcıların stale data görmesine ve destek bilet sayısının artmasına yol açar.

Çözüm: Data’yı volatility’sine göre sınıflandır:

const cacheStrategies = {
  static: {
    ttl: 86400 * 7, // 1 hafta
    pattern: 'static:*',
  },
  config: {
    ttl: 3600, // 1 saat
    pattern: 'config:*',
  },
  userProfile: {
    ttl: 300, // 5 dakika
    pattern: 'user:*',
    invalidateOn: ['user.updated'],
  },
  realtime: {
    ttl: 0, // Cache'leme
    pattern: 'inventory:*',
  },
};

2. Kötü Cache Key Tasarımı

Cache key’lerine timestamp ya da random değerler eklemek hit rate’i yok eder.

// Kötü: Gereksiz değişkenlik
const key = `user:${userId}:${timestamp}:${requestId}`;

// İyi: Deterministik ve minimal
const key = `user:${userId}`;

// İyi: Sadece anlamlı parametreleri dahil et
const key = `user:${userId}:posts:${page}`;

3. Cache Failure’ları Görmezden Gelmek

Cache failure uygulamanı düşürmemeli. Her zaman fallback implement et:

class ResilientCache {
  async get<T>(
    key: string,
    fetcher: () => Promise<T>
  ): Promise<T> {
    try {
      const cached = await Promise.race([
        redis.get(key),
        this.timeout(100), // 100ms timeout
      ]);

      if (cached) return JSON.parse(cached);
    } catch (error) {
      // Log yap ama throw etme
      logger.warn('Cache failure, origin kullanılıyor', {
        key,
        error,
      });
    }

    // Her durumda origin'den çek
    return fetcher();
  }
}

4. CloudFront Invalidation İstismarı

Sık invalidation maliyetleri artırır. Bunun yerine versiyonlu URL’ler kullan:

class AssetVersioning {
  private buildHash: string;

  constructor() {
    this.buildHash =
      process.env.BUILD_HASH || Date.now().toString();
  }

  // URL üzerinden otomatik cache busting
  getAssetUrl(path: string): string {
    return `${path}?v=${this.buildHash}`;
  }
}

Maliyet Optimizasyonu

AWS Servis Fiyatlandırması (us-east-1)

ElastiCache Redis (cache.r6g.large: 13.07 GB):

  • On-Demand: 0.206/saat=nodebas\cına 0.206/saat = node başına ~150/ay
  • 3-node cluster: ~$450/ay

MemoryDB (db.r6g.large: 13.07 GB):

  • On-Demand: 0.406/saat=nodebas\cına 0.406/saat = node başına ~293/ay
  • 3-node cluster: ~$879/ay (ElastiCache’in 1.5x’i)

CloudFront:

  • İlk 10 TB/ay: $0.085/GB
  • HTTP/HTTPS request’leri: 10.000 başına $0.0075
  • Invalidation: İlk 1.000 path ücretsiz, sonrası path başına $0.005

Right-Sizing Stratejisi

class CacheOptimization {
  async analyzeUtilization(): Promise<Report> {
    const metrics = await this.getWeeklyMetrics();

    const avgMemoryUsage = metrics.memory.average;
    const currentCapacity = this.getCurrentCapacity();

    const recommendations = [];

    // Sürekli düşük kullanım
    if (avgMemoryUsage < currentCapacity * 0.6) {
      const recommendedSize =
        this.calculateOptimalSize(metrics.memory.peak);
      const savings = this.calculateSavings(
        currentCapacity,
        recommendedSize
      );

      recommendations.push({
        type: 'DOWNSIZE',
        currentSize: currentCapacity,
        recommendedSize,
        monthlySavings: savings,
      });
    }

    // Yüksek eviction rate
    if (metrics.evictions.perDay > 1000) {
      recommendations.push({
        type: 'UPSIZE',
        reason: 'Yüksek eviction rate hit rate\'i etkiliyor',
        impact: 'Hit rate %15-20 iyileşebilir',
      });
    }

    return { metrics, recommendations };
  }
}

Önemli Çıkarımlar

Birden fazla projede caching ile çalışmak şu pattern’leri öğretti:

1. Cache pattern’leri önemli: Read-heavy için cache-aside, consistency için write-through, write-heavy için write-behind. Gerçek workload’una göre seç.

2. Stampede’i erken önle: Problem yaşamadan önce distributed locking ve request coalescing implement et. Bir incident’tan sonra eklemek çok daha zor.

3. AWS servisleri birbirinin yerine kullanılamaz: Genel caching için ElastiCache, durability gerekiyorsa MemoryDB, sadece DynamoDB için DAX. İhtiyacın olmayan özellikler için fazla ödeme yapma.

4. Multi-tier caching çalışır: L1 in-memory + L2 Redis + L3 CDN maliyet başına en iyi performance’ı sağlar. Her katmanın bir amacı var.

5. Sürekli monitoring yap: Cache hit rate, latency, memory kullanımı ve request başına maliyet. Gerçek kullanıma göre aylık right-size yap.

6. Failure için tasarla: Cache performance’ı iyileştirmeli, single point of failure olmamalı. Her zaman graceful degradation implement et.

7. Invalidate etme, versiyonla: CloudFront invalidation maliyetleri toplanır. Versiyonlu asset’ler ücretsiz ve anında.

%15 hit rate ile %90 hit rate arasındaki fark genellikle sadece doğru cache key tasarımı ve TTL yönetimi. Temellerle başla, her şeyi monitoring yap ve gerçek metriklere göre optimize et.

İlgili yazılar

Key-Value Storage Temelleri - Doğru Çözümü Anlama ve Seçme Rehberi

Key-value storage hakkında dört temel soruyu yanıtlayan kapsamlı bir temel rehber: KV storage nedir? Nerede kullanılır? Neden KV storage seçilir? Hangi tech stack'lerde hangi çözümler var?

redisdynamodbkey-value-storage+5
DynamoDB Rate Limiting: Single Table Design'da Ölçekte Stratejiler

Single Table Design uygulamalarında DynamoDB throttling'i önleme ve yönetme stratejileri. Partition key tasarımı, write sharding, kapasite modları, DAX caching, retry pattern'leri ve yüksek throughput sistemler için CloudWatch monitoring konularını kapsar.

dynamodbawsrate-limiting+5
AWS ile Edge Computing: CloudFront Functions vs Lambda@Edge

Global uygulamalar için AWS edge computing çözümlerini seçme ve uygulama üzerine pratik örnekler ve maliyet optimizasyonu stratejileri içeren kapsamlı teknik rehber.

awscloudfrontlambda+6
AWS Lambda Sub-10ms Optimizasyonu: Kapsamlı Rehber

Runtime seçimi, veritabanı optimizasyonu, bundle boyutu azaltma ve caching stratejileri ile AWS Lambda'da sub-10ms response süreleri elde edin. Gerçek benchmark'lar ve production deneyimleri dahil.

awslambdaperformance+7
Veritabanı Seçim Rehberi: Klasikten Edge'e - Kapsamlı Mühendislik Perspektifi

Projeniz için doğru veritabanını seçmek için kapsamlı rehber - SQL, NoSQL, NewSQL ve edge çözümlerini gerçek dünya implementasyon hikayeleri ve performans ölçümleri ile kapsıyor.

databasepostgresqlmysql+8