Caching in ASP.NET Core

asp-net-core-caching

Cache önceden elde edilmiş verilerin sonradan kullanılmak üzere önbellekte saklanmasıdır. Sık kullanılan ve nadir güncellenen ya da üretilmesi maliyetli verilere erişim böylelikle hızlanmış olur. Verinin nerede nasıl saklanacağı Caching olarak adlandırılır. In-Memory olarak aynı sunucu önbelleğinde veya Distributed olarak farklı bir sunucu önbelleğinde depolanabilmektedir.

asp-net-caching

Genel haliyle süreç, bir request sonucu (1) talep edilen verinin öncesinde önbelleğe yazılıp yazılmadığına bakılarak işlemektedir. Veri önbellekte mevcutsa (2) elde edilerek; aksi durumda talep edilen verinin, veri kaynağından (3) elde edilip önbelleğe (4) yazılmasıyla response (5) dönülür. Böylece veri kaynağı, çoğunlukla bir veritabanı, trafiği azaltılarak isteklere ziyaretçi sayısından bağımsız şekilde daha hızlı cevap verilmiş olunur.

Önbellekte depolanması planlanan verinin ne zaman önbelleğe yazılacağı Cache Strategy olarak adlandırılmaktadır.

On-DemandVeri talep edildiğinde önbelleğe yazılır.
Pre-PopulateUygulama ayağa kalkar kalkmaz ilgili veri elde edilerek önbelleğe yazılır.

Önbelleğe alınmış verinin ne kadar saklanacağı iki yaklaşımla belirlenmektedir. Saklanacak verinin süresinin iyi ayarlanması gerekmektedir; aksi durumda güncelliğini yitirmiş bayat verilerle karşılaşılacaktır.

Absolute TimeÖnbelleğe alınmış veri için mutlak bir son kullanım tarihi belirler.
Sliding TimeÖnbelleğe alınmış veriyi belirtilen süre kadar saklar, bu süre içerisinde yeni istek alındığında süre sıfırlanır; istek alınmazsa veri önbellekten temizlenir. Sık istek alacak yerlerde AbsoluteTime ile kullanılması önerilmektedir. Böylece olası veri güncellemesi sonrasında bayat veri son kullanım tarihinden sonra tazelenmiş olacaktır.

In-Memory Caching

Önbelleğe alınacak verinin yukarıdaki örnekte olduğu üzere uygulamanın çalıştığı sunucu RAM belleğinde saklandığı yaklaşımdır. Dolayısıyla depolanacak veri bellek miktarıyla doğrudan orantılıdır.

Bu yaklaşımda uygulamanın birden çok örneğe sahip olduğu senaryolarda bir handikap mevcuttur. Örneklere yapılacak istekler arasında veri güncellemesi olursa örnekler arasında veri tutarsızlığı olacak ve her istek farklı sonuçla dönecektir. Bu senaryoda Sticky-Session kullanılarak aynı ziyaretçinin IP Adresi veya Session Cookie kullanılarak aynı örneğe yönlendirilmesi sağlanabilir.

Örneklerden biri halen bayat veriye sahip olacağından ve veri tutarsızlığı devam edeceğinden dolayı birden çok örneğe sahip olunan senaryolarda Distributed Cache yaklaşımının tercih edilmesi daha uygun olacaktır. Örneğimize projemizi oluşturarak başlayalım.

dotnet new webapi -n InMemoryCaching

Bu yaklaşım IMemoryCache arayüzünü temel alır. Cache mekanizmasını built-in IoC Container bünyesine Program.cs içerisinden kaydedelim.

builder.Services.AddMemoryCache();

Böylece IMemoryCache arayüzünü istediğimiz yerden inject ederek cache işlemlerini gerçekleştirebiliriz.

public class CacheController : ControllerBase
{
    private readonly IMemoryCache _memoryCache;

