Ana içeriğe geç

Cache Patterns

Cache pattern’ları, verinin ne zaman ve nasıl cache’leneceğini belirler; yanlış pattern seçimi tutarsızlık ve performans sorunlarına yol açar.


1. Write-Through vs Write-Behind Karışımı

Yanlış Kullanım: Yazma işlemlerinde tutarsız cache stratejisi.

public async Task UpdateProductAsync(Product product)
{
    await _repository.UpdateAsync(product);
    // Bazen cache güncelleniyor, bazen siliniyor, bazen hiçbir şey yapılmıyor
    if (product.IsPopular)
        await _cache.SetAsync($"product:{product.Id}", product);
    // Tutarsız strateji
}

İdeal Kullanım: Tutarlı bir write stratejisi belirleyin.

// Write-Through: Veriyi DB ve cache'e aynı anda yaz
public async Task UpdateProductAsync(Product product)
{
    await _repository.UpdateAsync(product);
    await _cache.SetStringAsync($"product:{product.Id}",
        JsonSerializer.Serialize(product),
        new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) });
}

// Cache-Invalidation: Yazma işleminde cache'i sil, okumada yeniden yükle
public async Task UpdateProductAsync(Product product)
{
    await _repository.UpdateAsync(product);
    await _cache.RemoveAsync($"product:{product.Id}");
}

2. Cache Key Çakışması

Yanlış Kullanım: Belirsiz ve çakışmaya açık cache key’leri.

await _cache.SetAsync("products", data);      // Hangi filtre?
await _cache.SetAsync("user", userData);        // Hangi kullanıcı?
await _cache.SetAsync("1", orderData);          // Ne'nin 1'i?

İdeal Kullanım: Yapılandırılmış ve benzersiz cache key’leri oluşturun.

public static class CacheKeys
{
    public static string Product(int id) => $"product:{id}";
    public static string ProductList(int page, int size) => $"products:page:{page}:size:{size}";
    public static string UserProfile(int userId) => $"user:{userId}:profile";
    public static string UserOrders(int userId) => $"user:{userId}:orders";
}

await _cache.SetAsync(CacheKeys.Product(42), data);
await _cache.SetAsync(CacheKeys.ProductList(1, 20), listData);

3. Stale Data Toleransını Belirlememek

Yanlış Kullanım: Her veri için aynı cache süresi kullanmak.

// Tüm veriler 1 saat cache'leniyor
_cache.Set(key, data, TimeSpan.FromHours(1));
// Fiyat bilgisi 1 saat eski olabilir - sorunlu
// Ülke listesi 1 saatte bir yenileniyor - gereksiz

İdeal Kullanım: Verinin doğasına göre farklı TTL stratejileri uygulayın.

public static class CacheDurations
{
    public static TimeSpan Static => TimeSpan.FromHours(24);     // Ülke, şehir listeleri
    public static TimeSpan Reference => TimeSpan.FromHours(1);   // Kategori, marka
    public static TimeSpan Transactional => TimeSpan.FromMinutes(5); // Fiyat, stok
    public static TimeSpan Volatile => TimeSpan.FromSeconds(30);  // Dashboard, metrikler
}

await _cache.SetAsync(CacheKeys.Countries(), countries, CacheDurations.Static);
await _cache.SetAsync(CacheKeys.ProductPrice(id), price, CacheDurations.Transactional);

4. Multi-Level Cache Kullanmamak

Yanlış Kullanım: Her istek için Redis’e gitmek.

public async Task<Product> GetAsync(int id)
{
    var cached = await _distributedCache.GetAsync($"product:{id}"); // Her seferinde network call
    if (cached != null) return Deserialize(cached);

    var product = await _repository.GetByIdAsync(id);
    await _distributedCache.SetAsync($"product:{id}", Serialize(product));
    return product;
}

İdeal Kullanım: L1 (memory) + L2 (distributed) cache katmanları kullanın.

public class MultiLevelCache : ICacheService
{
    private readonly IMemoryCache _l1Cache;
    private readonly IDistributedCache _l2Cache;

    public async Task<T?> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan expiry)
    {
        // L1: In-Memory (hızlı)
        if (_l1Cache.TryGetValue(key, out T l1Value))
            return l1Value;

        // L2: Redis (network call)
        var l2Bytes = await _l2Cache.GetAsync(key);
        if (l2Bytes != null)
        {
            var l2Value = MessagePackSerializer.Deserialize<T>(l2Bytes);
            _l1Cache.Set(key, l2Value, TimeSpan.FromMinutes(1)); // L1'e de koy
            return l2Value;
        }

        // Cache miss: DB'den oku
        var value = await factory();
        var bytes = MessagePackSerializer.Serialize(value);

        _l1Cache.Set(key, value, TimeSpan.FromMinutes(1));
        await _l2Cache.SetAsync(key, bytes,
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiry });

        return value;
    }
}

5. Cache Warming Yapmamak

Yanlış Kullanım: Uygulama başladığında cache’in boş olması.

// Uygulama başladığında cache boş
// İlk istekler yavaş, tüm kullanıcılar cold cache ile karşılaşır

İdeal Kullanım: Uygulama başlarken sık erişilen verileri cache’e yükleyin.

public class CacheWarmupService : IHostedService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly IDistributedCache _cache;

    public async Task StartAsync(CancellationToken ct)
    {
        using var scope = _scopeFactory.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        // Sık erişilen verileri önceden yükle
        var categories = await context.Categories.ToListAsync(ct);
        await _cache.SetStringAsync("categories",
            JsonSerializer.Serialize(categories),
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) }, ct);

        var topProducts = await context.Products.OrderByDescending(p => p.ViewCount).Take(100).ToListAsync(ct);
        foreach (var product in topProducts)
        {
            await _cache.SetStringAsync($"product:{product.Id}",
                JsonSerializer.Serialize(product),
                new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }, ct);
        }
    }

    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}