Cache Nedir? Cache Tipleri ve .NET Core In-Memory Cache Kullanımı

Enes Çakır
Neredekaltech
Published in
7 min readDec 20, 2023

Cache, In-Memory Cache ve .NET Core’ da nasıl kullanıldığına dair elimden geldiği kadar yol gösterici bir yazı yazmaya çalıştım umarım sizler için faydalı olur. Cache’in ne olduğunu açıklayarak giriş yapalım.

Caching nedir?

Çok sık kullanılan verilerin geçici olarak bir alanda saklanmasıdır. Bahsedilen bu alan çoğunlukla RAM’ dir. RAM’ler diğer depolama alanlarına göre çok daha hızlı okuma/yazma işlemleri yapabilen donanımlardır böylece sık erişilen verilerimizi RAM’ de tutarak kullanıcılara çok daha hızlı cevaplar dönmemize olanak sağlar.

Hangi veriler cache’lenmelidir?

Performans açısından çok sık güncellenmeyen fakat çok sık erişilen verileri cache’lemek en mantıklı olanıdır.

Veriler ne zaman cache’lenmelidir?

Verileri talep üzerine cache’leyebilir (on-demand yöntemi) veya çok sık kullanılacağı öngörülen verileri uygulama ayağa kaldırıldığı esnada cache’leyebilirsiniz.

Cache ömrü ne kadar olmalıdır?

Uygulamanızın amacına, akışına, verilerin erişim ve güncellenme sıklığına göre değişkenlik gösterebilir. Bu parametreleri göz önünde bulundurarak en uygun cache ömrünü belirleyebilirsiniz. İki tip cache ömrü vardır bunlardan birincisi mutlak (absolute) zamandır. Mutlak zamanda cache’in ömrü sabittir ve belirlenen tarih-saat geldiğinde cache hafızadan silinir. İkinci cache ömrü ise değişken (sliding) zamandır. Bu tipte ise cache’ e her erişildiğinde cache’in ömrü verilen değer (zaman) kadar uzatılır.

Sadece sliding time kullanmak verilerin güncelliğini yitirmesine sebep olabilir. Cache’lenen verinin süresi dolmadan sürekli çağırıldığını fakat bu süre zarfında asıl verinin güncellenmiş olduğunu düşünelim. Veri cache’den düşemediği için güncel veri asla cache’lenemeyecektir. Bu sebeple sliding time, absolute time ile kullanılarak cache’in döngüye girmesi engellenir.

Cache Tipleri Nedir?

Distributed (Shared) Cache ve In-Memory (Private) Cache adları verilen iki tip cache bulunmaktadır.

Distributed (Shared) Cache

Ayağa kaldırılan uygulamadan bağımsız olarak çalışan cache servisleridir. Birden fazla instance’ da çalışan uygulamaların tek bir cache servisini kullanmasını sağlayarak uygulamalar arasındaki cache tutarsızlık probleminin (bu tutarsızlık problemini In-Memory Cache içerisinde açıklayacağım) önüne geçer.

Kullanabileceğiniz bazı cache servisleri:
- Redis
- Memcached
- ElasticCache (AWS)

In-Memory (Private) Cache

Verilerin ayağa kaldırılan uygulamanın bulduğu sunucu veya makinenin RAM’ inde tutulduğu bir caching yöntemidir. Uygulama kapatıldığında, yeniden başlatıldığında yani kısacası deploy alındığında cache temizlenir.

In-Memory Cache Tutarsızlık Problemi
In-Memory cache yöntemini kullanan bir uygulama iki farklı sunucuda çalışıyorsa kullanılan Load Balancer’ın yönlendirmesine bağlı olarak farklı zamanlarda cache’lenmiş veriler arasında farklılık olabilir.

In-Memory Cache Tutarsızlık Problemi

Bir uygulamanın iki farklı sunucuda ayakta olduğu örnek yapı düşünelim.

Bu yapıda x zamanında yapılan istek doğrultusunda kullanıcı Load Balancer tarafından A1 makinesine yönlendirildi ve bu sebeple x zamanında “data1_v1” verisi A1 makinesinde cache’lendi. Fakat daha sonra bir sebepten dolayı bu data güncellenerek veri tabanında “data1_v2” halini aldı.

