Caching: in-memory ve Redis ile distributed cache
Caching'i işliyoruz: sık istenen ama nadiren değişen veriyi geçici saklayarak performansı artırmak, in-memory cache ile Redis tabanlı distributed cache arasındaki fark ve cache'in en zor problemi olan tutarlılık.
Testlerle API'nin doğru çalıştığından emin olduk. Şimdi bir başka soruya geçiyoruz: hızlı çalışıyor mu? Bir uygulama büyüdükçe, aynı veriyi defalarca veritabanından çekmek ciddi bir maliyet haline gelir. Ama bu verinin çoğu her istekte değişmiyor. İşte caching tam bu israfı önler. Bugün, sık istenen ama nadiren değişen veriyi geçici olarak saklayarak performansı nasıl artıracağımızı konuşuyoruz.
Cache Nedir ve Neden Gerekli?
Cache, sık kullanılan veriyi, ona en hızlı ulaşabileceğin bir yerde geçici olarak tutmaktır. Fikir basit: bir veriyi bir kez pahalı bir işlemle (mesela veritabanı sorgusuyla) elde ettiysen, onu bir kenara koy. Bir dahaki sefere aynı veri istendiğinde, veritabanına hiç gitmeden doğrudan o kenardan ver. Böylece hem çok daha hızlı cevap verirsin hem de veritabanının yükünü azaltırsın.
Bunu bir aşçının çalışma tezgahı gibi düşün. Sık kullandığı tuzu, biberi, yağı her seferinde kilere gidip getirmez; onları elinin altında, tezgahın üstünde tutar. Kilere gitmek (veritabanı) yavaştır; tezgah (cache) ise anında erişilebilir. Nadiren kullanılan malzemeleri kilerde tutar ama günde yüz kez uzandığı şeyleri hep yakınında bulundurur. Cache mantığı tam olarak budur: sık dokunulan veriyi en yakın rafta tut.
Neyi Cache'lemeli, Neyi Cache'lememeli?
Her veri cache'lenmeye uygun değil. İyi bir cache adayı iki özelliği taşır: sık istenir ve nadiren değişir. Mesela bir ülke listesi, kategori isimleri ya da site ayarları gibi veriler mükemmel adaylardır; herkes ister ama neredeyse hiç değişmez. Buna karşılık, sürekli değişen ve her isteğinin en güncel halini görmesi gereken veriler (mesela bir kullanıcının anlık bakiyesi) cache için kötü adaylardır. Yanlış veriyi cache'lemek, kullanıcıya eski bilgi göstererek fayda yerine zarar verir.
In-Memory Cache: En Basit Yol
.NET'te caching'in en basit yolu, veriyi doğrudan uygulamanın kendi belleğinde tutmaktır. Buna in-memory cache denir. Kurulumu çok kolay; önce servisi kaydedersin:
builder.Services.AddMemoryCache();
Sonra bir servisin içinde IMemoryCache'i isteyip kullanırsın. Tipik desen şudur: önce cache'e bak, orada varsa doğrudan dön; yoksa veritabanından çek, cache'e koy ve öyle dön:
public async Task> GetCountriesAsync()
{
if (_cache.TryGetValue("countries", out List? cached))
{
return cached!;
}
var countries = await _context.Countries.ToListAsync();
_cache.Set("countries", countries,
TimeSpan.FromMinutes(30));
return countries;
}
Mantığı takip et. Önce TryGetValue ile "countries" anahtarının cache'te olup olmadığına bakıyoruz. Varsa, veritabanına hiç gitmeden doğrudan dönüyoruz; işte hız buradan geliyor. Yoksa veritabanından çekiyor, Set ile cache'e koyuyor ve bir süre (burada 30 dakika) orada kalmasını söylüyoruz. Bu süreye cache'in ömrü denir; süre dolunca veri cache'ten düşer ve bir sonraki istek onu tazeler. İlk istek biraz yavaş olur ama sonraki tüm istekler şimşek hızındadır.
In-Memory Cache'in Sınırı
In-memory cache harika ama önemli bir kısıtı var. Veri, o uygulama örneğinin kendi belleğinde durur. Peki uygulaman tek bir sunucuda değil, birden fazla sunucuda çalışıyorsa? On ikinci yazıda lifetime'ları konuşurken benzer bir dağıtık senaryoya değinmiştik. Her sunucunun kendi belleği, dolayısıyla kendi ayrı cache'i olur. Bir sunucu bir veriyi cache'lerken, diğerinin bundan haberi olmaz. Sonuç: aynı isteğe hangi sunucunun cevap verdiğine göre farklı sonuçlar dönebilir. Bu tutarsızlık, ölçeklenen uygulamalarda ciddi bir sorundur.
Distributed Cache: Redis Devreye Giriyor
Bu soruna çözüm, cache'i her sunucunun kendi belleğinden çıkarıp, hepsinin ortak eriştiği tek bir merkezi yere taşımaktır. İşte distributed cache budur ve bu iş için en yaygın araç Redis'tir. Redis, çok hızlı bir bellek-içi veri deposudur; tüm sunucuların bağlanıp ortak veri paylaştığı merkezi bir cache olarak kullanılır.
Az önceki aşçı benzetmesini genişletelim. Tek bir aşçının kendi tezgahı in-memory cache gibiydi. Ama koca bir restoranda birçok aşçı varsa, her birinin ayrı tezgahında farklı malzemeler olması karışıklık yaratır. Bunun yerine, hepsinin ortak eriştiği merkezi bir hazırlık istasyonu kurarsın; malzemeler orada, tek ve tutarlı bir yerde durur. Redis, mutfaktaki o merkezi istasyondur: tüm sunucular aynı cache'e bakar, herkes aynı veriyi görür.
.NET'te Redis'i distributed cache olarak bağlamak da bir servis kaydından ibaret:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration =
builder.Configuration.GetConnectionString("Redis");
});
Redis'in bağlantı bilgisini, on beşinci yazıda öğrendiğimiz gibi koda gömmüyor, configuration'dan okuyoruz. Bunu kaydettikten sonra, kodda IDistributedCache arayüzünü kullanırsın. Kullanım deseni in-memory cache'e çok benzer (önce bak, yoksa çek ve koy) ama artık cache tek bir sunucuda değil, tüm sunucuların paylaştığı merkezi Redis'te yaşar. Böylece kaç sunucun olursa olsun, hepsi aynı tutarlı cache'i görür.
Cache'in En Zor Problemi: Tutarlılık
Caching kulağa harika geliyor ama bir bedeli var ve bu bedel çoğu zaman göz ardı edilir. Cache'lediğin veri, altındaki gerçek veriden farklılaşabilir. Diyelim ki bir kategori listesini cache'ledin, sonra biri o kategorilerden birini değiştirdi. Cache hâlâ eski hali tutuyor; kullanıcılar cache'in ömrü dolana kadar eski veriyi görür. Buna cache'in "bayatlaması" denir.
Bu problemin iki temel çözümü var. Birincisi, cache'e makul bir ömür vermek; veri her 30 dakikada bir tazelenirse, en fazla 30 dakikalık bir bayatlık olur ki çoğu veri için bu kabul edilebilir. İkincisi, veri değiştiğinde cache'i elle temizlemek; mesela bir kategori güncellendiğinde, ilgili cache anahtarını silersin ki bir sonraki istek taze veriyi çeksin. Hangisini seçeceğin, verinin ne kadar güncel olması gerektiğine bağlıdır. Yazılımda meşhur bir söz vardır: en zor iki şeyden biri cache'i doğru zamanda geçersiz kılmaktır. Bu yüzden cache'i eklemek kadar, ne zaman temizleyeceğini de baştan düşün.
Küçük Bir Deneme
Sık istenen ama nadiren değişen bir veri seç (mesela bir kategori ya da ülke listesi) ve onu döndüren servise in-memory cache ekle. Endpoint'e art arda istek at; ilk istekte veritabanına gidildiğini (mesela bir log ile), sonraki isteklerde ise cache'ten dönüldüğünü gözlemle. Sonra cache'in ömrünü kısa tut (mesela 10 saniye) ve süre dolduğunda verinin nasıl tazelendiğini gör. İstersen bir adım ileri git: veriyi değiştir ama cache'i temizleme, ve kullanıcının bir süre eski veriyi gördüğünü fark et. Bu deney, cache'in hem gücünü hem de tutarlılık tuzağını aynı anda gösterir.
Bir sonraki yazıda health check ve monitoring konusuna geçeceğiz. Bir uygulama canlıda çalışırken, onun sağlıklı olup olmadığını, veritabanına ulaşıp ulaşamadığını nasıl otomatik olarak izleriz? API'ni sadece hızlı değil, aynı zamanda ayakta ve izlenebilir kılmanın yollarını konuşacağız.