    public CacheController(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    [HttpGet("{key}")]
    public IActionResult Get(string key)
    {
        return Ok(_memoryCache.Get<string>(key));
    }

    [HttpPost("{key}")]
    public IActionResult Set(string key, string value)
    {
        if (value is null) return BadRequest();
        return Ok(_memoryCache.Set<string>(key, value));
    }

    [HttpDelete("{key}")]
    public IActionResult Delete(string key)
    {
        _memoryCache.Remove(key);
        return Ok();
    }
}

Yukarıda Get methodu kullanılarak ilgili cache elde edilmektedir. Yine Set methoduyla ilgili değerler önbelleğe yazılmakta ve Remove methoduyla belirtilen cache önbellekten silinmektedir. Bu methodlar ilgili key ve value değerleriyle kullanılmaktadır.

[HttpGet("{key}/try")]
public IActionResult TryGet(string key)
{
    if (_memoryCache.TryGetValue<string>(key, out string value))
    {
        return Ok(value);
    }
    return NotFound();
}

[HttpGet("{key}/create/{value}")]
public IActionResult GetOrCreate(string key, string value)
{
    var val = _memoryCache.GetOrCreate<string>(key, factory =>
    {
        factory.AbsoluteExpiration = DateTime.Now.AddSeconds(30);
        factory.SlidingExpiration = TimeSpan.FromSeconds(5);
        factory.Priority = CacheItemPriority.High;
        return value;
    });
    return Ok(val);
}

Yukarıda farklı işlevleri olan TryGetValue methoduyla belirtilen değere sahip cache elde edilerek sonuç bool türünde döndürülmektedir. İşlem başarılıysa ilgili değer out modifier ile belirtilen değişkene atanmaktadır. GetOrCreate methoduyla belirtilen cache elde edilemezse girilen parametrelerle yeni bir tane oluşturulmaktadır.

SlidingExpiration özelliğiyle cache ömrü her istek alındığında beşer saniye uzatılacaktır; ancak AbsoluteExpiration özelliği de belirtildiğinden cache ömrü en fazla otuz saniye olacaktır. Veriler bellekte depolandığından depolanacak veri boyutu bellek miktarıyla sınırlıdır. Priority özelliğiyle bellek dolması durumunda silinme öncelikleri belirtilmektedir. Önem sırasına göre Low, Normal, High ve NeverRemove değerlerini alarak, önceliği düşük olan bellekten daha önce silinir.

[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(string id)
{
    var cacheKey = $"Post-{id}";
    if (!_memoryCache.TryGetValue<Post>(cacheKey, out Post? post))
    {
        post = await _postService.GetAsync(id);

        if (post is null) return NotFound();

        _memoryCache.Set<Post>(cacheKey, post, new MemoryCacheEntryOptions
        {
            AbsoluteExpiration = DateTime.Now.AddSeconds(10),
            SlidingExpiration = TimeSpan.FromSeconds(5),
            Priority = CacheItemPriority.High,
            PostEvictionCallbacks = 
            {
                {
                    new PostEvictionCallbackRegistration
                    {
                        EvictionCallback = (key, value, reason, state) => 
                            _logger.LogInformation($"Cache with key: {key} is evicted.")
                    } 
                }
            }
        });
    }
    return Ok(post);
}

Yukarıda gönderilen id değerine karşılık gelen makaleyi getiren bir örnek yer almaktadır. Burada önbellek öncelikle TryGetValue methoduyla kontrol edilmektedir. Önbellekte birden fazla makale olabileceğinden key değerinin unique olduğuna dikkat edin. İlgili veri öncesinde önbelleğe alınmışsa out modifier üzerinden elde edilerek if bloğuna girilmeden istek sonlandırılır. Aksi durumda makale veri kaynağından elde edilerek Set methoduyla önbelleğe alınarak ilgili değişkene atanır ve istek sonlandırılır. PostEvictionCallbacks olayı bellekten bir cache silindiğinde tetiklenir.

Distributed Caching

Önbelleğe alınacak verilerin uygulama sunucusu yerine ayrı bir cache sunucusunda tutulduğu yaklaşımdır. Uygulama sunucuları böylece aynı önbelleği paylaşarak veri tutarsızlıklarının önüne geçerler. Uygulamanın performansı ve ölçeklenebilirliği artarken cache uygulama bünyesinde tutulmadığından erişim nispeten yavaşlar.

distributed caching

ASP.NET Core Distributed Memory Cache, NCache Cache, SQL Server Cache ve Redis Cache gibi çeşitli Distributed Cache implementasyonlarını destekler. Bu implementayonlar IDistributedCache arayüzünü temel aldığından birinden diğerine geçmek oldukça kolaydır.

dotnet new webapi -n DistributedCaching

Örnek projemizi oluşturup bir kaç implementasyonu sağlayalım.

Distributed Memory Cache

Önbelleğe alınacak verilerin uygulama sunucusunun belleğinde tutulduğu basit bir IDistributedCache implemantasyonudur. Gerçek bir dağıtık önbellek altyapısı sunmamaktadır. Bu sebeple production ortamında tek bir örnek ile çalışılıp aynı zamanda uygulamanın ölçeklendirme esnekliğine de sahip olması istenen durumlarda tercih edilmelidir. Ölçeklendirme ihtiyacı doğması durumunda farklı bir implementasyona kolaylıkla geçilebilir.

builder.Services.AddDistributedMemoryCache();

Cache mekanizmasını IoC Container bünyesine Program.cs içerisinden kaydedelim. Uygulama IDistributedCache arayüzünü böylelikle dilediği yerden inject edebilir.

Redis Cache

Redis açık kaynaklı popüler in-memory caching servislerinden biridir. Database, Cache ve Message Broker olarak kullanılabilir. Verileri key-value pair şeklinde tutmaktadır. Öncelikle Microsoft.Extensions.Caching.StackExchangeRedis NuGet paketinin kurulması gerekmektedir.

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration["Redis:Configuration"];
    options.InstanceName = builder.Configuration["Redis:Instance"];
});

