Caching(önbellekleme) web uygulamalarında kullanılan en önemli performans iyileştirme yöntemidir. Normal şartlarda üretilmesi gerek zaman, gerekse kaynak tüketimi açısından maliyetli olan sayfa ve nesneler caching mekanizması ile sunucunun belleğinde(RAM) saklanabilmektedir. Böylece çok hızlı erişilebilir bir nesne elde edilerek uygulamadaki sayfaların çok daha hızlı çalışması sağlanabilmektedir. Data caching yapılırken kullanılan Cache nesnesinin kullanımı pratikte oldukça basittir. Ancak Cache nesnesinin daha düzgün çalışabilir olması uygulamaların sağlığı açısından da önemlidir. Zira hiç hesapta olmayan durumlarda karşılaşılacak hatalar nedeniyle kullanıcılara hata sayfası görüntülemek zorunda kalabilir veya Cache nesnesini gereksiz yere tekrar üreterek performans kayıplarına yol açabilirsiniz. Bu yazımızda Cache nesnesinin kullanımında önemli olan iki farklı tasarım deseninin(design pattern) uygulanışını ve bu yöntemlerin faydalarını inceleyeceğiz.
Klasik Yöntem: Singleton Pattern Kullanımı
Öncelikli olarak Cache nesnesinin normal kullanımına ve bu kullanımda ne gibi bir sıkıntı olduğuna bakalım. Aşağıdaki kod örneklerinde veritabanından getirilen kayıtlar Cache nesnesinde saklanmaktadır. Cache nesnesi belirli aralıklarla doldurulmalı ve zaman aşımına uğradığında içeriği yenilenmelidir. Dolayısıyla aşağıdakine benzer bir if kontrolü akışın düzgün gitmesini sağlar. Aslında burada kullandığımız kodlama biçimi Singleton Pattern olarakta bilinmektedir.
Not: Örnekteki kodlar CacheHelper adındaki yardımcı bir sınıf içerisinde yer almaktadır.
CacheHelper.cs
public static class CacheHelper
{
static Cache cache;
static CacheHelper()
{
cache = HttpContext.Current.Cache;
}
public static DataTable GetProductsSingletonPattern()
{
//Cache nesnesinin daha önceden referansının oluşup oluşmadığı kontrol edilir
if (cache["Products"] == null) //Cache nesnesinin içeriği ilk kez burada okunur
{
SqlConnection con = new SqlConnection("data source=localhost; database=Northwind; integrated security=true");
SqlDataAdapter da = new SqlDataAdapter("Select * From Products", con);
DataTable dt = new DataTable();
da.Fill(dt);
cache.Insert("Products", dt, null, DateTime.Now.AddMinutes(5), System.Web.Caching.Cache.NoSlidingExpiration);
}
return (DataTable)cache["Products"]; //Cache nesnesinin içeriği ikinci kez burada okunur
}
}
Görüldüğü gibi öncelikli olarak Cache nesnesinin içeriğinin null olup olmadığı kontrol edilmektedir. Cache null ise veritabanına bağlanarak gerekli veriler alınmakta ve Cache nesnesine aktarılıp metottan geri döndürülmekte, null değilse doğrudan Cache içeriği metottan geri döndürülmektedir. Yazılış ve işleyiş açısından düzgün bir kod yazımı gibi görünse de burada ufak bir ayrıntı var.
Daha Etkili Bir Yöntem: State Bag Access Pattern Kullanımı
Yukarıdaki kod parçasında if koşulunun bulunduğu satıra gelindiğinde Cache null değilse, ama return ifadesinin yer aldığı satıra gelindiğinde Cache bellekten silinmiş ve null gelmişse ne olur? "Yok, o kadar da olmaz diyebilirsiniz", haklısınız. Çünkü iki satır arasındaki geçiş belki de birkaç milisaniye olacaktır. Ancak bu durumunda gerçekleşmesi az da olsa ihtimal dahilindedir. Eğer geliştirdiğiniz projede bu tip bir sorun nedeniyle kullanıcılara hata sayfası görüntülemeniz sıkıntı doğuracaksa burada yapılacak birkaç değişikle kodunuzu güzel şekilde optimize edebilirsiniz. Yapılacak iş ise metot içerisinde ilk olarak Cache nesnesinin değerini bir değişkene aktarmak ve değişken üzerinden gerekli işlemleri gerçekleştirmektir. Buradaki kod yazım biçimi literatürde State Bag Access Pattern olarak bilinmekte ve sadece Cache nesnesi değil, Session, Application gibi diğer durum yönetimi nesnelerinde de kullanmasında fayda olan bir tasarım desenidir.
CacheHelper.cs
public static DataTable GetProductsStateBagAccessPattern()
{
//Bu desende Cache nesnesi sadece bir kez okunur
DataTable dt = cache["Products"] as DataTable;
if (dt == null)
{
SqlConnection con = new SqlConnection("data source=localhost; database=Northwind; integrated security=true");
SqlDataAdapter da = new SqlDataAdapter("Select * From Products", con);
dt = new DataTable();
da.Fill(dt);
cache.Insert("Products", dt, null, DateTime.Now.AddMinutes(5), System.Web.Caching.Cache.NoSlidingExpiration);
}
return dt;
}
State Bag Access Pattern'de durum(state) bilgisine erişim doğrudan nesne üzerinden değil, nesnenin referansını taşıyan bir başka nesne tarafından sağlanır. Yani erişimi sadece bir kez yapılarak nesne referansı farklı bir nesne ile ilişkilendirilir. Bu noktadan sonra Cache bellekten kaldırılsa dahi yeni nesne hala eski Cache'in bellekteki referansını işaretliyor olacaktır ve uygulama içerisinde istenilen veriye ulaşılabilecektir. Geliştirdiğiniz web uygulamalarında Cache nesnesine erişimi bu tasarım kalıbını kullanarak sağlamanızı tavsiye ederim(Best practices).
Cache Nesnesinin Gereksiz Yere Tekrar Üretilmesini Engellemek: Thread Safety Singleton Pattern
Şu ana kadar Cache nesnesine erişimi Singleton Pattern(klasik yöntem) ve State Bag Access Pattern ile nasıl yapıldığını gördük. Cache nesnesine erişimle ilgili bir diğer önemli durumda Cache nesnesine aynı anda birden fazla thread tarafından erişimin olma ihtimalidir. Sonuçta Cache'de genellikle üretilmesi zaman alan nesneleri saklarız. Örneğin veritabanından 10 saniyede sürede çektiğimiz bir veriyi Cache'e atıyoruz. Saat 12:00:00, 12:00:04 ve 12:00:08 gibi üç ayrı talebin geldiği bir durumda veritabanına 3 kez gidilir. Ancak bizim burada istediğimiz sadece ilk talepte veritabanına gidilmesi olacaktır. Bu durumu şekilde şöyle izah edebiliriz:
Şekil: Cache nesnesine gelen taleplerin sırası ve normal işleyişi
Görüldüğü gibi veritabanında 10 saniye sürecek bir işlem esnasında Cache nesnesine 10 saniyelik sürede gelen tüm talepler veritabanına gönderilecektir. Zira ilk talep sonrasında Cache nesnesi ancak 10 saniye sonra doldurulacaktır. Yani sonrasında gelen iki talepte Cache hala null görüneceği için bu işlemler yapılır. Halbuki caching'deki temel mantık veritabanında yoğunluğa yol açan sorguların azaltılması ve sadece belirli aralıklarla veritabanına gidilmesiydi. Burada performans açısından bizi veritabanına daha az götürecek bir yapı sağlıklı olacaktır. Singleton pattern'in Thread Safety yöntemi olarak bilinen nesne kilitlemesi işlemi burada belirttiğimiz sorunun giderilmesi için çözümümüz olacaktır. Thread Safety Singleton Pattern'de aynı anda birden fazla thread'in erişmesinin istenilmediği nesne bir lock bloğu ile kilitlenir. Bu kilitleme esnasında nesneye gelen diğer talepler bekletilecektir. Kitleme bittiğinde ise diğer threadler lock bloğuna giriş yapar ve gerekli işlemleri yaparlar. Dolayısıyla yukarıda şekilde anlattığımız senaryodaki 2. ve 3. talepler veritabanına gitmek yerine 1. talebin bitmesini bekleyecektir. Bu bekleme sonunda uygun if koşulunu yazarak veritabanına atılacak gereksiz sorguları engeleyebiliriz. Aşağıdaki kodlarda bu tasarım deseninin uygulanışı yer almaktadır.
CacheHelper.cs
public static class CacheHelper
{
static Cache cache;
static object obj;
static CacheHelper()
{
cache = HttpContext.Current.Cache;
obj = new object();
}
public static DataTable GetProductsThreadSafe()
{
//Bu desende ise Cache nesnesine eş zamanlı gelecek birden fazla talepten sadece ilk gelen talep
//Cache nesnesini olusturacaktir
if (cache["Products"] == null)
{
lock (obj) //Burada obj nesnesi kilitlenerek farklı thread'lerin blok içerisine erişimi engellenmektedir
{
if (cache["Products"] == null)
{
SqlConnection con = new SqlConnection("data source=localhost; database=Northwind; integrated security=true");
SqlDataAdapter da = new SqlDataAdapter(" Select * From Products", con);
DataTable dt = new DataTable();
da.Fill(dt);
cache.Insert("Products", dt, null, DateTime.Now.AddMinutes(5), System.Web.Caching.Cache.NoSlidingExpiration);
}
}
}
return (DataTable)cache["Products"];
}
}
lock bloğunda kullanılan obj isimli nesne class içerisine bir field olarak tanımlanmıştır. Burada Cache'in null gelmesi durumunda lock bloğuna girilecek ve obj nesnesi farklı talepler tarafından kullanılamayacaktır. Ne zaman ki lock bloğu dışına çıkılacak, bu andan sonra gelen talepler bu bloğa girebilecektir. Burada dikkat çeken noktalardan birisi Cache nesnesinin null olup olmama durumunun iki kez kontrol edilmesidir. Birinci if koşulu Cache'in null olmadığı durumlarda gereksiz nesne kilitlemesini engellemek, ikinci if ifadesi ise birinci talepten sonra gelen ve lock bloğunda bekleyen taleplerin Cache'i gereksiz yere tekrar oluşturulmasını engellemek için gereklidir. lock bloğuna giren ikinci, üçüncü ve sonraki talepler içerideki koşulda Cache'in null olmadığını görecek ve if blokları dışına çıkılarak Cache nesnesi okunacaktır. Aşağıdaki şekilde Thread Safe Singleton Pattern uygulanışında gelen taleplerin işleyişi görülmektedir.
Şekil: Thread Safe Singleton Pattern'in uygulanmasında ardarda gelen 3 talebin işleyişi
Bu yazımda Cache nesnesine erişim sağlarken kullanılan klasik yöntemi ve bu klasik yöntemin yol açabileceği sorunların nasıl giderilebileceğini inceledik. Cache nesnesine erişimde klasik Singleton Pattern'in uygulanmasından ziyade yukarıda incelediğimiz State Bag Access Pattern veya Thread Safe Singleton Pattern'in uygulanmasının daha faydalı olduğunu gördük. Şunu unutmamak gerekir ki bellek(RAM) uygulamalarda en hızlı ve en kolay erişebileceğimiz sistem kaynağımızdır. Eğer sunucuda yeterli miktarda bellek varsa(ki günümüzde uygulama sunucularının bellekleri çok büyük miktarlarda olabilmekte) işleyişi yavaşlatacak birçok işlemin çıktısının Cache'de saklayabiliriz. Tabi ki Cache'e erişimde de en kullanışlı ve sağlıklı yöntemi belirlemekte fayda olacaktır.
Merhaba bir sorum olacak.
Thread Safety Singleton Pattern’da da yine döndüren cache nesnesinin null dönme ihitimali yok mu? Eğer öyleyse Thread Safety Singleton Pattern ve State Bag Access Pattern karışımı bir şey yapsak olmaz mı?
Daha önce anlattığınız gibi bir satır aşağı geçene kadar Cache’in Memory’den silinmiş olabiliceğini düşünerekten bu soruyu sordum.
Thread Safety Singleton Pattern’de dediğiniz gibi return esnasında null olma ihtimali var. Burada obj nesnesi yerine doğrudan cache nesnesi lock’lanabilir. Böylece Cache nesnesi Remove edilmeye çalışıldığında erişim bekletilebilir. Ancak bunun testini yapmak gerekir sağlıksız bir sonuç doğurur mu diye.
Diğer yandan iki pattern’nin karışımını yapmanın doğru olmayacağı kanısındayım. Şimdi kağıt üzerinde karalayıp tasarlamaya çalıştım bu senaryoyu ancak bana mantıklı gelmedi. DataTable nesnesi üzerinden null kontrolü yapmak, object locking ile içeride DataTable’ı yeniden kontrol etmek anlamsız, çünkü DataTable metot içerisinde yerel bir değişken ve null olacağı belli. Benim fikrim iki pattern’in karışımının olmayacağı yönünde. Ama siz kodunu yazıp test edip sağlıklı çalıştığını görürseniz ve buradan paylaşırsanız sevinirim.
hocam öncelikle ellerinize sağlık çok güzel bir makale olmuş.
Benim burada anlamadığım şey 12:00:00 da gelen istek 12:00:08 e kadar bekletilecekmi ?
Eğer böyleyse mesela aynı anda 10.000 istek olduğunda kullanıcılar saniyelerce bekleyecekmi ?
Yada büyük kaynakları cache’lediğimizde ?
Bu arada devamını bekliyorum özellikle Jquery/Wcf/Cache birleşiminden oluşan makalelelerden
Evet, ilk Cache nesnesi oluşturulup belleğe atılana kadar bekler diğer requestler. Ama şu da bir gerçek ki, o kadar sürede 10000 kişinin request yaptığı bir işlem 8 saniye sürüyorsa, orada ciddi bir sorun vardır. Bu kadar kullanıcıya bu kadar uzun süren bir işlem… normal değil:)