Önbelleğe alma, modern, yoğun trafikli uygulamaların önemli bir parçasıdır. Web uygulamanız şu anda çok fazla trafik almıyor olsa bile, daha sonra alabilir ve onu en baştan önbelleğe almayı göz önünde bulundurarak tasarlamak iyi bir fikirdir.
.NET, önbelleğe alınmış öğeleri Redis, NCache vb. gibi uzak bir paylaşılan sunucuda depolamak için yerel, bellek içi önbelleğe alma (MemoryCache) ve dağıtılmış önbelleğe alma (IDistribatedCache) için mekanizmalar sağlar:
Serenity, .NET önbellekleme mekanizmalarını temel alır ve yerel ve dağıtılmış önbelleklerle çalışmayı kolaylaştırmak için bazı yardımcı işlevler sağlar.
Yerel terimi, önbelleğe alınmış öğelerin yerel bellekte tutulduğu anlamına gelir (bu nedenle serileştirme söz konusu değildir).
Uygulamanız bir web grubuna dağıtıldığında, bellek içi önbelleğe alınan öğeler birden fazla sunucu arasında paylaşılmadığından yerel önbelleğe alma yeterli olmayabilir veya bazen uygun olmayabilir. Dağıtılmış önbelleklemenin devreye girdiği yer burasıdır.
MemoryCacheExtensions Sınıfı
Statik MemoryCacheExtensions sınıfı MemoryCache, .
Uzantı Get<Item>, önbelleğe alınmış mevcut bir öğeyi alma veya mevcut değilse onu bir fabrika işlevi aracılığıyla başlatma olanağı sağlar. Ayrıca, öğenin önbellekte ne kadar süre saklanacağını (örneğin son kullanma tarihi) belirtmenize de olanak tanır.
memoryCache.Get<MyCachedItemType>("SomeUniqueCacheKey", TimeSpan.FromMinutes(5),
() => { // this is the factory or loader function
var item = new MyCachedItemType();
// populate item from database etc.
return item;
});
Uzatma yöntemimizin akış şeması Get:

