Merhaba,
Günümüzde backend uygulamalarında karşılaşılan en yaygın problemlerden biri performans ve ölçeklenebilirlik konusudur. Özellikle yoğun trafiğe sahip uygulamalarda, sık çağrılan endpoint’lerin her istekte doğrudan veritabanına erişmesi ciddi performans sorunlarına yol açabilir. Bu durum yalnızca yanıt sürelerini uzatmakla kalmaz, aynı zamanda veritabanı üzerindeki yükü artırarak sistem kaynaklarının verimsiz kullanılmasına ve maliyetlerin yükselmesine neden olur.
Bu tür problemlerin önüne geçebilmek için backend mimarilerinde cache mekanizmaları kritik bir rol oynar. Cache kullanımı sayesinde sık kullanılan veriler geçici olarak bellekte tutulur ve her istekte tekrar tekrar veritabanına sorgu atılması engellenir. Böylece uygulamalar daha hızlı yanıt verirken, sistem genelinde daha ölçeklenebilir ve sürdürülebilir bir yapı elde edilir.
Bu yazıda, ASP.NET Core özelinde cache kavramını detaylı bir şekilde ele alacağız. Cache nedir, hangi cache türleri vardır, ASP.NET Core’da nasıl uygulanır ve gerçek dünya projelerinde cache kullanırken nelere dikkat edilmelidir gibi sorulara adım adım yanıt vermeyi hedefliyorum.
Cache Nedir? Neden İhtiyaç Duyarız?
Cache, sık kullanılan veya tekrar tekrar erişilen verilerin, daha hızlı erişilebilen geçici bir ortamda saklanmasıdır. Cache mekanizmalarının temel amacı, sistemin asıl veri kaynağı olan veritabanına yapılan erişim sayısını azaltarak uygulamanın genel performansını artırmaktır. Bu sayede hem yanıt süreleri kısalır hem de sistem kaynakları daha verimli bir şekilde kullanılır.
Cache kullanımının sağladığı en önemli avantajlardan biri, veritabanı üzerindeki yükün ciddi oranda azalmasıdır. Her istekte veritabanına sorgu atmak yerine, daha önce alınmış ve hâlâ geçerliliğini koruyan veriler cache üzerinden okunur. Bu durum response sürelerini düşürürken, uygulamanın artan kullanıcı sayısına daha kolay uyum sağlamasını, yani daha ölçeklenebilir bir yapı kazanmasını sağlar.
Bunu daha iyi anlayabilmek için basit bir senaryo düşünelim. Bir ürün listeleme endpoint’inin saniyede yüzlerce kez çağrıldığını ve her istekte doğrudan veritabanına eriştiğini varsayalım. Bu durumda CPU ve veritabanı kaynakları gereksiz yere tüketilir, response süreleri giderek artar ve sistem büyüdükçe bu problemler katlanarak devam eder. Özellikle yüksek trafikli uygulamalarda bu durum, performans darboğazlarının en önemli sebeplerinden biri hâline gelir.
Cache mekanizmaları kullanılarak bu tür problemler büyük ölçüde önlenebilir. Ürün listesi gibi sık değişmeyen veriler belirli bir süre boyunca bellekte veya dağıtık bir cache sisteminde tutulabilir. Böylece aynı veriye yapılan tekrar eden istekler veritabanına gitmeden çok daha hızlı bir şekilde karşılanır ve sistem hem daha hızlı hem de daha sürdürülebilir bir yapıya kavuşur.
ASP.NET Core’da Cache Türleri
ASP.NET Core’da cache kullanımı söz konusu olduğunda en yaygın olarak karşılaşılan iki temel yaklaşım vardır: In-Memory Cache ve Distributed Cache. Her iki yöntem de performans iyileştirmesi sağlamayı amaçlasa da, kullanım alanları ve sundukları avantajlar birbirinden oldukça farklıdır. Bu nedenle hangi cache türünün tercih edileceği, uygulamanın mimarisine ve ihtiyaçlarına göre belirlenmelidir.
In-Memory Cache, uygulama çalıştığı sürece verilerin doğrudan uygulamanın belleğinde saklanmasını sağlar. Distributed Cache ise verilerin uygulamadan bağımsız, merkezi bir cache sistemi üzerinde tutulduğu bir yaklaşımdır. Özellikle Redis gibi çözümler bu kategoride yer alır. Bu iki cache türü, farklı ölçeklerdeki ve farklı mimari gereksinimlere sahip projeler için uygun seçenekler sunar.
| Özellik | In-Memory Cache | Distributed Cache |
| Depolama Alanı | Uygulamanın çalıştığı sunucunun RAM'i. | Harici bir servis |
| Erişim Hızı | En Yüksek. Veri uygulama içinden okunur. | Yüksek Veri ağ üzerinden gelir. |
| Veri Tutarlılığı | Düşük, Her sunucunun kendi cache'i vardır, veriler farklılık gösterebilir. | Tam, Tüm sunucular tek bir merkezi kaynağa bağlanır. |
| Uygulama Restart | Uygulama durduğunda tüm veriler silinir. | Uygulama kapansa bile veriler Redis'te kalmaya devam eder. |
| Veri Tipi | Her türlü C# nesnesi doğrudan saklanabilir. | Veriler Binary veya String (JSON) olarak saklanmalıdır. |
| Maliyet & Kurulum | Ekstra kurulum gerektirmez, maliyeti sıfırdır. | Harici sunucu ve yapılandırma maliyeti vardır. |
| En İyi Senaryo | Tek sunuculu, küçük-orta ölçekli uygulamalar. | Çok sunuculu, mikroservis mimarileri. |
In-Memory Cache (IMemoryCache)
IMemoryCache, verilerin uygulamanın çalıştığı sunucunun RAM belleğinde saklanmasını sağlayan bir cache mekanizmasıdır. ASP.NET Core ile birlikte gelen bu yapı, ekstra bir servis veya harici bir bağımlılık gerektirmez. Bu nedenle kurulumu ve kullanımı oldukça basittir ve kısa sürede projeye entegre edilebilir.
IMemoryCache’in en büyük avantajlarından biri, bellek üzerinden çalıştığı için son derece hızlı olmasıdır. Verilere doğrudan RAM’den erişildiği için response süreleri minimum seviyeye iner. Ayrıca yapılandırmasının kolay olması, geliştiricilerin cache mekanizmasını hızlıca devreye alabilmesini sağlar. Bu özellikleri sayesinde IMemoryCache, özellikle küçük ve orta ölçekli projelerde veya tek sunucu üzerinde çalışan uygulamalarda ideal bir çözümdür.
Bununla birlikte, IMemoryCache’in bazı önemli kısıtları da vardır. Uygulama yeniden başlatıldığında bellekte tutulan tüm cache verileri kaybolur. Ayrıca load-balanced veya birden fazla sunucunun bulunduğu sistemlerde cache verileri sunucular arasında paylaşılmaz; her sunucu kendi belleğinde ayrı bir cache tutar. Bu durum, tutarsız verilerle karşılaşılmasına neden olabilir. Son olarak, cache verileri doğrudan RAM’de saklandığı için bellek kullanımı dikkatli bir şekilde yönetilmelidir. Aksi takdirde aşırı bellek tüketimi, uygulamanın performansını olumsuz etkileyebilir.
public class ProductService
{
private const string ProductsCacheKey = "products";
private readonly IMemoryCache _cache;
private readonly IProductRepository _repository;
public ProductService(IMemoryCache cache, IProductRepository repository)
{
_cache = cache;
_repository = repository;
}
public async Task<List<Product>> GetAllAsync()
{
return await _cache.GetOrCreateAsync(ProductsCacheKey, async cacheEntry =>
{
cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
cacheEntry.Priority = CacheItemPriority.Normal;
return await _repository.GetAllAsync();
});
}
}
Bu örnekte GetAllAsync metodu çağrıldığında, öncelikle cache içerisinde products anahtarı kontrol edilir. Eğer bu anahtara karşılık gelen bir veri daha önce cache’lenmişse, uygulama veritabanına gitmeden doğrudan bellekte tutulan bu veriyi döner. Böylece hem response süresi kısalır hem de veritabanı üzerindeki yük azaltılmış olur.
Cache içerisinde ilgili veri bulunmuyorsa sistem veriyi repository üzerinden veritabanından çeker. Elde edilen bu veri yalnızca istemciye döndürülmekle kalmaz, aynı zamanda belirlenen cache politikalarına uygun şekilde cache’e eklenir.
Bu örnekte cache süresi AbsoluteExpirationRelativeToNow ile beş dakika olarak tanımlanmıştır. Bu süre boyunca yapılan tüm istekler, veritabanına erişmeden cache üzerinden karşılanır. Beş dakikalık sürenin dolmasının ardından cache’teki veri otomatik olarak silinir ve bir sonraki istekte veri tekrar veritabanından alınarak cache güncellenir.
Expiration Türleri
Cache mekanizmalarında expiration ayarları, cache’lenen verinin ne kadar süreyle geçerli olacağını belirler ve sistemin doğruluğu açısından kritik bir rol oynar. ASP.NET Core’da en sık kullanılan iki expiration yaklaşımı Absolute Expiration ve Sliding Expiration’dır.
Absolute Expiration, cache’e eklenen bir verinin, erişilip erişilmediğine bakılmaksızın belirlenen sürenin sonunda otomatik olarak silinmesini sağlar. Örneğin bir veri beş dakika için cache’lenmişse, bu süre dolduğunda veri mutlaka cache’ten çıkarılır. Bu yaklaşım, belirli aralıklarla güncellenmesi gereken ve zaman hassasiyeti olan veriler için oldukça uygundur.
Sliding Expiration ise cache’teki veriye her erişildiğinde expiration süresinin yeniden başlatılmasını esas alır. Yani veri aktif olarak kullanılmaya devam ediyorsa cache’te kalmaya devam eder. Bu yöntem, sık erişilen ancak her zaman güncel olmasının kritik olmadığı veriler için avantaj sağlar ve gereksiz cache silinmelerinin önüne geçer.
Expiration ayarlarının yanlış yapılandırılması güncelliğini yitirmiş verilerin istemcilere dönmesine neden olabilir. Bu nedenle expiration süreleri belirlenirken verinin güncellenme sıklığı, iş kuralları ve performans gereksinimleri mutlaka birlikte değerlendirilmelidir.
Cache Invalidation Neden Önemlidir?
Cache kullanırken en sık karşılaşılan problemlerden biri, cache’te tutulan verinin güncelliğini kaybetmesidir. Bir veri veritabanında güncellendiğinde veya silindiğinde, cache’teki karşılığının da buna uygun şekilde güncellenmesi gerekir. Aksi takdirde uygulama, kullanıcıya hatalı veya eski veriler dönebilir. Bu duruma cache invalidation problemi adı verilir.
Expiration süreleri bu problemi belirli ölçüde azaltabilir; ancak her zaman yeterli değildir. Özellikle veri, belirlenen süreden önce değiştiyse, cache süresi dolana kadar sistem stale data üretmeye devam eder. Bu nedenle cache mekanizmaları yalnızca süre bazlı değil, olay bazlı olarak da yönetilmelidir.
Gerçek dünya uygulamalarında cache invalidation genellikle veri üzerinde bir create, update veya delete işlemi gerçekleştiğinde yapılır. Örneğin bir ürün güncellendiğinde, o ürünü veya ürün listesini temsil eden cache anahtarının manuel olarak silinmesi gerekir. Böylece bir sonraki istekte veri yeniden veritabanından alınır ve cache güncel hâliyle oluşturulur.
Hem In-Memory Cache hem de Distributed Cache çözümlerinde cache invalidation doğru kurgulanmadığında, performans kazanımı sağlanırken veri tutarlılığı kaybedilebilir. Bu nedenle cache kullanımı, mutlaka invalidation stratejisiyle birlikte düşünülmeli ve mimarinin ayrılmaz bir parçası olarak ele alınmalıdır.
Distributed Cache Nedir?
Distributed cache, cache’lenen verilerin uygulamanın kendi belleği yerine, uygulamadan bağımsız çalışan harici bir cache sunucusunda tutulduğu bir yaklaşımdır. Bu yapıda cache, tek bir uygulama instance’ına bağlı değildir ve merkezi bir sistem üzerinden yönetilir. Distributed cache çözümleri arasında en yaygın ve en çok tercih edilen teknoloji Redis’tir.
Distributed cache kullanımının en önemli avantajı, birden fazla uygulama instance’ının aynı cache verisine erişebilmesidir. Özellikle load-balanced ve yatayda ölçeklenmiş sistemlerde, her isteğin farklı bir sunucuya düşebildiği senaryolarda bu özellik kritik hâle gelir. Böylece cache verileri tüm uygulama instance’ları arasında tutarlı bir şekilde paylaşılır.
Redis’in bu noktada öne çıkmasının temel sebeplerinden biri son derece hızlı olmasıdır. Verileri bellek üzerinde tutması sayesinde çok düşük gecikme süreleri sunar. Ayrıca Redis, cache verilerinin ne zaman silineceği konusunda geliştiriciye net bir kontrol imkanı sunar. Bu da stale data riskinin yönetilmesini kolaylaştırır.
Tüm bu özellikler göz önünde bulundurulduğunda, Redis tabanlı distributed cache çözümleri, yüksek trafikli ve production ortamlarında çalışan uygulamalar için güçlü ve güvenilir bir seçenek sunar. Özellikle ölçeklenebilirlik ve veri tutarlılığının önemli olduğu sistemlerde, distributed cache neredeyse vazgeçilmez bir mimari bileşen hâline gelir.
Redis ile Cache Örneği
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "SampleApp";
});
Bu ayar ile ASP.NET Core uygulamasında IDistributedCache implementasyonu olarak Redis kullanılması sağlanır. Configuration alanı Redis sunucusunun adresini belirtirken, InstanceName ise cache key’lerinin uygulama bazında ayrıştırılmasına yardımcı olur.
Buradaki localhost:6379 adresi, Redis’in varsayılan portunu temsil eder. Bu ayarın çalışabilmesi için Redis’in ilgili ortamda ayakta ve erişilebilir olması gerekir. Geliştirme ortamlarında Redis genellikle Docker üzerinden çalıştırılırken, production ortamlarda yönetilen bir Redis servisi tercih edilir. Geliştirme ortamında Redis’i hızlıca ayağa kaldırmak için Docker kullanılabilir. Aşağıdaki komut, Redis’i varsayılan portu ile çalışan bir container olarak başlatır:
docker run -d --name redis-cache -p 6379:6379 redis
public class ProductService
{
private const string ProductsCacheKey = "products";
private readonly IDistributedCache _distributedCache;
private readonly IProductRepository _repository;
public ProductService(IDistributedCache distributedCache, IProductRepository repository)
{
_distributedCache = distributedCache;
_repository = repository;
}
public async Task<List<Product>> GetAllAsync()
{
// Cache kontrolü
var cachedData = await _distributedCache.GetStringAsync(ProductsCacheKey);
if (cachedData != null)
{
// Cache varsa deserialize edip döndür
return JsonSerializer.Deserialize<List<Product>>(cachedData)!;
}
// Cache yoksa veritabanından çek
var products = await _repository.GetAllAsync();
var serialized = JsonSerializer.Serialize(products);
// Cache’e ekle (10 dk boyunca geçerli)
await _distributedCache.SetStringAsync(
ProductsCacheKey,
serialized,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
}
);
return products;
}
}
Bu örnekte IDistributedCache kullanılarak Redis üzerinden basit bir cache mekanizması uygulanmaktadır. İlk olarak products cache anahtarı ile Redis’te veri olup olmadığı kontrol edilir. Eğer ilgili anahtara karşılık gelen veri bulunuyorsa, bu veri deserialize edilerek doğrudan cache üzerinden döndürülür. Böylece veritabanına yapılan gereksiz sorguların önüne geçilmiş olur.
Cache’te veri bulunmaması durumunda ise bir cache miss yaşanır. Bu senaryoda ürün verileri repository aracılığıyla veritabanından çekilir, ardından JSON formatına serialize edilerek Redis’e kaydedilir. Kaydedilen veri için AbsoluteExpirationRelativeToNow kullanılarak verinin cache’te ne kadar süre geçerli olacağı belirlenir. Bu örnekte cache’lenen veri on dakika boyunca geçerli olacak şekilde ayarlanmıştır. Bu süre boyunca yapılan tüm istekler Redis üzerinden karşılanır ve sürenin dolmasıyla birlikte cache’teki veri otomatik olarak silinir.
Dikkat Edilmesi Gereken Noktalar
Distributed cache kullanırken en önemli konulardan biri serialization maliyetidir. Redis nesneleri doğrudan saklamaz; veriler genellikle JSON gibi metinsel formatlara dönüştürülerek cache’lenir. Bu serialize ve deserialize işlemleri, özellikle büyük veri yapılarında ek performans maliyeti oluşturabilir. Bu nedenle cache’e alınacak verilerin boyutu ve yapısı dikkatli bir şekilde seçilmelidir.
Bir diğer önemli konu cache key standardizasyonudur. Cache anahtarlarının rastgele veya tutarsız bir şekilde tanımlanması, zamanla yönetilmesi zor ve hataya açık bir yapı ortaya çıkarır. Özellikle büyük ve uzun ömürlü projelerde, cache key isimlendirmelerinin baştan belirlenmesi ve mümkünse merkezi bir yapıdan yönetilmesi cache kullanımını daha sürdürülebilir hâle getirir.
Son olarak, cache’lenen bir verinin ne kadar süre boyunca geçerli olacağının doğru belirlenmesi büyük önem taşır. Çok uzun süre cache’te kalan veriler güncelliğini kaybedebilir ve stale data problemlerine yol açabilir. Öte yandan, çok kısa sürelerle cache silinmesi de cache kullanımının sağladığı performans avantajını azaltır. Bu nedenle cache süreleri belirlenirken verinin güncellenme sıklığı ve iş gereksinimleri mutlaka göz önünde bulundurulmalıdır.
Redis Sadece String Cache Değildir
Bu yazıda Redis’i, konuyu dağıtmamak adına en temel kullanım şekliyle ele aldım. IDistributedCache ile yaptığımız örnek, Redis’i yalnızca string tabanlı bir cache olarak göstermektedir. Ancak Redis; Hash, Set ve benzeri farklı veri yapılarıyla çok daha gelişmiş senaryolara da olanak tanır. Production ortamlarında Redis kullanmayı planlıyorsanız, bu veri yapılarını ve hangi senaryolarda avantaj sağladıklarını mutlaka incelemenizi öneririm.
Cache Ne Zaman Kullanılmalı?
Cache mekanizmaları, her veri tipi için uygun değildir. Cache’den maksimum fayda sağlanabilmesi için, cache’lenecek verinin bazı temel özelliklere sahip olması gerekir. Özellikle sık okunan, ancak nadiren değişen veriler cache kullanımı için en ideal adaylardır. Bu tür veriler, veritabanına yapılan tekrar eden sorguların büyük bir kısmını ortadan kaldırarak performans kazancı sağlar.
Ayrıca hesaplaması pahalı olan veya birden fazla işlem sonucunda üretilen veriler de cache için oldukça uygundur. Örneğin karmaşık raporlar, istatistiksel veriler ya da filtrelenmiş listeleme sonuçları cache’lenerek ciddi zaman ve kaynak tasarrufu elde edilebilir. Ürün listeleri, kategori bilgileri veya sabit verileri gibi listeleme ve referans verileri de cache kullanımının en yaygın olduğu alanlar arasında yer alır.
Buna karşılık, transactional veriler cache kullanımı için genellikle uygun değildir. Her request’te güncellenen veya anlık olarak mutlak tutarlılık gerektiren verilerin cache’lenmesi, sistemde veri tutarsızlıklarına yol açabilir. Özellikle finansal işlemler, stok düşümleri veya kritik iş akışları gibi senaryolarda cache kullanımı dikkatli değerlendirilmelidir. Bu tür durumlarda performanstan ziyade veri doğruluğu ve tutarlılığı öncelikli olmalıdır.
Sonuç
ASP.NET Core uygulamalarında cache kullanımı, doğru senaryolarda uygulandığında performansı ciddi ölçüde artıran güçlü bir araçtır. Özellikle sık okunan verilerin veritabanına gitmeden cache üzerinden karşılanması, hem response sürelerini düşürür hem de sistem kaynaklarının daha verimli kullanılmasını sağlar. Ancak cache, yanlış veya bilinçsiz kullanıldığında ciddi problemlere yol açabilir. Yanlış expiration ayarları, hatalı cache invalidation stratejileri veya uygunsuz veri tiplerinin cache’lenmesi, uygulamalarda fark edilmesi zor bug’lara ve veri tutarsızlıklarına neden olabilir. Bu nedenle cache kullanımı, yalnızca performans ihtiyacı üzerinden değil, uygulamanın genel mimarisi ve iş kuralları göz önünde bulundurularak değerlendirilmelidir.
Unutulmamalıdır ki cache bir hızlandırıcıdır, veritabanının alternatifi değildir. Doğru tasarlanmış bir mimaride cache, veritabanını destekleyen ve sistemi ölçeklenebilir hâle getiren tamamlayıcı bir bileşen olarak konumlandırılmalıdır.
Diğer Bloglarımda Görüşmek Üzere 👋