Loading...

.NET ile Cache (Hybrid Cache)


Bu blog yazısı, ".NET'te Cache Kullanımı" serisinin dördüncü bölümü olup, hem bellek içi (in-memory) hem de dağıtılmış (distributed) önbelleklemenin avantajlarını birleştiren Hibrit Önbellek (Hybrid Cache) stratejisini açıklamaktadır. Amaç, performansı maksimize ederken dağıtılmış ortamda veri tutarlılığını sağlamaktır. Makalede, ASP.NET Core Web API'de özel bir HybridCacheService oluşturarak bu yöntemin nasıl uygulanacağına dair basit bir örnek sunulmaktadır.

.NET ile Hybrid Cache Kullanımı: Performans ve Tutarlılık Arasında Denge (Cache Serisi Bölüm 4)

Performans ve ölçeklenebilirlik, modern yazılım mimarilerinin temel taşlarıdır. .NET'te Cache Kullanımı serimizin önceki bölümlerinde bellek içi (in-memory) önbellekleme ile ultra hızlı erişimden ve Redis gibi dağıtılmış önbelleklerle birden fazla sunucu arasında veri tutarlılığından bahsettik. Peki, bu iki dünyanın en iyi yönlerini birleştiren bir çözüm olsaydı? İşte tam da bu noktada Hybrid Cache (Hibrit Önbellek) devreye giriyor!

Bu blog yazısında, Hybrid Cache'in ne olduğunu, neden hem hız hem de tutarlılık arayan uygulamalar için vazgeçilmez bir çözüm olduğunu, sağladığı faydaları, yaygın kullanım alanlarını ve özellikle ASP.NET Core Web API uygulamalarınızda bu güçlü tekniği nasıl uygulayabileceğinize dair basit bir örnekle açıklayacağız.

Hybrid Cache Nedir?

Hybrid Cache, yerel (in-memory) önbelleği dağıtılmış (distributed) bir önbellek ile birleştiren çok katmanlı bir önbellekleme stratejisidir. Temel fikir, en sık erişilen verileri uygulama sunucusunun kendi belleğinde (in-memory) tutarken, daha az erişilen veya tüm uygulama örnekleri arasında tutarlı olması gereken verileri dağıtılmış bir önbellekte (örneğin Redis, Couchbase) depolamaktır.

Bir veri istendiğinde süreç şu şekildedir:

  1. Yerel Önbellek Kontrolü: Önce verinin uygulama sunucusunun kendi bellek içi önbelleğinde olup olmadığına bakılır. Bu, en hızlı erişim katmanıdır.

  2. Dağıtılmış Önbellek Kontrolü: Eğer yerel önbellekte veri yoksa, dağıtılmış önbellek sorgulanır.

  3. Veri Kaynağı Kontrolü: Eğer veri dağıtılmış önbellekte de yoksa, asıl veri kaynağından (örneğin veritabanı veya harici API) çekilir.

  4. Önbelleğe Alma ve Senkronizasyon: Çekilen veri hem dağıtılmış önbelleğe hem de yerel önbelleğe yazılır. Ayrıca, verinin ana kaynakta değişmesi durumunda, dağıtılmış önbellek üzerinden (örneğin Pub/Sub mekanizmaları ile) yerel önbelleklerin geçersiz kılınması (invalidation) sağlanabilir.

Neden Hybrid Cache Kullanmalısınız? Faydaları ve Gerekliliği

Hybrid Cache, tek başına in-memory veya distributed cache kullanmanın sınırlamalarını gidererek önemli avantajlar sunar:

  • Maksimum Performans: Verinin çoğunluğu yerel bellekte tutulduğu için, okuma işlemleri neredeyse anlık olur. Bu, uygulamanın yanıt sürelerini dramatik şekilde iyileştirir.

  • Veri Tutarlılığı: Dağıtılmış önbellek katmanı sayesinde, uygulamanın birden fazla örneği arasında veri tutarlılığı sağlanır. Bir sunucu önbelleği güncellediğinde, diğer sunucuların önbellekleri de uygun şekilde geçersiz kılınabilir veya güncellenebilir.

  • Azaltılmış Ağ ve Veritabanı Yükü: Yerel önbellek, dağıtılmış önbelleğe yapılan gereksiz ağ çağrılarını azaltır. Dağıtılmış önbellek ise veritabanına yapılan çağrıları minimize ederek, hem ağ trafiğini hem de temel veri kaynağının yükünü düşürür.

  • Artırılmış Ölçeklenebilirlik: Uygulama katmanının daha az kaynak tüketmesini ve daha hızlı yanıt vermesini sağlayarak yatay ölçeklenmeyi kolaylaştırır.

  • Daha İyi Dayanıklılık: Dağıtılmış önbellekte geçici bir kesinti olsa bile, yerel önbellekteki veriler bir süre daha hizmet vermeye devam edebilir.

