Makale Özeti

Bu yazımızda Cache nesnesine erişim için kullanabileceğimiz birkaç farklı yöntemi inceleyeceğiz. Cache nesnesine temel erişim yolunun ne gibi sıkıntıları olduğunu ve bu sıkıntıları gidermek amacıyla başvurabileceğimiz iki farklı yöntem olan State Bag Access Pattern ve Thread Safety Singleton Pattern'lerinin uygulanışını göreceğiz.

Makale

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; Waitfor delay '00:00:20'", 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.


Uğur UMUTLUOĞLU
Microsoft MVP (ASP.NET)
www.umutluoglu.com
www.nedirtv.com