Başka bir kullanıcı y zamanında aynı isteği attığında load balancer kullanıcıyı A2 makinesine yönlendirdi ve bu sebeple y zamanında “data1_v2” verisi A2 makinesinde cache’lendi.

Görselde de görüldüğü üzere iki farklı makinede bir verinin iki farklı versiyonu bulunmaktadır. Yapılan her istekte Load Balancer’ın yönlendirmesine bağlı olarak güncel veya eski veriyi görürüz. Herhangi bir sitede sayfayı her yenilediğinizde verilerin farklı olduğunu gördüğünüz bir an yaşadıysanız bunun sebebinin In-Memory Cache tutarsızlık probleminden kaynaklandığını söyleyebilirim.

Bu problemin çözümü nedir?
Tam anlamıyla bir çözümü yoktur fakat Sticky Session adı verilen kısmi bir çözüm mevcuttur. Load Balancer istek atan her kullanıcıyı hangi makineye yönlendirdiğini kaydeder ve aynı kullanıcı tarafından atılan isteklerde aynı makineye yönlendirir böylece kullanıcı sayfayı her yenilediğinde farklı veriler ile karşılaşmamış olur kullanıcı özelinde veri tutarlılığı sağlanır.

Sticky Session, bu çözümün yanı sıra RAM kullanımını azaltarak avantajlı bir durum sağlamış gibi görünse de Load Balancer’ın dengeli çalışmasını engeller.

.NET Core In-Memory Cache Kullanımı

Birazdan okuyacağınız örnekte yer alan kodlara github hesabımdaki repodan erişebilirsiniz.

Hazırlık

Bu örnekte bir veri tabanı oluşturmak yerine JSONPlaceholder sahte API kullanmayı tercih ettim. Bir .NET 7.0 Core API projesi oluşturup içini temizledikten sonra MemoryCacheController ve AppService oluşturdum.

Sahte API’den gelecek olan veriye uygun bir model:

public class Todo
{
public int Id { get; set; }
public int UserId { get; set; }
public string? Title { get; set; }
public bool Completed { get; set; }
}

…ve sahte API endpointlerinin olduğu bir sınıf oluşturdum:

public class Urls
{
public static string BaseUrl = "https://jsonplaceholder.typicode.com";
public static string GetAll = "/todos";
public static string GetById = "/todos/{0}";
}

In-Memory Cache’ i kullanabilmek için Program.cs’ de gidip AddMemoryCache() servisimizi eklememiz gerekmektedir:

var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddMemoryCache();
...
var app = builder.Build();

Artık oluşturduğumuz servisimize API isteklerimiz için HttpClient sınıfını daha sonra da Dependency Injection ile IMemoryCache’i dahil ederek cache ile ilgili aksiyonları kullanabiliriz. Cache’de tutulacak veriler için basit iki anahtar kelimeyi de readonly olarak tanımlıyoruz.

public class AppService : IAppService
{
private readonly HttpClient _client;
private readonly IMemoryCache _memCache;

private readonly string TODOS_KEY = "TODOS";
private readonly string TODO_DETAIL_KEY = "TODO_{0}";

public AppService(IMemoryCache memCache)
{
_client = new HttpClient();
_memCache = memCache;
}
...
....
.....
}

Get(…) Metodu

Verilen anahtar kelimeye karşılık gelen bir cache verisi var ise bunu size döner.

var cacheData = _memCache.Get<Todo>(string.Format(TODO_DETAIL_KEY, id));

TryGetValue(…) Metodu

İlk parametre olarak verilen anahtar kelime cache’de mevcut ise out anahtar sözcüğüyle ikinci parametre üzerinden döndürür. Metodun kendi dönüş tip boolean’dır.

var isExist = memCache.TryGetValue(TODOS_KEY, out List<Todo>? cachedata);

if(isExist)
return cacheData;
...

Set(…) Metodu

Veriyi cache’e kaydeder, bunu örnek senaryo ile açıklayacağım. İlk olarak bir verinin cache’deki varlığını kontrol edip var ise cache’deki veriyi, yoksa sahte API’ye istek atarak gelen veriyi önce cache’leyip daha sonra da kullanıcıya cevap olarak döneceğiz:

