NET Core'da Cache Yönetimi (Cache Eviction) - 4
Cache eviction, önbellek sistemlerinin sürdürülebilirliği için kritik bir mekanizmadır. Bu yazıda, eviction politikalarının ne olduğunu, nasıl çalıştıklarını ve hangi senaryolarda hangi politikayı tercih etmeniz gerektiğini derinlemesine inceleyeceğiz.
Cache Eviction Nedir?
Cache eviction, önbellek belleği belirlenen maksimum kapasiteye ulaştığında, yeni verilere yer açmak için mevcut verilerden bazılarının sistem tarafından otomatik olarak silinmesidir. Bu işlem, cache'in boyutunu kontrol altında tutarak performansı ve kaynak kullanımını optimize eder. Eğer cache dolduğunda bir eviction politikası devreye girmezse, sistem ya yeni veri eklenmesine izin vermez (noeviction) ya da bellek taşmasına neden olabilir. Bu da uygulamanın çökmesine veya performansın ciddi şekilde düşmesine yol açar.
Doğru eviction politikası sayesinde:
- En değerli veriler cache'te kalır (yüksek hit oranı).
- Bellek verimli kullanılır.
- Sistem kararlılığı korunur.
Cache Eviction Politikaları
TTL — Time To Live (Absolute Expiration) : Entry'nin yaratıldığı andan itibaren sabit bir ömrü vardır. Süre dolduğunda erişilip erişilmediğinden bağımsız olarak evict edilir.
Ne zaman kullanılır: Verinin belirli bir süre sonra stale sayılması gerektiği durumlarda. Ör: döviz kurları, haber feed'i, ürün fiyatları.
Riski: Çok sık erişilen veriler bile süre dolunca silinir. Dolayısıyla bir veri istendiği zaman isteklerin DB'ye gitmesi sonucu gereksiz DB yükü yaratabilir.
LRU — Least Recently Used : Cache, her bir öğeye en son ne zaman erişildiğini kaydeder. Yer açılması gerektiğinde, en uzun süredir erişilmemiş öğe silinir.
Ne zaman kullanılır : Yakın zamanda kullanılan verilerin yakın gelecekte tekrar kullanılma olasılığı daha yüksektir. Bu tarz senaryolarda tercih edilmelidir.
Örneğin kullanıcılar genellikle güncel, indirimli veya yeni eklenen ürünlere bakar. LRU, en son hangi ürün listelerine bakıldığını takip eder. Black Friday gibi yoğun dönemlerde, bir anda binlerce farklı ürün sayfası ziyaret edilebilir.Bellek dolduğunda, en uzun süredir ziyaret edilmeyen (örneğin, geçen haftanın kampanyaları) cache'ten çıkarılır. Böylece o an popüler olan ürünler cache'te kalır.
LFU (Least Frequently Used) : Her öğeye kaç kez erişildiği sayılır. Yer açılması gerektiğinde, en az erişilen öğe silinir.
.NET'teki karşılığı: Native olarak desteklenmez. CacheItemPriority ile yaklaşık olarak simüle edilebilir ya da özel implementasyon gerekir.
LRU'dan farkı: LRU "ne zaman" erişildiğine, LFU "kaç kez" erişildiğine bakar. Yeni eklenen ama henüz erişilmemiş bir entry LFU'da hemen güme gidebilir :)
Ne zaman kullanılır: Örnek bir senaryo üzerinden anlatacak olursam (Spotify benzeri) kullanıcıların en çok dinlediği şarkılar, çalma listeleri ve sanatçı bilgileri cachelendiğini düşünelim. Bazı şarkılar yıllarca popüler kalır (örneğin, klasikler), bazıları ise kısa süreli trend olur. LFU, tüm zamanların en çok dinlenen şarkılarını cache'te tutar. Ancak yeni bir şarkı viral olduğunda, hemen yüksek erişim sayısına ulaşamayabilir; bu nedenle başlangıçta cache'te yer bulması zorlaşabilir. Bu durumu iyileştirmek için, LFU'nun "yaşlanma" (aging) mekanizmalarıyla birlikte kullanılması gerekir. Redis'te LFU, her erişimde sayaç artar ancak zamanla sayaç azalır, böylece eski popülerlik güncellenir.
Redis LFU Konfigürasyonu
Makalede LFU için aging mekanizmasından söz ettik. Redis'te LFU'yu aktif etmek ve ince ayar yapmak için redis.conf üzerinde şu iki parametre kullanılır:
# redis.conf # Eviction politikasını LFU olarak ayarla # allkeys-lfu → tüm key'lere LFU uygula # volatile-lfu → sadece TTL atanmış key'lere uygula maxmemory-policy allkeys-lfu # Erişim sayacının ne hızda azalacağını belirler (dakika cinsinden yarı ömür) # Varsayılan: 1 → 1 dakikada bir sayaç yarıya düşer # Yüksek değer: sayaç yavaş azalır, geçmiş popülerlik daha uzun hatırlanır # Düşük değer: sayaç hızlı azalır, sistemi güncel trende daha duyarlı yapar lfu-decay-time 1 # Erişim sayacının ne kadar hızlı artacağını belirler (logaritmik) # Varsayılan: 10 → ~1 milyon erişimde sayaç maksimuma ulaşır # Düşük değer (örn: 1) → sayaç çok hızlı doyar, hassasiyet azalır # Yüksek değer (örn: 100) → sayaç yavaş artar, frekans farkları daha net ayrışır lfu-log-factor 10
.NET tarafında Redis bağlantısını kurarken bu ayarları doğrulamak için:
// Uygulama başlangıcında Redis eviction policy kontrolü
public class RedisHealthCheck : IHealthCheck
{
private readonly IConnectionMultiplexer _redis;
public RedisHealthCheck(IConnectionMultiplexer redis) => _redis = redis;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct)
{
var server = _redis.GetServer(_redis.GetEndPoints().First());
var config = await server.ConfigGetAsync("maxmemory-policy");
var policy = config.FirstOrDefault().Value;
return policy == "allkeys-lfu"
? HealthCheckResult.Healthy($"Eviction policy: {policy}")
: HealthCheckResult.Degraded($"Beklenmeyen policy: {policy}");
}
}
FIFO (First In, First Out) : Cache'e ilk giren entry ilk çıkar. Erişim sıklığı ya da zamanına bakılmaz, sıra önemlidir.
.NET'teki karşılığı: Native implementasyonu yoktur. Queue<T> ile manuel olarak inşa edilebilir.
Ne zaman kullanılır: Örnek bir senaryo üzerinden anlatacak olursam endüstriyel bir tesisteki sensörlerden saniyede binlerce veri geldiğini farz edelim.. Bu veriler anlık izleme için cache'lenir, ancak çok kısa süreli tutulur (örneğin, son 10 saniye). Verilerin her biri yaklaşık aynı öneme sahiptir ve hemen hemen aynı süre sonra ihtiyaç kalmaz. FIFO, en eski veriyi silerek yeni veriye yer açar. Bu senaryoda verilerin kullanım sıklığı veya son erişim zamanı önemli değildir, çünkü tüm veriler geçicidir.
Sliding Expiration (Activity-Based TTL) : Her erişimde entry'nin ömrü sıfırlanır. Erişilmeye devam ettiği sürece cache'de kalır, belirlenen süre boyunca hiç erişilmezse evict edilir.
Ne zaman kullanılır: Aktif kullanıcı session'ları, kullanıcı bazlı tercihler, aktivite bazlı oturum yönetimi.
Priority-Based Eviction : Cache’teki her veriye bir öncelik atarsınız. Bellek sıkıştığında (dolduğunda), sistem önce düşük öncelikli olanları siler, yüksek öncelikli olanları ise mümkün olduğunca korur.
.NET’te şu öncelik seviyeleri var:
- Low (Düşük): En kolay silinecek olanlar. Mesela bir ara hesaplama yapıp attığınız geçici sonuçlar.
- Normal (Orta): Varsayılan seviye. Çoğu veri için uygun.
- High (Yüksek): Silinmesi istenmeyen ama mecbur kalınırsa silinebilecek veriler.
- NeverRemove (Asla Silme): Sistem bellek sıkışıklığında bile bu veriyi kesinlikle silmez. Ancak bellek gerçekten biterse uygulama çökebilir.
Ne zaman kullanılır:
- Low: Geçici, pahalı hesaplanmamış, tekrar hesaplanması kolay veriler.
- High: Kullanıcı oturumları, sık kullanılan ama yine de yenilenebilecek veriler.
- NeverRemove: Uygulama başlangıcında okunan, değişmeyen ayarlar gibi olmazsa olmaz veriler. Dikkat! Çok fazla NeverRemove kullanırsanız, bellek dolduğunda yeni veri ekleyemezsiniz ve uygulama çökme riski artar.
Risk: NeverRemove’u “önemli her şey” için kullanırsanız, cache’de tutulması gerekmeyen veriler bile birikir, bellek taşar ve uygulama OutOfMemory hatasıyla çöker. Yani “asla silme” dediğiniz verileri çok dikkatli seçin.
Token-Based (Proactive) Eviction : Expiration süresini beklemeden, dış bir event tetiklendiğinde entry anında geçersiz kılınır. CancellationToken ya da IChangeToken ile implemente edilir.
Ne zaman kullanılır: Kaynak veri değiştiği anda cache'in invalidate edilmesi gereken durumlarda. Ör: admin panelinden ürün güncelleme, config dosyası değişimi, pub/sub event'ı.
LRU/TTL'den farkı: Zamana değil, olaya bağlı çalışır. Stale data süresi en aza indirilir.
Kısacası hiçbir strateji tek başına yeterli değildir.
.NET Core'da Cache Eviction
1. IMemoryCache — In-Process Cache / MemoryCacheEntryOptions ile Eviction Kontrolü
public class CacheService
{
private readonly IMemoryCache _cache;
private readonly ILogger<CacheService> _logger;
public CacheService(IMemoryCache cache, ILogger<CacheService> logger)
{
_cache = cache;
_logger = logger;
}
// GetOrCreateAsync önce cache'e bakar, key varsa direkt döner,
// yoksa factory fonksiyonunu çalıştırır ve sonucu cache'e yazar.
// Bu sayede her çağrıda "önce cache'e bak, yoksa üret" mantığını tek satırda kapsüllemiş olursun.
// factory parametresi bir Func<Task<T>> olduğu için bu metod her tip için generic çalışır.
// veritabanı sorgusu da olabilir, HTTP çağrısı da. entry nesnesi üzerinden yapılan
// tüm konfigürasyonlar yalnızca o key'e ait entry için geçerlidir, global cache ayarlarını etkilemez.
public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory)
{
return await _cache.GetOrCreateAsync(key, async entry =>
{
// Absolute expiration: entry kesinlikle bu süre sonunda ölür
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
// Sliding expiration: her erişimde süre sıfırlanır
entry.SlidingExpiration = TimeSpan.FromMinutes(5);
// Priority: bellek baskısında hangi entry'ler önce evict edilir
entry.SetPriority(CacheItemPriority.High);
// Size: toplam cache boyutu yönetimi için
entry.SetSize(1);
// Eviction callback
// RegisterPostEvictionCallback ile kayıt edilen bu metod, entry cache'den
// her ne sebeple çıkarılırsa çıkarılsın tetiklenir. EvictionReason enum'u size
// tam olarak neden evict edildiğini söyler: Expired süresi doldu demektir.
// Capacity bellek baskısı vardı demektir, Removed manuel olarak silindi demektir.
// TokenExpired ise bir CancellationToken iptal edildi demektir. Bu callback'i yalnızca loglama
// için değil, evict edilen entry'yi yeniden ısıtmak (cache warming) ya da bir metrik
// sistemine yazmak için de kullanabilirsin. Dikkat edilmesi gereken nokta şudur: callback,
// eviction'ı yapan thread üzerinde çalışır — dolayısıyla burada ağır iş yapmamalısın.
entry.RegisterPostEvictionCallback(OnEvicted, state: key);
return await factory();
});
}
private void OnEvicted(object key, object value, EvictionReason reason, object state)
{
_logger.LogWarning(
"Cache entry evicted. Key: {Key}, Reason: {Reason}",
key, reason);
// Reason enum değerleri:
// None, Removed, Replaced, Expired, TokenExpired, Capacity
}
}
```
### CacheItemPriority Hiyerarşisi
```
NeverRemove → High → Normal (default) → Low
↑ ↑
Asla evict edilmez İlk evict edilenler
2. Expiration Stratejileri / Absolute vs Sliding
// AbsoluteExpirationRelativeToNow entry'nin yaratıldığı andan itibaren
// sabit bir ömür tanımlar — erişilse de erişilmese de o süre sonunda ölür.
// SlidingExpiration ise her erişimde ömrü sıfırlar, çok erişilen bir entry teorik
// olarak sonsuza kadar yaşayabilir. Bu yüzden ikisini birlikte kullandığında sliding
// "hareketsizlik nedeniyle erken temizle", absolute ise "ne olursa olsun en geç bu kadar yasa"
// görevini üstlenir. Hybrid yaklaşım production'da en güvenli seçenektir çünkü hem gereksiz bellek
// işgalini önler hem de stale data riskini sınırlar.
public class ExpirationStrategyDemo
{
// Absolute: Sık değişen data, tutarlılık kritik
public void SetUserSession(IMemoryCache cache, string sessionId, UserSession session)
{
var options = new MemoryCacheEntryOptions
{
// Token kesinlikle 1 saat sonra expire olur
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
};
cache.Set(sessionId, session, options);
}
// Sliding: Aktif kullanıcılar için — activity-based expiration
public void SetUserPreferences(IMemoryCache cache, string userId, UserPrefs prefs)
{
var options = new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(20),
// Sliding tek başına tehlikeli! Max süreyi de belirle
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(4)
};
cache.Set(userId, prefs, options);
}
// Hybrid: En güvenli kombinasyon
public void SetProductCatalog(IMemoryCache cache, string key, ProductList products)
{
var options = new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(10), // 10dk erişilmezse temizle
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2) // max 2 saat
};
cache.Set(key, products, options);
}
}
3. Change Token ile Proactive Eviction
// Buradaki temel fikir şudur: tüm ürün entry'leri aynı CancellationToken'ı dinler.
// token iptal edildiğinde hepsi bir anda evict edilir. Interlocked.Exchange kullanılmasının
// sebebi thread-safety'dir — birden fazla thread aynı anda InvalidateAllProducts çağırırsa
// eski token yalnızca bir kez iptal edilir, yeni token ise atomik olarak yerine geçer.
// oldCts.Cancel() çağrıldığında bu token'a bağlı tüm cache entry'leri TokenExpired reason'ıyla evict edilir.
// TTL beklemek yerine olaya bağlı çalıştığı için bu pattern stale data süresini sıfıra yaklaştırır.
public class ProductCacheService
{
private readonly IMemoryCache _cache;
private CancellationTokenSource _resetCts = new();
// Tüm product cache'ini bir anda invalidate et
public void InvalidateAllProducts()
{
var oldCts = Interlocked.Exchange(
ref _resetCts,
new CancellationTokenSource());
oldCts.Cancel();
oldCts.Dispose();
}
public async Task<Product> GetProductAsync(int productId)
{
return await _cache.GetOrCreateAsync($"product:{productId}", entry =>
{
// Bu token cancel edildiğinde entry evict edilir
entry.AddExpirationToken(
new CancellationChangeToken(_resetCts.Token));
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
return FetchFromDatabaseAsync(productId);
});
}
}
// IChangeToken ile File-Based Invalidation
// IFileProvider.Watch() verilen dosyayı izlemeye alır ve dosya her değiştiğinde token'ı tetikler.
// Entry bu token'a bağlı olduğu için dosya değişir değişmez cache otomatik olarak temizlenir
// bir sonraki istek yeni içeriği okur. Bu yaklaşım özellikle appsettings.json dışında tutulan
// harici config dosyaları, feature flag dosyaları ya da şablon dosyaları için son derece kullanışlıdır.
// Alternatif olarak IOptionsMonitor aynı işi yapabilir ama bu pattern daha düşük seviyeli ve daha esnek kontrol sağlar.
public class ConfigCacheService
{
private readonly IMemoryCache _cache;
private readonly IFileProvider _fileProvider;
public string GetConfig(string configFile)
{
return _cache.GetOrCreate($"config:{configFile}", entry =>
{
// Dosya değiştiğinde cache otomatik invalidate olur
var changeToken = _fileProvider.Watch(configFile);
entry.AddExpirationToken(changeToken);
return File.ReadAllText(configFile);
});
}
}
3. IDistributedCache — Redis ile Production Cache
// IDistributedCache ham byte[] ile çalışır, doğrudan kullanmak hem tekrarlı hem de hata-prone'dur.
// Bu wrapper iki şeyi sağlar: birincisi serialization/deserialization'ı tek bir yerde kapsüller
// ikincisi ve daha önemlisi — cache hatalarını yutan bir try/catch bloğu içerir.
// Redis geçici olarak erişilemez hale geldiğinde ya da network hatası oluştuğunda uygulamanın
// çökmemesi gerekir, cache her zaman best-effort bir katmandır. null dönmek yeterlidir
// çünkü çağıran kod cache miss olarak değerlendirip veriyi başka yerden alacaktır.
public class DistributedCacheService
{
private readonly IDistributedCache _cache;
private readonly ILogger<DistributedCacheService> _logger;
// Strongly-typed wrapper — raw distributed cache kullanma
public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
where T : class
{
try
{
var bytes = await _cache.GetAsync(key, ct);
if (bytes is null) return null;
return JsonSerializer.Deserialize<T>(bytes);
}
catch (Exception ex)
{
// Cache hatası uygulamayı çökertmemeli — graceful degradation
_logger.LogError(ex, "Cache read failed for key: {Key}", key);
return null;
}
}
public async Task SetAsync<T>(
string key,
T value,
DistributedCacheEntryOptions? options = null,
CancellationToken ct = default)
{
options ??= new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
};
var bytes = JsonSerializer.SerializeToUtf8Bytes(value);
await _cache.SetAsync(key, bytes, options, ct);
}
}
4. Cache-Aside Pattern (En yaygın production pattern)
// Bu pattern "thundering herd" problemini çözer.
// Thundering herd şu durumda oluşur: popüler bir key expire olduğunda onlarca thread aynı anda
// cache miss görür ve hepsi birden DB'ye gider, bu da ani bir yük patlamasına neden olur.
// SemaphoreSlim ile yalnızca bir thread DB'ye gitmesine izin verilir, diğerleri bekler.
// Lock alındıktan sonra yapılan ikinci cache kontrolü (double-check) ise şunu önler: lock bekleyen thread
// lock'u alan thread zaten DB'den veriyi çekip cache'e yazmışken tekrar DB'ye gitmesin.
// finally bloğunda _lock.Release() çağrısı zorunludur, exception durumunda lock sonsuza kadar
// tutulu kalır aksi takdirde.
public class OrderService
{
private readonly IDistributedCache _cache;
private readonly IOrderRepository _repo;
private readonly SemaphoreSlim _lock = new(1, 1);
// Cache stampede'i önleyen thread-safe implementasyon
public async Task<Order> GetOrderAsync(int orderId, CancellationToken ct)
{
var cacheKey = $"order:{orderId}";
// 1. Cache'e bak
var cached = await _cache.GetStringAsync(cacheKey, ct);
if (cached is not null)
return JsonSerializer.Deserialize<Order>(cached)!;
// 2. Lock al — thundering herd problemini önle
await _lock.WaitAsync(ct);
try
{
// 3. Double-check (başka thread doldurmuş olabilir)
cached = await _cache.GetStringAsync(cacheKey, ct);
if (cached is not null)
return JsonSerializer.Deserialize<Order>(cached)!;
// 4. DB'den çek
var order = await _repo.GetByIdAsync(orderId, ct);
// 5. Cache'e yaz
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(order),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
},
ct);
return order;
}
finally
{
_lock.Release();
}
}
}
Yukarıdaki implementasyonun kritik bir kısıtı vardır: SemaphoreSlim process-local çalışır. Yani uygulamanın birden fazla instance'ı ayaktaysa (Kubernetes'te 3 pod gibi) her pod kendi lock'unu tutar, pod'lar arası koordinasyon olmaz. Bu durumda thundering herd problemi hâlâ oluşabilir, sadece daha küçük ölçekte. Multi-instance ortamlarda bu problemi çözmek için distributed lock mekanizması gerekir. En yaygın yaklaşım Redis üzerinde Redlock algoritmasıdır.
// Redlock için StackExchange.Redis + RedLock.net paketi
public class DistributedOrderService
{
private readonly IDistributedCache _cache;
private readonly IOrderRepository _repo;
private readonly IRedLockFactory _redLockFactory;
public async Task<Order> GetOrderAsync(int orderId, CancellationToken ct)
{
var cacheKey = $"order:{orderId}";
var cached = await _cache.GetStringAsync(cacheKey, ct);
if (cached is not null)
return JsonSerializer.Deserialize<Order>(cached)!;
// Tüm instance'lar aynı lock key'i için Redis'te yarışır
var lockKey = $"lock:order:{orderId}";
await using var redLock = await _redLockFactory.CreateLockAsync(
resource: lockKey,
expiryTime: TimeSpan.FromSeconds(10));
if (!redLock.IsAcquired)
throw new Exception("Lock alınamadı, tekrar dene.");
// Double-check
cached = await _cache.GetStringAsync(cacheKey, ct);
if (cached is not null)
return JsonSerializer.Deserialize<Order>(cached)!;
var order = await _repo.GetByIdAsync(orderId, ct);
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(order),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
}, ct);
return order;
}
}
5. Memory Pressure & Size Limiting
// Program.cs / Startup
// SizeLimit tek başına bir şey yapmaz — entry'lere SetSize() ile boyut atanmadığı sürece
// limit hiçbir zaman aşılmaz gibi davranır. SizeLimit = 1000 ve her entry için SetSize(1) dersen
// en fazla 1000 entry tutulur şeklinde yorumlanabilir. CompactionPercentage limit aşıldığında cache'in
// ne kadarını temizleyeceğini belirler, 0.25 değeri mevcut cache'in %25'inin evict edileceği anlamına gelir.
// çok düşük tutarsan cache sürekli eviction döngüsüne girer, çok yüksek tutarsan gereksiz yere temizlik yapılır.
// ExpirationScanFrequency ise expired entry'lerin ne sıklıkla taranacağını belirler
// erişim olmayan entry'ler bu tarama olmadan otomatik temizlenmez.
builder.Services.AddMemoryCache(options =>
{
// Toplam cache boyutunu sınırla
options.SizeLimit = 1000;
// Bellek baskısında ne kadar compact edilsin (%25)
options.CompactionPercentage = 0.25;
// Expiration tarama sıklığı
options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);
});
6. Caching Anti-Patterns: Yaygın Hatalar ve Çözümleri
// ❌ YANLIŞ: Sliding expiration'ı tek başına kullanmak
entry.SlidingExpiration = TimeSpan.FromMinutes(5);
// Çok erişilen bir key SONSUZA kadar cache'de kalır!
// ✅ DOĞRU: Her zaman absolute ile birleştir
entry.SlidingExpiration = TimeSpan.FromMinutes(5);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
// ❌ YANLIŞ: Cache hatalarını yukarı fırlatmak
var data = await _cache.GetAsync(key); // throw ederse uygulama çöker
// ✅ DOĞRU: Cache'i best-effort olarak kullan
try { cached = await _cache.GetAsync(key); }
catch { /* log, devam et, DB'den çek */ }
// ❌ YANLIŞ: Size belirtmeden SizeLimit kullanmak
services.AddMemoryCache(o => o.SizeLimit = 500);
// entry.SetSize() çağırmazsan eviction çalışmaz!
// ✅ DOĞRU: Her entry için size ver
entry.SetSize(1); // ya da data boyutuna göre
6. Eviction Reason'larını İzleme (Observability)
public class CacheMetrics
{
private readonly Counter<long> _evictionCounter;
public CacheMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("MyApp.Cache");
_evictionCounter = meter.CreateCounter<long>("cache.evictions");
}
public PostEvictionDelegate CreateCallback() =>
(key, value, reason, state) =>
{
_evictionCounter.Add(1,
new TagList
{
{ "reason", reason.ToString() },
{ "key_prefix", GetPrefix(key?.ToString()) }
});
};
private static string GetPrefix(string? key) =>
key?.Split(':').FirstOrDefault() ?? "unknown";
}