GetOrCreate.NET'te de benzer bir yöntem var ancak süre sonu belirtmenize izin vermiyor ve özel bir değere sahip CacheExtensionsbir sonucu önbelleğe almıyor .not foundDBNull
Kullanıcı Profili Önbelleğe Alma Örneği
Sitemizde çeşitli sorgular kullanılarak oluşturulan bir profil sayfamızın olduğunu varsayalım. Bu sayfa için bir modelimiz olabilir, örneğin bir kullanıcının tüm profil verilerini içeren UserProfile sınıfı ve bunu belirli bir kullanıcı kimliği için üreten bir GetProfile yöntemi.
public class UserProfile
{
public string Name { get; set; }
public List<CachedFriend> Friends { get; set; }
public List<CachedAlbum> Albums { get; set; }
...
}
public UserProfile GetProfile(int userID)
{
using (var connection = new SqlConnection("..."))
{
// load profile by userID from DB
}
}
LocalCacheExtensions.Get yöntemini kullanarak bu bilgileri bir saat boyunca kolayca önbelleğe alabilir ve bu bilgilere her ihtiyaç duyulduğunda DB çağrılarından kaçınabiliriz.
public UserProfile GetProfile(int userID)
{
return localCache.Get<UserProfile>(
cacheKey: "UserProfile:" + userID,
expiration: TimeSpan.FromHours(1),
loader: delegate {
using (var connection = new SqlConnection("..."))
{
// load profile by userID from DB
}
}
);
}
WEB Grupları ve Önbelleğe Alma
Bir sosyal ağ sitemizin ve milyonlarca kullanıcı profilimizin olduğunu düşünelim. Bazı ünlü kullanıcıların profil sayfaları dakikada yüzlerce veya binlerce ziyaret alıyor olabilir.
Bir kullanıcı profili oluşturmak için birden fazla SQL sorgusuna ihtiyacımız var (arkadaşlar, albüm adları ve resim sayıları, profil bilgileri, son durum vb.).
Bir kullanıcı profilini güncellemediği sürece sayfasında gösterilen bilgiler neredeyse statik olacaktır. Böylece profil sayfalarının anlık görüntüsü 5 dakika veya 1 saat vb. süreyle önbelleğe alınabilir.
Ancak bu yeterli olmayabilir. Yüz milyonlarca profil ve kullanıcıdan bahsediyoruz. Kullanıcılar yalnızca bazı profil sayfalarına bakmaktan çok daha fazlasını yapıyor olacaktır. Dünyanın çeşitli coğrafi konumlarına dağıtılmış birden fazla sunucuya (WEB Çiftliği) ihtiyacımız olacaktır.
Belirli bir zamanda, tüm bu sunucular çok önemli bir kişinin (VIP) profilini yerel önbellekte önbelleğe almış olabilir. VIP, profilinde bir değişiklik yaptığında, tüm bu sunucuların yerel önbelleğe alınmış profillerini yenilemeleri gerekir ve bu, birkaç saniye içinde gerçekleşir. Artık kullanıcı başına yük yerine sunucu başına yük ile ilgili bir sorunumuz var.
Bu sunuculardan biri VIP profilini SQL veritabanından yükleyip önbelleğe aldığında, diğer sunucular veritabanına çarpmadan aynı bilgileri kullanabilir. Ancak her sunucu, önbelleğe alınmış bilgileri kendi yerel belleğinde sakladığından, bu bilgilere diğer sunucuların erişmesi önemsiz değildir.
Tüm sunucuların erişebileceği ortak bir hafızamız olsaydı:
| Bilgi anahtarı | Değer |
|---|---|
| Profil:ÇokFamousOne | (VeryFamousOne için önbelleğe alınmış bilgiler) |
| Profil:SomeAnother | ... |
| ... | ... |
| ... | ... |
| Profil: JohnDoe | ... |
Bu belleğe dağıtılmış önbellek adını verelim. Eğer tüm sunucular DB'yi denemeden önce bu ortak belleğe bakarsa, sunucu başına yükleme sorununu önlemiş oluruz.
Memcached ve Redis dahil olmak üzere dağıtılmış önbellek sistemlerinin birçok çeşidini bulabilirsiniz. Bunlara veritabanları da denir NoSQL. Bunları basitçe uzak bir sözlük olarak düşünebilirsiniz. Anahtar/değer çiftlerini hafızalarında saklarlar ve bunlara mümkün olduğunca hızlı erişmenizi sağlarlar.
Uyarı! Dağıtılmış önbellek, doğru kullanıldığında tıpkı yerel önbellek gibi uygulamanızın performansını artırabilir. Aksi takdirde, ağ aktarımı ve serileştirme maliyeti söz konusu olduğundan yerel önbelleğe göre daha kötü bir etkiye sahip olabilir. Dolayısıyla "eğer bazı şeyleri dağıtılmış önbellekte tutarsak sitemiz daha hızlı çalışır" bir efsanedir.
Önbelleğe alınan veriler çok fazla olduğunda, bir bilgisayarın belleği tüm anahtar/değer çiftlerini depolamaya yetmeyebilir. Bu durumda Redis gibi sunucular verileri kümeleyerek dağıtır. Bu, tuşların ilk harfiyle yapılabilir. Bir sunucu A ile başlayan, diğerleri B ile başlayan çiftleri tutabilir, vb. Bu amaç için genellikle anahtarların karma değerini kullanırlar.
DistributedCacheExtensions Sınıfı
DistributedCacheExtensions'a benzer şekilde MemoryCacheExtensions, ve dışındaki türler için JSON serileştirmesiyle çalışmayı ve kullanmayı kolaylaştırmak için çeşitli uzantı yöntemleri içerir .IDistributedCacheStringbyte[]
SetAutoJson genel yöntemi, önbellekteki bir öğenin a stringveya byte[]değerleriyle ayarlanmasına olanak tanır veya JSON, onu başka herhangi bir tür için serileştirir.
distributedCache.SetAutoJson("SomeByteArray", new byte[] { 1, 3, 5, });
distributedCache.SetAutoJson("SomeString", "SomeString");
distributedCache.SetAutoJson("SomeKey", new MyObject { ... });
distributedCache.SetAutoJson("AnotherKey", new MyObject { ... }, TimeSpan.FromMinutes(5));
GetAutoJson genel yöntemi , yaptığının tam tersini yapar SetAutoJson. JSON, stringveya dışında herhangi bir türü seri durumdan çıkarır byte[].
var byteArray = distributedCache.GetAutoJson<byte[]>("SomeKey");
var str = distributedCache.GetAutoJson<string>("SomeKey");
var myObj = distributedCache.GetAutoJson<MyObject>("SomeKey");
Serenity herhangi bir özel uygulama sağlamaz IDistributedCache. Serene ve StartSharp DistributedMemoryCachevarsayılan olarak bunu kullanır.
Varsayılan uygulamalar, dosyadaki çağrı yoluyla kaydedilir IMemoryCacheve bu çağrı, CoreServiceCollectionExtensions'ın uzantı yöntemini çağırır.IDistributedCacheStartup.csAddServiceHandlersStartup.csAddCaching
Redis veya SQL Server gibi diğer dağıtılmış önbellek sunucularını etkinleştirme talimatları için .NET Önbelleğe Alma belgesini görebilirsiniz . Dosyadaki veya çağrılarından IDistributedCacheönce özel uygulamayı kaydettiğinizden emin olun .AddServiceHandlersAddCachingStartup.cs
İki Seviyeli Önbelleğe Alma
Yerel (bellek içi) önbelleğe almayı kullandığınızda, bir sunucu bazı bilgileri önbelleğe alabilir ve mümkün olduğu kadar hızlı bir şekilde alabilir, ancak diğer sunucular önbelleğe alınan verilere erişemediğinden, aynı bilgileri veritabanından sorgulamak zorundadırlar.
Bazı serileştirme/seri durumdan çıkarma ve ağ gecikmesi ek yükü nedeniyle diğer sunucuların önbelleğe alınmış verilere erişmesine izin vermek için dağıtılmış önbelleğe almayı tercih ederseniz, bazı durumlarda performansı düşürebilir.
Önbelleğe almayla ilgili ele alınması gereken başka bir sorun daha vardır: önbelleğin geçersiz kılınması:
Bilgisayar Bilimlerinde yalnızca iki zor şey vardır: önbelleği geçersiz kılmak ve şeyleri adlandırmak.
--Phil Karlton
Bazı bilgileri önbelleğe aldığınızda, kaynak veriler değiştiğinde önbelleğe alınan bilgilerin geçersiz kılındığından (yeniden oluşturulduğundan veya önbellekten kaldırıldığından) emin olmalısınız.
Yerel Önbelleği ve Dağıtılmış Önbelleği Senkronize Etme
Basit bir algoritmayı takip ederek her iki dünyanın en iyilerinden faydalanabiliriz:
- Yerel önbellekte bir anahtar olup olmadığını kontrol edin.
- Anahtar yerel önbellekte mevcutsa değerini döndürün.
- Anahtar yerel önbellekte yoksa dağıtılmış önbelleği deneyin.
- Anahtar dağıtılmış önbellekte mevcutsa değerini döndürün ve onu da yerel önbelleğe ekleyin.
- Anahtar dağıtılmış önbellekte yoksa, onu veritabanından üretin ve hem yerel önbelleğe hem de dağıtılmış önbelleğe ekleyin. Üretilen değeri döndürün.
Bu şekilde, bir sunucu bazı bilgileri yerel önbellekte önbelleğe aldığında, bunu aynı zamanda dağıtılmış önbellekte de önbelleğe alır, ancak bu sefer diğer sunucular, eğer belleklerinde yerel bir kopya yoksa, dağıtılmış önbellekteki bilgileri yeniden kullanabilirler.
Tüm sunucuların yerel bir kopyası olduğunda, hiçbirinin dağıtılmış önbelleğe tekrar erişmesine gerek kalmayacak, böylece serileştirme ve gecikme yükü ortadan kalkacak.
Yerel Kopyaları Doğrulama
Her şey yolunda görünüyor. Ancak şimdi önbellek geçersiz kılma sorunumuz var. Sunuculardan birinde önbelleğe alınan veriler değiştirilirse ne olur? Yerel olarak önbelleğe alınmış kopyalarını geçersiz kılabilmeleri için onları bu değişiklikten nasıl haberdar edeceğiz ?
Dağıtılmış önbellekteki değeri değiştirirdik, ancak artık dağıtılmış önbelleği kontrol etmedikleri için (son algoritmada 2. adımdaki kısayol) fark edilmezler.
Bu soruna bir çözüm, yerel kopyaları belirli bir süre (örneğin 5 saniye) tutmak olabilir. Böylece bir sunucu önbelleğe alınan veriyi değiştirdiğinde diğer sunucular çoğunlukla 5 saniye boyunca güncel olmayan bilgileri kullanır.
Bu yöntem, önbelleğe alınmış aynı bilgilerin tekrar tekrar kullanılmasını gerektiren toplu işlemlerde yardımcı olacaktır. Ancak dağıtılmış önbellekte hiçbir şey değişmese bile, her 5 saniyede bir dağıtılmış önbellekten yerel önbelleğe bir kopya almamız gerekir. Önbelleğe alınan veriler büyükse, bu durum ağ bant genişliği kullanımını ve seri durumdan çıkarma maliyetini artıracaktır.
Dağıtılmış önbellekteki verilerin yerel kopyadan farklı olup olmadığını bilmenin bir yoluna ihtiyacımız var. Bunun hayal edebileceğim birkaç yolu var:
- Karmayı verilerle birlikte yerel ve dağıtılmış önbellekte depolayın (hafif karma hesaplama maliyeti)
- Artan sürüm numarasına sahip verileri depolayın (iki sunucunun aynı sürüm numaralarını oluşturmadığından nasıl emin olunur?)
- Verilerin dağıtılmış önbellekte en son ayarlandığı zamanı depolayın (zaman senkronizasyonu sorunları)
- Verilerin yanında rastgele bir sayı (nesil) saklayın
Serenity, sürüm olarak nesil sayılarını (rastgele int) kullanır.
Yani dağıtılmış önbellekte bir değer sakladığımızda, diyelim ki SomeCachedKey , ayrıca SomeCachedKey$GENERATION$ anahtarıyla rastgele bir sayı da saklıyoruz .
Şimdi önceki algoritmamız şu oluyor:
- Yerel önbellekte bir anahtar olup olmadığını kontrol edin.
- Anahtar yerel önbellekte mevcutsa
- Kendi neslini dağıtılmış önbellekteki nesille karşılaştırın
- Eşitlerse yerel olarak önbelleğe alınan değeri döndürün
- Eşleşmiyorlarsa 4'e devam edin
- Anahtar yerel önbellekte yoksa dağıtılmış önbelleği deneyin.
- Anahtar dağıtılmış önbellekte mevcutsa, değerini döndürün ve onu da oluşturmanın yanı sıra yerel önbelleğe ekleyin.
- Anahtar dağıtılmış önbellekte mevcut değilse, onu veritabanından oluşturun ve rastgele bir nesille hem yerel önbelleğe hem de dağıtılmış önbelleğe ekleyin. Üretilen değeri döndürün.
Önbelleğe Alınmış Birden Çok Öğeyi Tek Çekimde Doğrulama
Bazı tablolardan üretilen verileri önbelleğe almış olabilirsiniz. Bu tablo için dağıtılmış önbellekte birden fazla anahtar bulunabilir.
Diyelim ki bir profil tablonuz var ve profil öğelerinizi Kullanıcı Kimliği değerlerine göre önbelleğe aldınız.
Bir kullanıcının profil bilgileri değiştiğinde, önbelleğe alınmış profilini önbellekten kaldırmayı deneyebilirsiniz. Peki ya bilmediğiniz başka bir sunucu veya uygulama aynı kullanıcı profili verilerinden oluşturulan bazı bilgileri önbelleğe aldıysa? Bazı kullanıcı kimliklerine bağlı olarak dağıtılmış önbellekte hangi önbelleğe alınmış bilgi anahtarlarının bulunduğunu bilmiyor olabilirsiniz.
Çoğu dağıtılmış önbellek uygulaması, bir dizeyle başlayan tüm anahtarları bulmanın bir yolunu sağlamaz veya hesaplama açısından yoğundur (sözlük tabanlı oldukları için).
Dolayısıyla, bir dizi veriye bağlı olarak tüm öğelerin süresinin dolmasını istediğinizde bu mümkün olmayabilir.
Serenity, öğeleri önbelleğe alırken, grubun bağlı olduğu veriler değiştiğinde bunların süresinin dolmasını sağlayacak bir grup anahtarı belirlemenize olanak tanır.
Diyelim ki bir uygulama ID 17'nin profil verilerine sahip bir kullanıcıdan CachedItem17 üretti ve bu ID'yi grup anahtarı olarak kullanıyoruz (Group17_Generation):
| Anahtar | Değer |
|---|---|
| Önbelleğe Alınmış Öğe17 | cxyzyxzcasd |
| Önbelleğe AlınmışItem17_Generation | 13579 |
| Grup17_Generation | 13579 |
Burada grubun rastgele üretimi (versiyonu) 13579'dur. Önbelleğe alınmış verilerle (CachedItem17) birlikte, bu verileri ürettiğimizde grup üretimi ne olursa olsun (CachedItem17_Generation) depoladık.
Başka bir sunucunun, Kullanıcı 17'nin verilerinden AnotherItem17'yi önbelleğe aldığını varsayalım:
| Anahtar | Değer |
|---|---|
| Önbelleğe Alınmış Öğe17 | cxyzyxzcasd |
| Önbelleğe AlınmışItem17_Generation | 13579 |
| Başka Bir Öğe17 | uwsdasdas |
| AnotherItem17_Generation | 13579 |
| Grup17_Generation | 13579 |
Burada Group17_Generation'ı yeniden kullandık, çünkü dağıtılmış önbellekte zaten bir grup sürüm numarası vardı, aksi takdirde yeni bir tane oluşturmamız gerekecekti.
Artık önbellekteki her iki öğe de (CachedItem17 ve AnotherItem17) geçerlidir çünkü sürüm numaraları grup sürümüyle eşleşir.
Birisi Kullanıcı 17'nin verilerini değiştirmişse ve biz onunla ilgili önbelleğe alınmış tüm öğelerin süresinin dolmasını istiyorsak, yalnızca grup oluşturmayı değiştirmemiz gerekir:
| Anahtar | Değer |
|---|---|
| Önbelleğe Alınmış Öğe17 | cxyzyxzcasd |
| Önbelleğe AlınmışItem17_Generation | 13579 |
| Başka Bir Öğe17 | uwsdasdas |
| AnotherItem17_Generation | 13579 |
| Grup17_Generation | 54237 |
Artık önbelleğe alınan tüm öğelerin süresi doldu. Önbellekte var olmalarına rağmen nesillerinin grup nesliyle eşleşmediğini, dolayısıyla geçerli sayılmadığını görüyoruz.
Kullandığımız grup anahtarları genellikle verinin üretildiği tablonun adıdır.
ITwoLevelCache Arayüzü
Fark ettiğiniz gibi hafızayı ve dağıtılmış önbellekleri birlikte kullanmamız gerekiyor.
ITwoLevelCache arayüzü , bir bellek önbelleği ile dağıtılmış önbelleğin birleşimidir:
public interface ITwoLevelCache
{
IMemoryCache Memory { get; }
IDistributedCache Distributed { get; }
}
TwoLevelCacheExtensions Sınıfı
Statik TwoLevelCacheExtensionsITwoLevelCache sınıfı , örnekler üzerinde çalışan ve şu ana kadar konuştuklarımızı (örn. aralarında senkronizasyon ve önbellek geçersiz kılma) uygulayan uzantı yöntemlerini içerir .
TwoLevelCacheExtenison.Get Yöntem
Yerel önbellekten bir değer okumaya çalışır. Orada bulunamazsa (veya süresi dolmuş bir sürümü varsa), dağıtılmış önbelleği dener.
Her ikisi de belirtilen anahtarı içermiyorsa, bir yükleyici işlevini çağırarak değer üretir ve değeri belirli bir süre sonu için yerel ve dağıtılmış önbelleğe ekler.
Get yönteminin iki aşırı yüklemesi vardır. Biri yerel ve dağıtılmış önbellekler için ayrı ayrı sona erme süresini alır, diğeri ise her ikisi için de yalnızca bir süre sonu parametresine sahiptir.
Bir grup anahtarı kullanılarak, her iki önbellek türündeki bu grubun üyesi olan tüm öğelerin süresi bir defada dolabilir (bu, nesile dayalı bir sona ermedir, zamana değil).
Gruba ait bir öğe her istendiğinde grup oluşturmanın kontrol edilmesini önlemek için grup oluşturmanın kendisi de yerel önbellekte önbelleğe alınır. Bu nedenle, bir nesil numarası değiştiğinde, yerel önbelleğe alınan öğelerin süresi 5 saniye sonra dolabilir.
Bu, bir web grubu kurulumunda bu stratejiyi kullanırsanız, bir sunucuda değişiklik meydana geldiğinde diğer sunucuların eski yerel önbelleğe alınmış verileri 5 saniye daha kullanmaya devam edebileceği anlamına gelir.
Bu, yapılandırmanız için bir sorunsa TwoLevelCache'e bağlı olmak yerine doğrudan DistributedCache yöntemlerini kullanmalısınız.