Cache mekanizmasını IoC Container bünyesine Program.cs dosyasından yukarıdaki şekilde kaydedelim. Uygulama ayağa kalktığında Redis Server varsayılan 6379 portunda çalışıyor olmasına dikkat edilmelidir. Arayüz methodları aşağıdaki gibidir.

GetStringÖnbellekten key değerine karşılık gelen veriyi string türünde getirir.
SetStringÖnbelleğe key değerinde string türünden veri ekler.
RemoveÖnbellekten key değerine karşılık gelen veriyi siler.
GetÖnbellekten key değerine karşılık gelen veriyi byte[] dizisi türünde getirir.
SetÖnbelleğe key değerinde byte[] dizisi türünden veri ekler.
RefreshÖnbellekte key değerindeki veriyi ve SlidingExpiration belirtilmişse zaman aşımını yeniler.

Yeniden kullanılabilirliği arttırmak üzere IDistributedCache arayüzüyle çalışan bir kaç extension method yazalım.

public static class CacheExtensions
{
    public static Task SetAsync<T>(this IDistributedCache cache, string key, T value)
    {
        return SetAsync<T>(cache, key, value, new DistributedCacheEntryOptions());
    }
    
    public static Task SetAsync<T>(this IDistributedCache cache, string key, T value, DistributedCacheEntryOptions options)
    {
        var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value));
        return cache.SetAsync(key, bytes, options);
    }

    public static bool TryGetValue<T>(this IDistributedCache cache, string key, out T? value)
    {
        var val = cache.Get(key);
        value = default;
        if (val is null) return false;
        value = JsonSerializer.Deserialize<T>(val);
        return true;
    }
}

Yukarıda SetAsync methodunda json ve binary serileştirme işlemleri yapılarak kod tekrarının önüne geçilmektedir. Aynı şekilde TryGetValue methodu IMemoryCache arayüzünde olduğu gibi belirtilen key değerine karşılık gelen değeri elde etmeye çalışmaktadır.

public class PostsController : ControllerBase
{
    private readonly IPostService _postService;
    private readonly IDistributedCache _distributedCache;

    public PostsController(IPostService postService, IDistributedCache distributedCache)
    {
        _postService = postService;
        _distributedCache = distributedCache;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetAsync(string id)
    {
        var cacheKey = $"Post-{id}";
        if (!_distributedCache.TryGetValue<Post>(cacheKey, out Post? post))
        {
            post = await _postService.GetAsync(id);

            if(post is null) return NotFound();

            var options = new DistributedCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromSeconds(10))
                .SetSlidingExpiration(TimeSpan.FromSeconds(5));

            await _distributedCache.SetAsync<Post>(cacheKey, post, options);
        }
        return Ok(post);
    }
}

Yukarıda in-memory cachingte olduğu gibi IDistributedCache arayüzünü constructor üzerinden inject etmekteyiz. Yazmış olduğumuz TryGetValue extension methoduyla unique olarak belirlenmiş key ile veri önbellekte kontrol edilmektedir. Verinin bulunamaması durumunda aynı key değeriyle yeni bir cache SetAsync extension methoduyla oluşturulmaktadır.

Redis aynı zamanda String, List, Set, Sorted Set ve Hash gibi veri yapılarını desteklemektedir. Şimdiye değin IDistributedCache arayüzünün sunduğu özellikler kullanıldı. Önbellekte verilerimizi ilgili veri yapılarını kullanarak da tutabiliriz. Bunun için StackExchange.Redis NuGet paketine ihtiyaç duyulmaktadır. İlgili implementasyona Redis yazısından ve projenin tamamına GitHub üzerinden erişebilirsiniz.

You may also like...

2 Responses

  1. serdar dedi ki:

    merhaba core kullanmıyorum asp.net mvc 5.0 bunu uygularsam sorun çıkartırmı

    • Burak Neiş dedi ki:

      Merhaba, Mvc versiyonu yerine kullandığın .NET Framework ya da Core versiyonuna bakmalısın. İkisi arasında çok bir fark yok yalnızca uygun paketi kurman gerekiyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir