Redis ile .NET Önbellekleme (Caching)

Redis nedir, .NET uygulamalarinda IDistributedCache ve StackExchange.Redis ile nasil kullanilir.

Bir ürün detay API’si aynı ürün kartını saniyede onlarca kez okuyor, ürün adı ve görseli ise seyrek değişiyorsa Redis veritabanındaki tekrar eden sorguları azaltabilir. ASP.NET Core uygulaması birden fazla kopya halinde çalıştığında bu kopyalar aynı önbelleği kullanabilir. Bu örnekte ürün kartı herkese açık ve birkaç dakika eski kalması kabul edilebilir. Stok rezervasyonu ve ödeme tutarı gibi kararlar için güncel değerleri asıl veri kaynağından okuyun.

Redis’i uygulamaya bağlayın

Projeye hedef .NET sürümünüzle uyumlu Microsoft.Extensions.Caching.StackExchangeRedis paketini ekleyin. Program.cs içinde Redis sağlayıcısını kaydedince uygulama servisleri IDistributedCache arayüzünü kullanabilir. Yerel bir Redis örneği için ConnectionStrings__Redis ortam değişkenine localhost:6379 verebilirsiniz. Üretimde bağlantı bilgilerini gizli değerler için kullandığınız yapılandırma kaynağından alın. InstanceName, aynı Redis sunucusunu paylaşan uygulamaların anahtarlarını ayırmak için önek ekler.

var redisConnection = builder.Configuration.GetConnectionString("Redis")
    ?? throw new InvalidOperationException("Redis connection string is missing.");

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = redisConnection;
    options.InstanceName = "catalog:";
});

Önce önbelleği okuyun

Cache-aside akışında servis önce Redis’e bakar. Kayıt varsa JSON’u ürün kartına dönüştürür ve döndürür. Kayıt yoksa EF Core üzerinden yalnızca kartta gösterilen alanları okur ve sonucu süreli olarak Redis’e yazar. Örnekte ProductCard, Id, Name ve ImageUrl alanlarını içeren bir DTO. Servis, cache ve db alanlarını sırasıyla IDistributedCache ve uygulamanın DbContext bağımlılıklarından alır. Bulunamayan ürünler önbelleğe alınmadığı için sonradan eklenen bir ürün ilk sorguda görülebilir. Aynı geçersiz kimlik çok sık isteniyorsa kısa süreli bir yokluk kaydı ayrı bir tasarım kararıdır.

private static string CacheKey(int id) => $"product-card:v1:{id}";

public async Task<ProductCard?> GetCardAsync(int id, CancellationToken ct)
{
    var key = CacheKey(id);
    var json = await cache.GetStringAsync(key, ct);
    if (json is not null)
        return JsonSerializer.Deserialize<ProductCard>(json);

    var card = await db.Products
        .Where(p => p.Id == id)
        .Select(p => new ProductCard(p.Id, p.Name, p.ImageUrl))
        .SingleOrDefaultAsync(ct);

    if (card is null)
        return null;

    await cache.SetStringAsync(key, JsonSerializer.Serialize(card),
        new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
        }, ct);

    return card;
}

Beş dakika örnek bir yaşam süresi (TTL). Mutlak süre, kayıt sık okunsa da her okumada uzamaz. Anahtardaki v1, DTO biçimi uyumsuz değiştiğinde yeni bir anahtar alanına geçmeyi sağlar. Sonuç mağazaya veya dile göre değişiyorsa bu boyutları da anahtara katın. Aksi halde aynı ürün kimliği için farklı isteklere yanlış içerik dönebilir.

Güncellemede kaydı silin

Ürün adı veya görseli güncellendiğinde önce veritabanı işlemini başarıyla tamamlayın, ardından aynı anahtarı RemoveAsync ile silin. Sonraki okuma güncel kartı yeniden oluşturur. Açık bir transaction kullanıyorsanız silme adımı commit sonrasına gelmeli. Ayrı bir ürün listesi de önbellekteyse ürün kartını silmek listeyi yenilemez. Güncellemenin etkilediği anahtarları okuma akışını tasarlarken belirleyin.

// Ürün alanları değiştirildikten sonra
await db.SaveChangesAsync(ct);
await cache.RemoveAsync(CacheKey(product.Id), ct);

Veritabanı güncellemesi ile Redis’ten silme iki ayrı işlemdir. Silme başarısız olursa kaydı ve hatayı izleyin, gerekiyorsa silmeyi güvenilir biçimde yeniden deneyin. Eşzamanlı bir okuma da eski değeri silmeden sonra tekrar yazabilir. TTL, bu örnekte Redis’e yazılan kopyanın ömrünü sınırlar. Okuma sırasında kesin güncellik gereken bir alanı bu akışın dışında tutun.

Kazancı ölçün

Redis’te kaydı olmayan bir ürün için ilk isteği gönderin ve ürün sorgusunun çalıştığını gözlemleyin. Aynı isteği ikinci kez, mümkünse başka bir uygulama kopyasına gönderin. Aynı kartın geldiğini ve veritabanına tekrar gidilmediğini doğrulayın. Ürün adını güncelledikten sonra yeni adın döndüğünü, TTL dolduğunda ise sonraki isteğin veritabanına döndüğünü kontrol edin. Bu senaryoyu hızlandırmak için test ortamında kısa bir TTL kullanabilirsiniz.

Yük testinde isabet oranını, veritabanı sorgu sayısını ve isteklerin p95 gecikmesini birlikte karşılaştırın. Popüler bir anahtarın süresi dolunca eşzamanlı istekler veritabanına yığılabilir. Çok sayıda anahtar aynı anda doluyorsa TTL’ye küçük bir rastgele fark eklemek süreleri dağıtır. Tek bir popüler anahtar için aynı anahtarı dolduran eşzamanlı işleri birleştirmek gerekir. Örnekte Redis hataları çağırana ulaşır. Redis kesintisinde veritabanına geçmeyi seçerseniz, bu trafiği kaldırıp kaldıramadığını da test edin.

Kaynaklar