public Todo GetTodo(int id)
{
var cacheData = _memCache.Get<Todo>(string.Format(TODO_DETAIL_KEY, id));
if (cacheData != null)
return cacheData;

var todo = _client.GetFromJsonAsync<Todo>(string.Concat(Urls.BaseUrl, string.Format(Urls.GetById, id)), default).Result;
if (todo == null)
return new();

_memCache.Set(string.Format(TODO_DETAIL_KEY, id), todo, DateTime.Now.AddMinutes(2));

return todo;
}

Basit kullanımında Set(…) metodu 3 parametre almaktadır, bunlar: KEY, DATA, ABSOLUTE_TIME örnekte de görüldüğü üzere cache ömrü 2 dakika olarak verildi. Uygulama ayağa kaldırıldıktan sonra buraya atılan ilk istekte cache’de veri mevcut olmadığı için önce sahte API’den veriyi alıp cache’ledikten sonra da cevap olarak dönmektedir. Buraya atılan ikinci istekte ise cache’in ömrü dolmadıysa veriler cache’den getirilecektir.

Set(…) metodunun diğer bir kullanımında ise 3.parametre olarak MemoryCacheEntryOptions sınıfından bir nesne vererek absolute time’ın yanı sıra sliding time, verinin cache’de kaplayacağı alan ve cache önceliği gibi değerleri de verebilirsiniz. Örneğin:

var options = new MemoryCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(15),
SlidingExpiration = TimeSpan.FromMinutes(2),
Priority = CacheItemPriority.High,
Size = 1
};

_memCache.Set(string.Format(TODO_DETAIL_KEY, id), todo, options);

GetOrCreateAsync(…) Metodu

Bu metot yukarıda manuel olarak yapmış olduğumuz senaryoyu birazcık daha kolaylaştırmaktadır. Veri cache’de mevcut ise getirir, değil ise API’ye istek atarak verinin getirilip cache’lenmesi sağlanır.

public async Task<List<Todo>> GetTodos()
{
var todos = await _memCache.GetOrCreateAsync(TODOS_KEY, async factory =>
{
factory.SlidingExpiration = TimeSpan.FromSeconds(20);
factory.AbsoluteExpiration = DateTime.Now.AddMinutes(2);
factory.Priority = CacheItemPriority.High;

var data = await _client.GetFromJsonAsync<List<Todo>>(string.Concat(Urls.BaseUrl, Urls.GetAll), default);
if (data == null || !data.Any())
return new();
return data;
});

if (todos == null || !todos.Any())
return new();

return todos;
}

GetOrCreateAsync(…) ilk parametresi olan anahtar kelimeye ait bir cache mevcut değil ise ikinci parametrede verilen delegeyi çalıştırır. Bu delege içerisinde MemoryCacheEntryOptions sınıfın sağladığı özellikleri kullanabilirsiniz. Delege’den dönülen veri bu metot aracılığıyla kendiliğinden cache’lenecektir.

Remove(…) Metodu

Verinin cache’den silinmesini sağlar. Parametre olarak silinmesi istenen cache’in anahtar kelimesini vermenizi bekler.

public void RemoveFromCache(string key)
{
_memCache.Remove(key);
}

.NET Core Custom In-Memory Cache Kullanımı

.NET Core cache yapısını özelleştirmemize olanak sağlamaktadır, şimdi bunu nasıl yapabileceğimize bir göz atalım.

Öncelikle yeni bir sınıf oluşturup MemoryCache tipinde sadece get edilebilen bir property tanımlayıp aynı zamanda new anahtar sözcüğüyle oluşturulmasını sağlıyoruz. MemoryCache sınıfı kendisinden nesne oluşturulurken mutlaka bir MemoryCacheOptions sınıfı nesnesi bekler işte tam burada cache’de özelleştirmek istenen alanları belirtebiliriz.

public class AppCustomCache
{
public MemoryCache Cache { get; } = new(new MemoryCacheOptions()
{
SizeLimit = 1024, //cache kapasitesini tanımlar
TrackStatistics = true //cache istatistiklerini takip etmemizi sağlar
ExpirationScanFrequency = TimeSpan.FromMinutes(5), //cache ömrü kontrolünün yapılma sıklığı
CompactionPercentage = 0.25 //cache kapasitesi dolduğunda yeni verilere yer açmak için yüzdelik dilimde ne kadarının sileceğini belirtir
});
}