CachedProfile GetCachedProfile(int userID)
{
twoLevelCache.Get("CachedProfile:" + userID, TimeSpan.FromDays(1), "SomeGroupKey",
() =>
{
using (var connection = new SqlConnection("..."))
{
connection.Open();
return LoadProfileFromDB(connection, userID);
}
});
}
CachedProfile LoadProfileFromDB(IDbConnection connection, int userID)
{
// ...
}
TwoLevelCache.GetLocalStoreOnly Yöntem
Öğeleri dağıtılmış önbellek yerine yalnızca yerel önbellekte depolamak istiyorsanız GetLocalStoreOnly yararlı olabilir.
Bir sunucu tarafından önbelleğe alınan veriler diğerleri için yararlı olmadığında (sunucudan sunucuya değişiklikler), serileştirme/seri durumdan çıkarma çok büyük veya yavaş olduğunda, bu tür verileri dağıtılmış önbellekte depolamak anlamlı değildir.
Peki bu durumda neden doğrudan LocalCache kullanmıyorsunuz?
Bir grup anahtarı belirlemek ve o grubun kaynak verileri değiştiğinde (dağıtılmış önbellekte saklanıyorlarmış gibi) yerel önbelleğe alınmış öğelerin geçerliliğini kolayca sona erdirmek istiyorsanız bunu yapabilirsiniz, ancak yapamazsınız.
TwoLevelCache.ExpireGroupItems Yöntem
Bu yöntem, bir grup anahtarının üyesi olan tüm öğelerin süresinin dolmasına olanak tanır. Grup anahtarını yerel önbellekten ve dağıtılmış önbellekten kaldırır, böylece bir sonraki sorgulandığında başka bir sürüm oluşturulur.
twoLevelCache.ExpireGroupItems("SomeGroupKey");
Bunu verileri değiştiren yöntemlerden çağırmalısınız.
Oluşturma, Güncelleme, Silme ve Silmeyi Geri Alma işleyicileri bunu grup anahtarı olarak ConnectionKey.TableName ile otomatik olarak yapar.
TwoLevelCache.Remove Yöntem
Bir öğeyi ve sürümünü yerel ve dağıtılmış önbellekten kaldırır.