Caching in ASP.NET Core
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.
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-Demand | Veri talep edildiğinde önbelleğe yazılır. |
Pre-Populate | Uygulama 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.
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.
merhaba core kullanmıyorum asp.net mvc 5.0 bunu uygularsam sorun çıkartırmı
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.