Bu sınıfı Program.cs’de container’a ekledikten sonra cache işlemlerini Cache property’sinin özelliklerinden faydalanarak gerçekleştireceğiz.

var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddSingleton<AppCustomCache>();
...
var app = builder.Build();

AppService’te DI ile sınıfımızı verdikten sonra ilk metodumuzu test edebiliriz.

public class AppService : IAppService
{
private readonly HttpClient _client;
private readonly AppCustomCache _memCache;

private readonly string TODOS_KEY = "TODOS";
private readonly string TODO_DETAIL_KEY = "TODO_{0}";

public AppService(AppCustomCache memCache)
{
_client = new HttpClient();
_memCache = memCache;
}
...
....
.....
}

Get(…) Metodu

Önceki kullamıyla birebir aynıdır.

var cacheData = _memCache.Cache.Get<Todo>(string.Format(TODO_DETAIL_KEY, id));

Set(…) Metodu

Önceki kullamıyla neredeyse aynıdır fakat custom cache yapısında cache’e kaydedeceğiniz her verinin cache’de yer kaplayacağı alanı belirtmeniz zorunludur. Bu sebeple Set(…) metodunun MemoryCacheEntryOptions nesnesi kabul eden overload’ını kullanmamız gerekmektedir.

var cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpiration = DateTime.Now.AddMinutes(1),
Size = 1
};

var cacheData = _memCache.Cache.Set(string.Format(TODO_DETAIL_KEY, id), todo, cacheOptions);

GetOrCreateAsync(…) Metodu

Bu metodda da cache’e kaydedilmek için bir Size değeri belirtmek zorundasınız.

var todos = await _memCache.Cache.GetOrCreateAsync(TODOS_KEY, async opt =>
{
opt.SlidingExpiration = TimeSpan.FromSeconds(20);
opt.AbsoluteExpiration = DateTime.Now.AddMinutes(2);
opt.Size = 1;
opt.Priority = CacheItemPriority.High;

var data = await _client.GetFromJsonAsync<List<Todo>>(string.Concat(Urls.BaseUrl, Urls.GetAll), default);
if (data == null || !data.Any())
return new();

return data;
});

Remove(…) Metodu

Önceki kullamıyla birebir aynıdır.

public void RemoveFromCache(string key)
{
_memCache.Cache.Remove(key);
}

Clear(…) Metodu

Cache’in tamamını temizler.

public void ClearCache()
{
_memCache.Cache.Clear();
}

Compact(…) Metodu

Double tipinde parametre alır ve verdiğiniz değere karşılık gelen yüzdelik dilimdeki (0.25 = %25) cache’i temizler.

public void CompactCache(double percantage)
{
_memCache.Cache.Compact(percantage);
}

Bu temizleme esnasında aşağıdaki şartlara sırasıyla dikkat eder:

1- Son kullanma tarihi geçmiş tüm öğeler
2- CacheItemPriority değerine göre
3- En son kullanılan nesneler
?- Absolute time’ı en yakın olan öğeler
?- Sliding time’ı en yakın olan öğeler
?- Daha büyük nesneler

Son üç şartın net bir sıralaması bulunmamaktadır, kendi aralarında uygulanma sırası değişebilir.

GetCurrentStatistics(…) Metodu

Mevcut cache’in istatistiklerini görmemizi sağlar, kullanmak için oluşturulan cache sınıfında TrackStatistics = true olarak belirtilmesi gerekmektedir.

public MemoryCacheStatistics GetCacheStatistics()
{
return _memCache.Cache.GetCurrentStatistics()!;
}

MemoryCacheStatistics tipinde veri döner. Bu sınıfta ise aşağıdaki istatistikler yer alır:

CurrentEntryCount -> Cache’deki veri sayısı,
CurrentEstimatedSize -> Cache’deki verilerin kapladığı alan,
TotalMisses -> Cache’de bulamadığı veri sayısı,
TotalHits -> Cache’de bulduğu veri sayısı

Clear(…) ve Compact(…) metotları çalıştırıldığında istatistik alanlarından TotalMisses ve TotalHits temizlenmez.

Vakit ayırıp okuduğunuz için teşekkür ederim, performanslı kodunuz bol olsun 🤓

--

--