Yaygın Hybrid Cache Kullanım Alanları

Hybrid Cache stratejisi, özellikle aşağıdaki senaryolarda parlaklığını gösterir:

  • Yüksek Trafikli Web Siteleri ve API'ler: Sıkça erişilen statik veya yarı-statik içeriğin (ürün katalogları, haber makaleleri, kullanıcı profilleri) çok hızlı sunulması gereken durumlar.

  • Mikroservis Mimarileri: Mikroservisler arasında paylaşılan, ancak her servisin kendi hızlı erişimli yerel kopyasına ihtiyaç duyduğu veriler.

  • Okuma Yoğun Uygulamalar: Okuma işlemlerinin yazma işlemlerine göre çok daha fazla olduğu, ancak verinin nispeten güncel olması gereken sistemler.

  • Dinamik Konfigürasyon Verileri: Uygulama genelinde kullanılan ve çalışma zamanında değişebilen, ancak her isteği için veritabanından çekilmesi gerekmeyen ayarlar.

ASP.NET Core Web API ile Hybrid Cache Kullanımı

ASP.NET Core'da doğrudan “Hybrid Cache” diye bir sınıf olmasa da, IMemoryCache ve IDistributedCache'i birleştirerek kendi hibrit çözümünüzü kolayca oluşturabilirsiniz. Genellikle bu, özel bir önbellek hizmeti yazmayı ve bu hizmetin içinde her iki önbelleği de kullanmayı içerir.

İşte basit bir örnek ile IMemoryCache ve IDistributedCache'i birleştirerek bir Hybrid Cache yaklaşımı nasıl oluşturulur:

1. NuGet Paketlerini Yükleme:

Hem in-memory hem de dağıtılmış önbellek için gerekli paketleri ekleyin (örneğin Redis için):

Install-Package Microsoft.Extensions.Caching.Memory
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis

2. Custom Cache Service Oluşturma:

using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Text.Json;
using System.Threading.Tasks;

namespace MyWebApp.Services
{
    public interface IHybridCacheService
    {
        Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null);
        Task RemoveAsync(string key);
    }

    public class HybridCacheService : IHybridCacheService
    {
        private readonly IMemoryCache _memoryCache;
        private readonly IDistributedCache _distributedCache;
        private readonly DistributedCacheEntryOptions _defaultDistributedOptions;
        private readonly MemoryCacheEntryOptions _defaultMemoryOptions;

        public HybridCacheService(IMemoryCache memoryCache, IDistributedCache distributedCache)
        {
            _memoryCache = memoryCache;
            _distributedCache = distributedCache;

            // Varsayılan süreleri belirleyin (isteğe bağlı)
            _defaultDistributedOptions = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
                SlidingExpiration = TimeSpan.FromMinutes(2)
            };
            _defaultMemoryOptions = new MemoryCacheEntryOptions()
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
                SlidingExpiration = TimeSpan.FromMinutes(1)
            };
        }

        public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null)
        {
            // 1. Yerel (In-Memory) Cache'ten oku
            if (_memoryCache.TryGetValue(key, out T item))
            {
                return item;
            }

            // 2. Dağıtılmış (Distributed) Cache'ten oku
            var distributedCacheJson = await _distributedCache.GetStringAsync(key);
            if (!string.IsNullOrEmpty(distributedCacheJson))
            {
                item = JsonSerializer.Deserialize<T>(distributedCacheJson);
                // Dağıtılmış cache'ten okunan veriyi yerel cache'e de ekle
                _memoryCache.Set(key, item, _defaultMemoryOptions);
                return item;
            }

            // 3. Veri Kaynağından (Source) çek
            item = await factory();
            
            // Veriyi hem dağıtılmış hem de yerel cache'e yaz
            var jsonToCache = JsonSerializer.Serialize(item);
            var distributedOptions = _defaultDistributedOptions;
            if (absoluteExpirationRelativeToNow.HasValue || slidingExpiration.HasValue)
            {
                 distributedOptions = new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow ?? _defaultDistributedOptions.AbsoluteExpirationRelativeToNow,
                    SlidingExpiration = slidingExpiration ?? _defaultDistributedOptions.SlidingExpiration
                };
            }
            
            await _distributedCache.SetStringAsync(key, jsonToCache, distributedOptions);
            _memoryCache.Set(key, item, _defaultMemoryOptions);

            return item;
        }

        public async Task RemoveAsync(string key)
        {
            _memoryCache.Remove(key); // Yerel cache'ten kaldır
            await _distributedCache.RemoveAsync(key); // Dağıtılmış cache'ten kaldır
            // Not: Gerçek bir sistemde, diğer uygulama örneklerindeki yerel cache'leri
            // geçersiz kılmak için Pub/Sub veya benzeri bir mekanizma gerekir.
        }
    }
}

