In-Memory Caching, .NET dünyasında performans artırmak ve gereksiz veri tabanı sorgularını azaltmak için kullanılan, verilerin RAM'de tutulduğu hızlı ve pratik bir önbellekleme yöntemidir. Bu notta, IMemoryCache ile in-memory cache'in temelleri, kullanım senaryoları, kurulum, gelişmiş ayarlar, thread safety, best practice'ler ve gerçekçi kod örnekleriyle tüm detayları bulabilirsiniz.
In-Memory Caching
IMemoryCache
.NET
Cache Expiration
Cache Size Limit
Compaction
Thread Safety
Cache Wrapper
Region Namespace
Performance Optimization
Cache Monitoring
Best Practices
Cache Implementation
MemoryCache
Cache Design Patterns
Cache Operations
Cache için .NET dünyasında performans artırmak ve gereksiz veri tabanı sorgularını azaltmak için en çok başvurulan tekniklerden biri In-Memory Caching'dir. Özellikle web uygulamalarında, sık erişilen verileri bellekte (RAM) tutarak hem uygulamanın hızını artırır hem de altyapı maliyetlerini düşürür. Bu yazıda, bir Junior developer'ın aklındaki tüm soruları giderecek şekilde, IMemoryCache ile in-memory cache kullanımını, temel kurulumları, örnekleri ve dikkat edilmesi gereken tüm noktaları adım adım anlatacağım.
Neden Cache Kullanırız?
Cache, bir veriyi daha hızlı ulaşmak için geçici olarak saklama yöntemidir.
Bir uygulama çalışırken birçok veriyle etkileşim halindedir; örneğin kullanıcı bilgileri, ürün listeleri, kategoriler, roller, yetkiler gibi veriler.
Bu veriler çoğu zaman bir veritabanından ya da başka bir kaynaktan (API, dosya vs.) alınır.
Ancak bu kaynaklardan her seferinde veri çekmek:
⏳ Yavaş olabilir (özellikle uzak veritabanı ya da API'ler)
🔁 Sürekli tekrar eden veri talepleri aynı işlemleri zorunlu kılar
⚙️ Sunucu kaynaklarını (CPU, bellek, IO) gereksiz yere tüketir
🌐 Trafiği arttırır, özellikle dış servis kullanılıyorsa ağ maliyetine yol açar
İşte tam bu noktada cache devreye girer.
🧊 Cache, temel olarak şu mantıkla çalışır:
“Bir veriye daha önce ulaşmışsam, tekrar ihtiyaç duyduğumda doğrudan belleğimden getireyim.”
Bu sayede:
🔥 Performans ciddi şekilde artar
🧪 Daha az veri sorgusu çalıştırılır (özellikle veritabanı için)
🧠 Bellek kullanımı kontrollü yapılırsa kaynaklar verimli kullanılır
🧱 Cache Yapıları: In-Memory vs Distributed Cache
Daha önceki yazılarımda bahsettiğimiz gibi, iki temel cache yaklaşımı vardır:
1. 🗂️ In-Memory Cache
Uygulamanın kendi belleğini kullanır (RAM)
Çok hızlıdır çünkü veriler doğrudan sunucunun belleğinde tutulur
Kullanımı basittir (IMemoryCache gibi servisler ile)
Genellikle tek sunuculu uygulamalarda tercih edilir
Uygulama restart olduğunda cache silinir
Yatay ölçeklenebilir sistemlerde sorun yaratabilir çünkü her sunucunun kendi cache'i olur
Avantajları:
Hızlıdır
Kolay yapılandırılır
Harici bir sistem gerekmez
Dezavantajları:
Uygulama belleğini tüketir
Veriler sadece bir sunucuda tutulur (senkronizasyon yok)
2. 🌐 Distributed Cache
Veriler harici bir sistemde saklanır (örneğin Redis, SQL Server)
Birden fazla sunucu bu cache'e erişebilir
Yüksek erişilebilirlik ve ölçeklenebilirlik sağlar
Mikroservis mimarilerinde ya da büyük ölçekli projelerde tercih edilir
Genellikle Redis gibi araçlarla uygulanır
Avantajları:
Tüm sunucular aynı veriye erişir (paylaşımlı cache)
Uygulama yeniden başlasa bile cache silinmez (Redis gibi kalıcı sistemlerde)
Yüksek trafiği daha iyi yönetir
Dezavantajları:
Harici servis kurulumu gerekir (örnek: Redis sunucusu)
Ağ üzerinden erişildiği için In-Memory kadar hızlı değildir
Yönetim ve izleme daha karmaşık olabilir
🧩 Hangi Durumda Hangisini Seçmeliyiz?,
Senaryo
Tercih Edilen Cache
Tek sunuculu küçük/orta ölçekli API
✅ In-Memory Cache
Gerçek zamanlı uygulama (chat, oyun vs.)
✅ In-Memory Cache
Birden fazla sunucu varsa (load balancing)
✅ Distributed Cache
Mikroservis yapısı kullanılıyorsa
✅ Distributed Cache
Uygulama kapanınca verinin silinmemesi isteniyorsa
✅ Distributed Cache
Anlık performans çok kritikse (milisaniyelik hesaplamalar)
✅ In-Memory Cache
Bu yazı dizisinde In-Memory Cache konusuna odaklanacağız.
Çünkü özellikle tek başına geliştirilen projelerde, monolit yapılı uygulamalarda ve başlangıç seviyesinde en hızlı ve en pratik çözüm In-Memory cache kullanmaktır.
Redis gibi Distributed cache sistemleriyle ilgili konulara daha önce ayrı bir yazı serisinde değinmiştik. Eğer Redis tarafındaki SET, GET, TTL, EXPIRE, GEO gibi konularla ilgileniyorsan o seriye göz atmanı öneririm.
📋 In-Memory Caching Özellikleri
Özellik
Açıklama
Örnek Kod/Not
Bellekte Saklama
Veriler RAM'de tutulur, uygulama kapanınca silinir
IMemoryCache
Çok Hızlı Erişim
Disk/DB yerine RAM'den okuma, milisaniyelik yanıtlar
-
Zaman Aşımı (TTL)
Her veri için ayrı süreyle otomatik silinebilir
AbsoluteExpiration, SlidingExpiration
Key-Value Yapısı
Her veri bir anahtar (key) ile tutulur
cache.Set("user:1", userObj)
Thread-Safe
Çoklu isteklerde güvenli, eşzamanlı erişim
-
Dependency Desteği
Birden fazla cache birbirine bağlanabilir
PostEvictionCallback
Kolay Entegrasyon
.NET Core ile native gelir, ek paket gerekmez
-
🎯 In-Memory Cache Kullanım Senaryoları
Kullanım Alanı
Açıklama
Kod Örneği
Sık Okunan Veriler
Ürün listesi, sabit ayarlar, menüler
cache.Set("products", list)
Kullanıcı Oturumları
Kullanıcıya özel geçici bilgiler
cache.Set("session:123", data)
API Sonuçları
Dış servisten gelen verilerin cache'lenmesi
cache.Set("weather:istanbul", result)
Yetki/Rol Bilgileri
Kullanıcı rolleri, izinler
cache.Set("roles:admin", roles)
Sayfa Parçaları (Partial)
Sık değişmeyen HTML blokları
cache.Set("footer", html)
Geçici Hesaplamalar
Sık yapılan, maliyetli hesaplamalar
cache.Set("calc:sum", value)
🧩 1. In-Memory Caching Temelleri
.NET uygulamalarında in-memory cache için en çok kullanılan arayüz IMemoryCache'tir. Bu yapı, RAM üzerinde key-value (anahtar-değer) mantığıyla veri saklar. Uygulama yeniden başlatıldığında tüm cache silinir.
🔧 Temel Özellikler
RAM tabanlıdır: Veriler fiziksel bellekte tutulur, uygulama kapanınca kaybolur.
Key-Value: Her veri bir anahtar ile tutulur.
Çok hızlıdır: DB sorgusuna göre çok daha hızlıdır.
Zaman aşımı desteği: Her veri için ayrı TTL (Time To Live) ayarlanabilir.
Thread-safe: Aynı anda birden fazla istek güvenle cache'e erişebilir.
🧪 2. IMemoryCache Temel Kullanım Senaryoları
2.1. Veri Ekleme (Set)
bash
//Basit bir cache kayıt işlemi_cache.Set("anahtar", veri, TimeSpan.FromMinutes(10));//Basit bir cache kayıt işlemi
2.2. Veri Okuma (Get / TryGetValue)
bash
varvalue= _cache.Get<string>("anahtar");// veyaif(_cache.TryGetValue("anahtar",outstringvalue)){// value kullan}
2.3. Veri Silme (Remove)
bash
//Basit bir cache kayıt silme işlemi_cache.Remove("anahtar");//Daha önce atanan anahtar key değerine karşı gelen value siliniyor.
🛠️ 3. Temel Kurulumlar ve Program.cs Yapılandırması
3.1. IMemoryCache Servisinin Eklenmesi
Öncelikle, Program.cs veya Startup.cs dosyanıza aşağıdaki satırı ekleyin:
bash
// Program.csbuilder.Services.AddMemoryCache();//Program.cs Bu satır, DI (Dependency Injection) ile IMemoryCache'i uygulamanıza ekler.
3.2. Controller veya Service'de Kullanım
bash
publicclassProductService{privatereadonlyIMemoryCache _cache;publicProductService(IMemoryCache cache){ _cache = cache;}publicList<Product>GetProducts(){// 1. Önce cache'de var mı kontrol etif(_cache.TryGetValue("products",outList<Product> cachedProducts)){return cachedProducts;// Cache'den döndür}// 2. Yoksa DB'den çekvar products =GetProductsFromDb();// 3. Cache'e ekle (örnek: 5 dakika sakla) _cache.Set("products", products, TimeSpan.FromMinutes(5));return products;}}
3.3. Basit Kullanım
bash
// Ekle_cache.Set("key","değer", TimeSpan.FromMinutes(1));// Okuvar val = _cache.Get<string>("key");// Sil_cache.Remove("key");
3.4. Complex Tipler ile Kullanım
bash
publicclassUser{publicint Id {get;set;}publicstring Name {get;set;}}var user =newUser{ Id =1, Name ="Ali"};_cache.Set("user:1", user, TimeSpan.FromMinutes(5));var cachedUser = _cache.Get<User>("user:1");
3.5. Sliding Expiration ile Kullanım
bash
var options =newMemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(30));_cache.Set("temp","veri", options);
IMemoryCache ile cache'e eklerken birçok gelişmiş ayar yapabilirsin:
bash
var cacheEntryOptions =newMemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(10))// 10 dk sonra kesin sil.SetSlidingExpiration(TimeSpan.FromMinutes(2))// 2 dk erişilmezse sil.SetPriority(CacheItemPriority.High)// Temizlemede öncelik.RegisterPostEvictionCallback((key,value, reason, state)=>{// Silindiğinde tetiklenir Console.WriteLine($"Cache'den silindi: {key}, Sebep: {reason}");});_cache.Set("user:1", userObj, cacheEntryOptions);
4.1 Tüm Konfigürasyon Ayarları (Detaylı Açıklamalı)
In-Memory cache kullanırken belleği nasıl yöneteceğin, öğeleri nasıl sınırlayacağın ve ne zaman silineceklerini nasıl belirleyeceğin, uygulamanın performansı açısından oldukça önemlidir. Aşağıda AddMemoryCache metoduyla yapılandırabileceğin tüm önemli ayarları detaylı şekilde ele alıyoruz.
🕰️ SetAbsoluteExpiration – Mutlak Süreli Önbellekleme
SetAbsoluteExpiration, bir cache öğesinin ne olursa olsun belirli bir süre sonra kesinlikle silinmesini sağlar.
🧠 Ne zaman kullanılır?
Cache verisinin belirli aralıklarla yenilenmesi gerektiği durumlarda.
Örneğin: Her 10 dakikada bir veritabanından veri çekmek istiyorsan.
bash
var options =newMemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(10));// 10 dakika sonra kesin silinir_cache.Set("productList", productList, options);
Yani sen cache'e bir productList koyarsın ve ona dersin ki: "10 dakika içinde bu endpoint yada bu fonksiyona kimse dokunmasa bile bu cache silinsin."
📌 Dikkat!
Kullanıcı erişim yapsa bile bu süre uzamaz.
Bu sürenin sonunda öğe otomatik olarak evict (temizlenir) edilir.
🔁 SetSlidingExpiration – Kaymalı Süreli Önbellekleme
SetSlidingExpiration, cache'teki veriye erişildikçe sürenin sıfırlanmasını sağlar.Yani kullanıcı o veriyi kullandıkça cache'te kalmaya devam eder.
🧠 Ne zaman kullanılır?
Veri sık erişiliyorsa, ve bu yüzden cache'te kalması gerekiyorsa.
Örneğin: Popüler ürün listeleri, kullanıcı sepeti, oturum bilgileri.
bash
var options =newMemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(5));// 5 dakika boyunca erişilmezse sil_cache.Set("user-cart", userCart, options);
Burada da şunu dersin: "Kullanıcı sepete her baktığında süre 5 dakikadan tekrar başlasın. Ama 5 dakika boyunca hiç kimse bu sepete bakmazsa, sil."
📌 Dikkat!
Her erişimde süre sıfırlanır (resetlenir).
Eğer belirlenen süre boyunca hiç erişim olmazsa, verinin süresi dolar ve silinir.
SetAbsoluteExpiration ve SetSlidingExpiration Birlikte Kullanılabilir mi?
Evet! Genelde şöyle kullanılır:
bash
var options =newMemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(30))// 30 dakika içinde kesin sil.SetSlidingExpiration(TimeSpan.FromMinutes(5));// 5 dakikada erişilmezse de sil_cache.Set("popular-products", productList, options);
Bu durumda: 30 dakikadan uzun yaşayamaz (mutlak sınır), Ama 5 dakika kimse bakmazsa erken silinir (kaymalı süre).
Özellik
SetAbsoluteExpiration
SetSlidingExpiration
Ne zaman silinir?
Belirtilen süreden sonra kesin
Belirtilen süre boyunca erişim olmazsa
Erişim süresi etkiler mi?
❌ Hayır
✅ Evet
Sık kullanılan veriye uygun mu?
❌ Hayır
✅ Evet
✅ Popüler Ürün Listesi için bir senaryo geliştirelim.
Bir e-ticaret sitesinde, en çok görüntülenen ürünleri belirli aralıklarla veritabanından alıp cache'e koymak istiyoruz. Ama şu kurallar geçerli:
Kullanıcılar bu listeye sık sık erişiyor (ana sayfada gösteriliyor).
Ama maksimum 30 dakikada bir güncellenmesi gerekiyor.
Eğer 5 dakika boyunca hiç erişim yoksa, cache'ten silinsin.
Hem SetAbsoluteExpiration hem de SetSlidingExpiration birlikte kullanalım.
bash
publicclassProductService{privatereadonlyIMemoryCache _cache;publicProductService(IMemoryCache cache){ _cache = cache;}publicList<Product>GetPopularProducts(){conststring cacheKey ="popular-products";if(_cache.TryGetValue(cacheKey,outList<Product> products)){ Console.WriteLine("✅ Cache'ten geldi.");return products;}// Simüle: Veritabanından veri çekme (gerçekte burada DB çağrısı olur) products =GetFromDatabase();// Cache'e ekliyoruzvar cacheOptions =newMemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(30))// 30 dakika sonra kesin sil.SetSlidingExpiration(TimeSpan.FromMinutes(5));// 5 dakika erişilmezse sil _cache.Set(cacheKey, products, cacheOptions); Console.WriteLine("💾 Veritabanından geldi ve cache'lendi.");return products;}privateList<Product>GetFromDatabase(){returnnewList<Product>{newProduct{ Id =1, Name ="Laptop"},newProduct{ Id =2, Name ="Kulaklık"},newProduct{ Id =3, Name ="Klavye"}};}}
🧪 Test Senaryosu
bash
var productService =newProductService(memoryCache);// İlk çağrı → DB'den gelirvar products1 = productService.GetPopularProducts();// 2 dakika sonra tekrar çağrı → Cache'ten gelirawait Task.Delay(TimeSpan.FromMinutes(2));var products2 = productService.GetPopularProducts();// 6 dakika bekleyelim → Sliding süresi dolsun, sonra tekrar çağır → Cache silinmiş olurawait Task.Delay(TimeSpan.FromMinutes(6));var products3 = productService.GetPopularProducts();
✅ Çıktı
text
💾 Veritabanından geldi ve cache'lendi.//Console çıktısı ekrana yazıldı.✅ Cache'ten geldi.//Console çıktısı ekrana yazıldı💾 Veritabanından geldi ve cache'lendi.//Console çıktısı ekrana yazıldı
🧱 SizeLimit ve SetSize – Poşet Benzetmesiyle
Bu konu biraz mantıksal olarak karışık geliyor bana çevremde bir çok geliştiriciye sorduğumda da aynı şekilde bir dönüş aldım çünkü ister istemez beynimiz boyuta kayıyor(Ram boyutu) internette araştırma yaparken şöyle bir benzetme bulmuştum bu benzetme üzerinden sizlere bu konuyu açıklamak istiyorum.
Hayal et ki marketten alışveriş yapıyorsun. Elinde bir poşet var ve bu poşet en fazla 10 kilo taşıyabiliyor bu senin elindeki poşetin taşıma kapasitesi yani Size Limit değerine karşılık geliyor.
Poşete eklemek istediğin ürünler ise senin ürünlerinin ağırlığını belirtiyorsun: bu da SetSize olarak isimlendiriliyor. Ama bu gerçek bir ağırlık değil , göreli bir birim bunu unutma.
Şimdi programatik olarak biz bu poşetin taşıma kapasitesini tanımlayalım.
SizeLimit , cache'in toplam bellekte kullanabileceği maksimum birim miktarını belirler. Bu değer, gerçek bellek boyutunu (MB, GB) değil, senin tanımladığın göreli birimleri ifade eder.
Bu ayarla, cache'in toplamda en fazla 10 birimlik veri tutabileceğini söylüyorsun.
Bu birim senin belirlediğin bir ölçüdür kilo gibi düşünebilirsin.Buradaki 10 değeri bizim belirlediğimiz bir değer bunun yerine biz 1024, 2048, 40 gibi değerlerde yazabiliriz. 1024 yazdığımızı hayal edelim , her cache öğesini 1 birim sayarsan, en fazla 1024 öğe saklanabileceği anlamına gelir. Eğer sen her ürün için SetSize(10) tanımlarsan, yaklaşık 102 öğe saklayabilirsin.
💡 SizeLimit ayarlandıktan sonra, tüm cache öğelerine .SetSize() verilmesi zorunludur**. Verilmezse InvalidOperationException hatası alırız.
🎒 SetSize nedir?
SetSize, her bir cache öğesinin bellekte ne kadar yer kaplayacağını belirtmek için kullanılır. SizeLimit ayarı yapılmışsa, bu ayarı kullanmadan cache'e veri ekleyemezsin.
Şimdi poşetin içine ürünleri koyuyorsun. Her ürünün ağırlığını belirtiyorsun: bu da SetSize.
Bu durumda sistem "dolap doldu" der ve en az öncelikli (veya zamanı geçmiş) kutuları çıkarır, yeniye yer açar. Bu süreç "compaction" (sıkıştırma, temizlik) işlemidir.
📌 Dikkat Edilecek Noktalar
Eğer SizeLimit tanımladıysan ama SetSize vermezsen, sistem hata fırlatır: InvalidOperationException: Size must be set on item when cache has a size limit.
🧠 Gerçek Hayattan Örnek
Bir e-ticaret sitesinde:
Tek bir ürün detayı: SetSize(1)
Tüm ürün listesi (sayfalı): SetSize(20)
Kullanıcı sepeti: SetSize(5)
Ana sayfa promosyon bloğu: SetSize(10)
Bu yapıyla dolabın (cache') hacmini bilinçli kullanırsın.
🧪 Mini Uygulama Senaryosu
bash
builder.Services.AddMemoryCache(options =>{ options.SizeLimit =50;// toplam kapasite});memoryCache.Set("product_1", data1,newMemoryCacheEntryOptions().SetSize(10));memoryCache.Set("product_2", data2,newMemoryCacheEntryOptions().SetSize(10));memoryCache.Set("product_3", data3,newMemoryCacheEntryOptions().SetSize(10));memoryCache.Set("product_4", data4,newMemoryCacheEntryOptions().SetSize(10));memoryCache.Set("product_5", data5,newMemoryCacheEntryOptions().SetSize(10));memoryCache.Set("product_6", data6,newMemoryCacheEntryOptions().SetSize(10));// bu eklenirken önceki bir tanesi silinecek!
Burada 6 ürün eklenmeye çalışılıyor ama 5 tanesi sığıyor.
Sonuncusu eklenirken sistem en eski ya da en önemsiz olanı çıkarıp yer açar.
🔁 CompactionPercentage – Otomatik Temizlik Oranı
Mimarisel olarak .NET'te IMemoryCache, SizeLimit ile bir sınır belirlediğimizde bellekteki cache kapasitesini dolu tutmak ister. Ancak bazen yeni veriler geldiğinde mevcut verilerin silinmesi gerekir. İşte bu temizlik (compaction) işlemi yapılırken ne kadar veri silineceğini belirleyen ayara CompactionPercentage denir.
🧠 Gerçek Hayat Benzetmesiyle
Diyelim ki yine elimizde 100 birimlik bir poşet var (SizeLimit = 100). Bu poşeti ürünlerle doldurduk. Yeni bir ürün koymak istiyoruz ama yer kalmadı.
Normalde şöyle düşünürüz "Yeni ürün geliyorsa bir şeyleri atmalıyım."
Peki ne kadarını atacağım? İşte CompactionPercentage burada devreye girer.
// Cache'e ürünler ekleniyor:_cache.Set("ürün1", data1,newMemoryCacheEntryOptions().SetSize(30).SetPriority(CacheItemPriority.Low));// En kolay silinecek_cache.Set("ürün2", data2,newMemoryCacheEntryOptions().SetSize(40).SetPriority(CacheItemPriority.Normal));// Orta öncelik_cache.Set("ürün3", data3,newMemoryCacheEntryOptions().SetSize(30).SetPriority(CacheItemPriority.High));// En zor silinecek// Toplam boyut: 30 + 40 + 30 = 100 birim// Yeni ürün ekleniyor (limit aşılacak):_cache.Set("ürün4", data4,newMemoryCacheEntryOptions().SetSize(10).SetPriority(CacheItemPriority.Normal));
🔄 Şimdi ne olur?
Sistem der ki: "Cache dolu, yeni ürün 10 birimlik yer istiyor." CompactionPercentage = 0.20 olduğu için, mevcut cache'in %20'si (100 x 0.2 = 20) kadar veri temizlenir. En düşük öncelikli olanlardan başlanarak 20 birimlik yer açılır. Sonra ürün4 bu boşluğa eklenir.
❓ Compaction Sırasında Öğelerden "Parça Parça" mı Yoksa "Tamamen" mi Silinir?
Öğeler her zaman "tamamen" silinir. Parça parça silme yoktur.
bash
// Bellek dolu olmasına rağmen eklenen kısım_cache.Set("ürün4", data4,newMemoryCacheEntryOptions().SetSize(10));//Size limit dolu nasıl bir yol izlenir ?
Yani, .NET şu satır çalıştığında ve cache kapasitesi (SizeLimit) doluysa bu şekilde davranır:
Compaction başlatılır.
Önceden belirlediğimiz CompactionPercentage (örneğin %25) kadar yer açılmalıdır. Diyelim ki SizeLimit = 100, bu durumda 25 birimlik yer açılmalı. Cache, öncelik sırasına göre öğeleri incelemeye başlar. Öğeler tek tek, tamamen silinir. Herhangi bir öğeden "bir kısmı" silinmez çünkü öğeler atomic (bölünemez) olarak saklanır.
Yani cache şu durumda:
Öğeler
SetSize
Priority
ürün1
30
Low
ürün2
40
Normal
ürün3
30
High
Toplam
100
En az 100 x 0.25 = 25 birimlik yer açılması gerekir.
.NET öncelik sırasına göre silmeye başlar.
İlk olarak ürün1 (30 birim) silinir çünkü en düşük öncelikli olan o. Silme tamamlandıktan sonra 30 birimlik yer elde etmiş oluruz çünkü 25 birim ihtiyacımız olan değerdi fakat parça parça silme olmadığından ilk eklenen ve en düşük Priority değere sahip olanın ürün 1 olduğuna karar verdi code ve bize 30 birimlik yer açılmış oldu.
Yani ürün1 tamamen silindi. ürün2 ve ürün3 etkilenmesine gerek kalmadı.
Peki bu durumda ürün1 'in tamamen silinmesi veri kaybına sebep olur mu ?
Cache için malesef sebep olur. Bu nedenle kritik verileri cache'e koyarken şu önlemleri almalısın:
🔐 Koruma Yöntemleri
Strateji
Açıklama
CacheItemPriority.High
Daha az ihtimalle silinir.
CacheItemPriority.NeverRemove
Asla silinmez (ama dikkat: çok kullanılırsa cache dolup yeni öğe eklenemez!)
AbsoluteExpiration veya SlidingExpiration
Otomatik süreli silme ile kontrol sağlanabilir.
CompactionPercentage düşürülerek daha az veri silinir
Örn: 0.10 gibi.
⏱️ ExpirationScanFrequency – Süresi Dolmuşları Ne Sıklıkla Temizlesin?
IMemoryCache içinde bazı öğelere AbsoluteExpiration ya da SlidingExpiration tanımlayabiliriz. Bu öğeler belirli bir süre sonunda "süresi dolmuş" (expired) olurlar.
IMemoryCache içinde, bazı cache öğelerine süreli geçerlilik tanımlayabilirsin. Örneğin, bir değeri cache'e koyarken şunu diyebilirsin;
"Bu veri 5 dakika geçerli olsun." veya Bu veriye son erişimden 1 dakika sonra artık ihtiyaç kalmayacak."
Bunlar için AbsoluteExpiration ya da SlidingExpiration kullanırsın. Ancak şunu bilmelisin: Sen cache yapmak istediğin key değerineAbsoluteExpiration ya da SlidingExpiration versen bile, bu veri hemen silinmez.
.NET, belirli aralıklarla arkaplanda bir tarama (scan) yaparak süresi dolmuş öğeleri temizler. İşte bu tarama sıklığını belirleyen ayar ExpirationScanFrequency'dir.
🧠 Gerçek Hayat Benzetmesiyle
Bir kafede çalışan bir garson düşün. Her 15 dakikada bir masaları dolaşıp "boş bardak var mı?" diye kontrol ediyor.
Ama her saniye kontrol etse, hem yorulur hem sistemi yavaşlatır. Ama bu kontrol her saniye yapılmaz çünkü bu sistemin performansına zarar verebilir. Ne kadar sık kontrol edileceğini ExpirationScanFrequency ile biz ayarlıyoruz.
bash
builder.Services.AddMemoryCache(options =>{//.NET, her 2 dakikada bir expired olan cache öğelerini tarayıp siler. options.ExpirationScanFrequency = TimeSpan.FromMinutes(2);});
Nasıl Çalışır?
Sen cache yapmak istediğin key değerine, örneğin "UrunListesi" key'ine bir AbsoluteExpiration ya da SlidingExpiration verirsin.
Belirlediğin süre dolunca, bu veri artık geçersiz (expired) sayılır.
Ancak bu veri hemen silinmez. Hâlâ memory içindedir.
Sistem, ExpirationScanFrequency süresi geldiğinde, belleği tarar ve süresi dolmuşları temizler.
bash
builder.Services.AddMemoryCache(options =>{ options.ExpirationScanFrequency = TimeSpan.FromSeconds(10);// Her 10 saniyede bir tarama});
Bu örnekte, süresi dolmuş veriler maksimum 10 saniye içinde bellekten silinir.
📌 Unutma
Veri hemen silinmez Sen süre tanımlasan bile öğe, ancak bir sonraki taramada silinir.
Çok sık tarama = yüksek yük Sürekli temizlik yapması sistem performansını etkileyebilir.
Çok seyrek tarama = fazla veri Expired veriler bellek içinde daha uzun kalabilir.
Expiration verilen key'lerde çalışırAbsoluteExpiration ve SlidingExpiration gibi süreli cache'lerde anlamlıdır.
Basit Test Senaryosu
bash
_cache.Set("ürün1","değer",newMemoryCacheEntryOptions{ AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5)});// Bu örnekte:// - 5 saniye sonra "ürün1" geçersiz olur (expired)// - Ancak bellekteki silinmesi 10 saniyelik tarama süresine göre gerçekleşir
🔁 Remove(key) – Cache'ten Manuel Silme İşlemi
Cache'de tutulan bir veriyi, belirli bir anahtar (key) üzerinden manuel olarak silmek için Remove metodu kullanılır. Bu, özellikle güncellenen veya geçersizleşen verilerin cache'den temizlenmesi gerektiğinde önemlidir.
🧠 Ne zaman kullanılır?
Veri güncellendiğinde eski cache'in silinmesi gerekiyorsa
//Daha önce user:1 adında bir key atandığını düşünün._cache.Remove("user:1");// "user:1" anahtarındaki cache silinir
📌 Dikkat!
Silinen anahtar bir daha çağrıldığında cache miss olur ve veri kaynağından tekrar çekilir.
PostEvictionCallback tanımlıysa, silme sonrası tetiklenir.
🧪 TryGetValue(key, out T) – Performanslı Veri Okuma Tekniği
Cache'den veri okurken, hem varlık kontrolü hem de okuma işlemini tek adımda yapmak için TryGetValue metodu kullanılır. Bu yöntem, gereksiz iki ayrı çağrıdan (Contains + Get) kaçınır ve performanslıdır.
Tip güvenliğinden emin olmalısın, yanlış tipte okuma yaparsan null döner.
Sık kullanılan bir pattern: "Cache varsa getir, yoksa kaynaktan çek ve cache'e ekle."
🧼 Compact() Metodu – Elle Compaction (Temizlik) Tetiklemek
IMemoryCache, otomatik olarak compaction (temizlik) işlemi yapsa da, bazı durumlarda elle temizlik başlatmak isteyebilirsin. Compact() metodu, belirli bir oranda cache'i temizler.
⚙️ Kullanımı
bash
//IMemoryCache'i somut sınıf olan MemoryCache'e cast etmen gerekir.var memoryCache = _cache asMemoryCache;memoryCache?.Compact(0.5);// Cache'in %50'sini temizler
📌 Dikkat!
Sadece MemoryCache tipine cast edilerek kullanılabilir.
Parametre 0 ile 1 arasında bir orandır (örn: 0.2 = %20'sini temizle).
Genellikle bakım, acil temizlik veya test amaçlı kullanılır.
Cache anahtarlarını gruplamak ve yönetimi kolaylaştırmak için isimlendirme stratejileri kullanılır. Region veya namespace benzeri bir yapı, anahtarların başına kategori ekleyerek sağlanır.
🧠 Neden önemli?
Benzer verileri topluca silmek veya yönetmek kolaylaşır.
Tüm "user:" ile başlayan anahtarları silmek için anahtarları listede tutmak veya özel bir yönetim mekanizması gerekir.
📌 Dikkat!
IMemoryCache doğrudan region desteği sunmaz, anahtar isimlendirmesiyle mantıksal gruplama yapılır.
🧵 Multithreading ve Locks – Çoklu Erişimde Önbellek Güvenliği
IMemoryCache, thread-safe olarak tasarlanmıştır. Yani birden fazla thread aynı anda cache'e erişebilir. Ancak, cache miss durumunda veri kaynağından veri çekme işlemi için ekstra kilitleme (lock) mekanizması kullanmak gerekebilir.
🧠 Neden lock gerekir?
Aynı anda birden fazla thread aynı cache miss'i yaşarsa, hepsi veri kaynağına gider. Bunu önlemek için lock ile sadece bir thread'in veri çekmesini sağlarsın.
Çok sık kullanılan anahtarlar için lock contention oluşabilir, dikkatli kullanılmalı.
.NET 8 ile gelen GetOrCreateAsync gibi yöntemler de tercih edilebilir.
🔄 Refresh / LazyLoad Pattern – Cache Miss Durumunda Veri Getirme
Cache'de veri yoksa (cache miss), otomatik olarak veri kaynağından çekip cache'e ekleme işlemi "Lazy Load" veya "Refresh" pattern olarak bilinir.
⚙️ Kullanımı
bash
if(!_cache.TryGetValue("user:1",outUser user)){ user =GetUserFromDb(1); _cache.Set("user:1", user, TimeSpan.FromMinutes(10));}// user artık cache'te
.NET 8 ile GetOrCreateAsync
bash
var user =await _cache.GetOrCreateAsync("user:1", entry =>{ entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);returnGetUserFromDbAsync(1);});
📌 Dikkat!
Bu pattern, veri kaynağına gereksiz yük binmesini önler.
Thread-safe olması için lock veya GetOrCreateAsync gibi yöntemler tercih edilmeli.
🧰 IMemoryCache'i Service Olarak Yeniden Sarmak (Wrapper)
IMemoryCache'i doğrudan kullanmak yerine, kendi cache servis katmanını yazarak tekrar kullanılabilir, test edilebilir ve merkezi bir yapı oluşturabilirsin.
🧠 Avantajları
Tek noktadan yönetim ve loglama
Mock/test kolaylığı
Anahtar isimlendirme, default ayarlar, istatistik toplama gibi ek özellikler
var products = _appCache.GetOrAdd("products", GetProductsFromDb, TimeSpan.FromMinutes(10));_appCache.Remove("products");
📌 Dikkat!
Wrapper ile merkezi loglama, istatistik, anahtar yönetimi gibi ek fonksiyonlar kolayca eklenebilir.
📊 TrackStatistics – Cache'in Skor Tabelası
TrackStatistics, .NET 8 (ve sonraki sürümler) ile gelen bir MemoryCacheOptions özelliğidir.
Bu özelliği true yaptığında, IMemoryCache hit / miss / entry sayısı gibi ölçümleri aktif olarak tutmaya başlar. Varsayılan değeri false—yani istatistikler kapalıdır.
🧠 Gerçek Hayat Benzetmesi
Düşün ki büyük bir marketteki yazar kasa görevlisisin.
Her kasaya gelen müşteri = cache erişimi
Ürünü bulursan = hit
Bulamazsan yan raflara gidip arıyorsun = miss TrackStatistics , kasanın üstündeki "toplam müşteri, kaçında ürün hazırdı, kaçında raf arandı" sayaçlarını açar. Böylece hangi kasanın (cache') ne kadar verimli olduğunu ölçebilirsin.
🔗 TrackLinkedCacheEntries – Cache Öğeleri Arası Bağlantıları Takip Etme
TrackLinkedCacheEntries özelliği, .NET'in MemoryCacheEntryOptions içinde bulunan ve cache öğelerinin birbiriyle bağlantılı olduğunu belirtmek için kullanılan gelişmiş bir özelliktir.
Bu özellik sayesinde, bir cache öğesi silindiğinde veya süresi dolduğunda, ona bağlı diğer cache öğelerinin de otomatik olarak geçersiz kılınması (invalidate) sağlanabilir.
🧠 Neden Önemli?
Diyelim ki bir web sayfasında: Ana içerik cache'de tutuluyor (key = "AnaIcerik"),
Bu ana içerikle ilgili farklı parçalar (örneğin key = "Icerik_Parca1", key = "Icerik_Parca2") ayrı ayrı cache'leniyor.
Eğer ana içerik güncellenirse, ona bağlı parçaların da cache'den temizlenmesi gerekir. Aksi halde eski parçalar kullanılır ve tutarsızlık oluşur. İşte burada TrackLinkedCacheEntries devreye girer: Bağlı öğeler birbirine bağlanır, biri değişirse diğerleri de temizlenir.
Örnek: Ürün ve Kategori Cache'leri
bash
// Kategori cache'e ekleniyor_cache.Set("Kategori_5", kategori5,newMemoryCacheEntryOptions{ AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30), Size =1});// Kategoriye bağlı ürün cache'leri_cache.Set("Urun_101", urun101,newMemoryCacheEntryOptions{ AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15), Size =1, TrackLinkedCacheEntries =true, LinkedCacheEntries =new[]{"Kategori_5"}});_cache.Set("Urun_102", urun102,newMemoryCacheEntryOptions{ AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15), Size =1, TrackLinkedCacheEntries =true, LinkedCacheEntries =new[]{"Kategori_5"}});// Eğer kategori silinirse, ona bağlı ürünler de silinir.
Burada TrackLinkedCacheEntries = true ve LinkedCacheEntries dizisi ile "Urun_101" ve "Urun_102" cache öğeleri "Kategori_5" öğesine bağlı hale geliyor. Bu sayede kategori silinirse ürünlerde siliniyor.
🔄 PostEvictionCallback – Cache Öğesi Silindikten Sonra Çalışan Geri Çağırma
PostEvictionCallback, bir cache öğesi cache'den çıkarıldıktan sonra tetiklenen bir geri çağırma (callback) fonksiyonudur. Bu sayede cache'den silinen verilerle ilgili özel işlemler (loglama, temizlik, yeniden yükleme vb.) yapılabilir.
🧠 Neden Önemli?
Cache'deki bir öğe:
Süresi dolduğu için,
Bellek baskısı nedeniyle,
Manuel silindiği için çıkarıldığında uygulama haberdar olmazsa, bazı durumlarda tutarsızlık veya izleme problemi yaşanabilir.PostEvictionCallback ile silinme anı yakalanır, gereken aksiyon alınabilir.
⚙️ Nasıl Kullanılır?
bash
var options =newMemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(5)).RegisterPostEvictionCallback((key,value, reason, state)=>{ Console.WriteLine($"Cache'den silindi: {key}, Sebep: {reason}");// Örneğin: Loglama, yeni cache doldurma veya temizleme işlemi yapılabilir});_cache.Set("user:1", userObj, options);
🔍 PostEvictionCallback Parametreleri
Parametre
Açıklama
key
Cache'den silinen öğenin anahtarı
value
Silinen cache öğesinin değeri
reason
Silinme nedeni (EvictionReason enum'u)
state
Opsiyonel kullanıcı durumu (state) bilgisi
EvictionReason enum değerleri:
Removed – Manuel silme
Expired – Zaman aşımı
TokenExpired – Bağımlılık nedeni ile
Capacity – Bellek doluluğu nedeniyle temizleme
Replaced – Üzerine yazma işlemi
Product listesi için cache silindiğinde PostEvictionCallback ile cache'in yeniden doldurulması senaryosu:
bash
publicclassProductService{privatereadonlyIMemoryCache _cache;privatereadonlyAppDbContext _db;publicProductService(IMemoryCache cache,AppDbContext db){ _cache = cache; _db = db;}privateconststring CacheKey ="products:list";publicasyncTask<List<Product>>GetProductListAsync(){if(_cache.TryGetValue(CacheKey,outList<Product> cachedList))return cachedList;var products =await _db.Products.ToListAsync();var cacheEntryOptions =newMemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(10)).RegisterPostEvictionCallback(async(key,value, reason, state)=>{if(key isstring k && k == CacheKey){ Console.WriteLine($"Cache '{CacheKey}' silindi, yeniden oluşturuluyor...");// Db'den yeniden çekip cache'e tekrar eklevar freshProducts =await _db.Products.ToListAsync();// Cache'e tekrar ekle (sync callback içinde async kullanmak için dikkatli ol) _cache.Set(CacheKey, freshProducts, TimeSpan.FromMinutes(10));}}); _cache.Set(CacheKey, products, cacheEntryOptions);return products;}}/*
PostEvictionCallback içindeki callback metodu senkron olduğu için,
async işlemler async void gibi yapılmamalı; burada örnek kolaylık için böyle gösterildi.
Gerçek projede, daha güvenli ve temiz yapı için başka mekanizmalar
(background servis, event queue vb.) tercih edilir.
*/
🔔 IChangeToken – Cache İçeriği İçin Değişiklik Bildirimi
IChangeToken, .NET içinde bir kaynağın (dosya, veri, ayar, vs.) değişip değişmediğini izlemek için kullanılan bir arayüzdür. Özellikle cache yapılarında, dışarıdaki bir kaynaktaki değişiklikleri takip edip, cache'i otomatik olarak yenilemek için kullanılır.
🧠 Neden Önemli?
Cache'te tuttuğun veri, dış kaynaklardan (örneğin dosya, config, başka bir servis) sağlanıyorsa, bu kaynak değiştiğinde cache'in de güncellenmesi gerekir.
IChangeToken sayesinde;
Kaynak değiştiğinde cache otomatik temizlenir,
Böylece eski ve güncel olmayan veriler kullanılmaz,
Uygulama kendini otomatik günceller.
⚙️ Nasıl Çalışır?
IChangeToken bir değişiklik olup olmadığını (HasChanged) ve izleme durumunu (ActiveChangeCallbacks) bildirir. Ayrıca, RegisterChangeCallback metodu ile değişiklik olduğunda çalışacak bir callback kaydedilir.
🛠️ IChangeToken ile Cache Bağlama Örneği
Diyelim ki bir yapılandırma dosyası appsettings.json değiştiğinde cache temizlensin.
bash
usingMicrosoft.Extensions.Primitives;// Örnek: Dosya değişikliklerini takip eden IChangeTokenvar fileProvider =newPhysicalFileProvider(Directory.GetCurrentDirectory());var changeToken = fileProvider.Watch("appsettings.json");var cacheEntryOptions =newMemoryCacheEntryOptions().AddExpirationToken(changeToken)// Değişiklik olduğunda cache temizlenir.SetAbsoluteExpiration(TimeSpan.FromMinutes(30));_cache.Set("configData", configData, cacheEntryOptions);
📌 Önemli Metod ve Özellikler
Üye
Açıklama
bool HasChanged
Kaynakta değişiklik olup olmadığını belirtir
bool ActiveChangeCallbacks
Değişiklik geri çağırmalarının aktif olup olmadığını belirtir
IDisposable RegisterChangeCallback(Action
Değişiklik olunca çağrılacak fonksiyonu kaydeder
🧩 Kullanım Senaryoları
Dosya değişikliklerinin izlenmesi (PhysicalFileProvider gibi)
Uygulama ayarlarının dinamik olarak güncellenmesi
Harici kaynaklardan gelen verilerin cache ile senkronizasyonu
Minimal Api ve CQRS pattern ile beraber kullanım senaryosu.
bash
publicclassGetProductsHandler:IQueryHandler<GetProductsQuery, GetProductsResult>{privatereadonlyCatalogDbContext _dbContext;privatereadonlyIMemoryCache _cache;publicGetProductsHandler(CatalogDbContext dbContext,IMemoryCache cache){ _dbContext = dbContext; _cache = cache;}publicasyncTask<GetProductsResult>Handle(GetProductsQuery request,CancellationToken cancellationToken){var pageIndex = request.PaginationRequest.PageIndex;var pageSize = request.PaginationRequest.PageSize;string cacheKey =$"products_page_{pageIndex}_size_{pageSize}";//Daha önce memory'de cache edilmiş veri varsa o dönsün.if(_cache.TryGetValue(cacheKey,outGetProductsResult cachedResult)){return cachedResult;}var totalCount =await _dbContext.Products.LongCountAsync(cancellationToken);var products =await _dbContext.Products
.AsNoTracking().Skip(pageIndex * pageSize).Take(pageSize).ToListAsync(cancellationToken);var productDtos = products.Adapt<List<ProductDto>>();var result =newGetProductsResult(newPaginatedResult<ProductDto>( pageIndex, pageSize, totalCount, productDtos
));//Yoksa dönen veriyi cachele. _cache.Set(cacheKey, result, TimeSpan.FromMinutes(1));// 1 dakika cache'lereturn result;}}
🚀 Sonuç ve İleriye Dönük Adımlar
In-Memory Caching, .NET ekosisteminde performans optimizasyonu ve kaynak verimliliği için vazgeçilmez bir araçtır. Doğru kullanıldığında hem uygulama hızını artırır hem de altyapı maliyetlerini düşürür. Bu rehberde temel kavramlardan gelişmiş senaryolara, best practice'lerden gerçekçi kod örneklerine kadar kapsamlı bir yolculuk yaptık. Artık IMemoryCache ve in-memory cache mimarisiyle ilgili hem teorik hem de pratik bilgiye sahipsiniz.
Unutmayın, her uygulamanın ihtiyaçları farklıdır; bu yüzden öğrendiklerinizi kendi projelerinizde deneyerek, farklı konfigürasyonları test ederek en uygun cache stratejisini belirleyin. Daha ileri seviye için distributed cache (Redis, NCache vb.), cache invalidation stratejileri ve monitoring araçlarını da keşfetmenizi öneririm.
Başarılar ve yüksek performanslı kodlar dilerim! 🎉