3. Servis Konfigürasyonu (Program.cs):

Couchbase servisini ve kendi HybridCacheService'inizi Program.cs dosyanızda yapılandırın. (Bu örnekte Redis kullanılmıştır, Couchbase için AddCouchbaseDistributedCache kullanmanız gerekir.)

using MyWebApp.Services; // HybridCacheService için
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.StackExchangeRedis; // Redis için
using System;
using System.Text.Json;
using System.Threading.Tasks;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// In-memory cache servisini ekle
builder.Services.AddMemoryCache();

// Dağıtılmış cache servisini ekle (Örn: Redis)
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("RedisConnection");
    options.InstanceName = "HybridCacheSample_";
});

// Kendi HybridCacheService'imizi bağımlılık enjeksiyonuna ekle
builder.Services.AddScoped<IHybridCacheService, HybridCacheService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

4. appsettings.json'a Redis Bağlantı Dizesi Ekleme:

{
  "ConnectionStrings": {
    "RedisConnection": "localhost:6379,abortConnect=false"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

5. Cache Kullanımı (Örnek Bir Web API Controller'ı):

IHybridCacheService'i enjekte ederek ürün verilerini önbelleğe alın.

using Microsoft.AspNetCore.Mvc;
using MyWebApp.Services;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MyWebApp.Controllers
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }
    }

    [ApiController]
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private readonly IHybridCacheService _hybridCacheService;

        public ProductsController(IHybridCacheService hybridCacheService)
        {
            _hybridCacheService = hybridCacheService;
        }

        [HttpGet("get-products-hybrid")]
        public async Task<IActionResult> GetProductsHybrid()
        {
            string cacheKey = "AllProductsHybrid";

            // Hybrid cache'ten veriyi al veya kaynaktan çek ve önbelleğe al
            var products = await _hybridCacheService.GetOrCreateAsync(
                cacheKey,
                async () =>
                {
                    // Bu kısım normalde veritabanı veya harici API çağrısı olur.
                    // Simülasyon için 3 saniye bekletiyoruz.
                    await Task.Delay(3000);
                    return new List<Product>
                    {
                        new Product { Id = 1, Name = "Laptop Pro", Price = 2500, Category = "Electronics" },
                        new Product { Id = 2, Name = "Gaming Mouse", Price = 75, Category = "Peripherals" },
                        new Product { Id = 3, Name = "Mechanical Keyboard", Price = 120, Category = "Peripherals" }
                    };
                },
                absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(10), // Dağıtılmış ve yerel için max süre
                slidingExpiration: TimeSpan.FromMinutes(2) // Dağıtılmış ve yerel için kayan süre
            );

            return Ok(new { Source = "Hybrid Cache Logic", Data = products });
        }

        [HttpGet("clear-products-hybrid-cache")]
        public async Task<IActionResult> ClearProductsHybridCache()
        {
            string cacheKey = "AllProductsHybrid";
            await _hybridCacheService.RemoveAsync(cacheKey);
            return Ok("AllProductsHybrid cache hem yerel hem de dağıtılmış katmandan temizlendi (diğer sunucular için Pub/Sub gerekli).");
        }
    }
}

Sonuç

Hybrid Cache, .NET uygulamalarınıza hem maksimum performans (yerel önbellek sayesinde) hem de veri tutarlılığı (dağıtılmış önbellek sayesinde) sağlamak için güçlü bir stratejidir. Yüksek trafikli ve dağıtılmış ortamlarda, pahalı veri kaynağı çağrılarını azaltarak ve yanıt sürelerini iyileştirerek kullanıcı deneyimini önemli ölçüde artırabilir. Kendi HybridCacheService'inizi oluşturarak veya mevcut hibrit önbellekleme kütüphanelerini kullanarak bu gücü ASP.NET Core uygulamalarınıza entegre edebilirsiniz.

Bu blog yazısı, .NET'te Cache Kullanımı serimizin üçüncü bölümünü oluşturdu. Serinin önceki bölümlerini (In-Memory ve Dağıtılmış Önbellek) okumanızı ve bu güçlü önbellekleme stratejilerini kendi projelerinizde deneyimlemenizi şiddetle tavsiye ederiz. Gelecek bölümlerde daha ileri düzey önbellekleme konularını ele alacağız!