C# Data Caching ve Uygulama Senaryosu

By Burak TUNGUT - 1.12.2013 - 11 Yorum - Kategori C#

Proje KatmanlarıUzun, hatta baya bir uzun aradan sonra herkese merhabalar :)

Haziran 2013'de yazdığım son makaleden sonra yaklaşık 6 ay geçmiş. Açıkçası nasıl geçti bu kadar zaman anlamış değilim. Neyse. Klişe lafları bırakıp işimize bakalım :)

Bu makale de Data Caching konusunu ele alıyor olacağız. Makaleyi yazmadan önce anlaşılır ve uyarlanabilir olması için daha önce bir kaç projede kullandığım yapı üzerinden ilerlemeye ve bunu bir diyagrama dökmeye karar verdim.

Açıkçası hayatımda ilk kez bir diyagram hazırladım (Yazılım Mühendisliği derslerinde ki Use-Case ve UML'leri saymazsak tabi ki :) ). 

Yapacağımız örnekte ki projeleri (katmanları) Presentation ve Infrastructure olmak üzere ikiye ayırdım.
Presentation kısmını 1 adet Asp.Net MVC projesi üstleniyor olacak. Bunun haricinde Business, Data-Access, Caching ve Core katmanları ise Infrastructure içerisinde olacaklar.

Senaryo ve Akış

Üretiminde içinde bulunduğunuz, senaryosu ürün bazlı olan bir Web projesi düşünün. Ya da kısa bir değiş ile bir E-Ticaret sitesi. Üst tarafta kategoriler, solda markalar ve layoutda listelenen ürünler. Şimdiye kadar ki kısımda pek sorunumuz yok.

Peki her bir ürüne tıkladığımızda (ürünün detay sayfasına gittiğimizde) arka tarafta bir SQL sorgusunun çalıştığını size hatırlatsam. Hatta bir de sitemizin popüler olduğunu söylesem. Bence daha fazla ileriye gitmeye gerek yok. Sorunların ortaya çıkması için yeterli neden saydım gibi :)

Açıklık getirecek olursak; binlerce request'in olduğu bir sistemde değişen birşeyler olmamasına rağmen her requesti bir veritabanı sorgusu ile karşılıyor olmak hiç ama hiç tutarlı bir çözüm olmayacaktır. Bilhassa da sistem kaynakları artık yetersiz hale geldiği zaman.

Bu ve benzeri durumların önüne geçmek için caching kullanılabilir. Bahsettiğimiz caching presentation seviyesinde bir Output Cache olabileceği gibi, veritabanı ile sistem arasına yerleştirilecek bir data-cache katmanıda olabilir.

Önceki makalelerimde benzer bir senaryo üzerinden Asp.Net MVC ile Output Cache kullanımını anlatmıştım. Fakat Output Cache her zaman her problem için yeterli olmuyor.

Örneğin; ürünleri arama kriterlerine göre database'den alan ve listeleyen bir Action ya da Web Form düşünelim. Arama kriterleri olarakta Kategori, Marka, Stok Durumu, Ürün Kodu ve Kampanya durumu, Sayfa ve Listelenecek ürün adetlerini aldığımızı varsayalım.
Böyle bir durumda söz konusu Output'ları Cache edebilmeniz için gelecek her request'i 5 ayrı kombinasyon için de Cache etmeniz gerekir.

Bir düşünelim, 10 kategori, her bir kategori altında da ortalama 5 marka olduğunu, 2 adette stok ve kampanya durumu olduğunu, ortalama 10 sayfa olduğunu varsayarsak en iyi ihtimalle bile (Ürün ismi gibi string bir parametreyi hiç varsaymazsak) 1000 adet stream için Output Cache yaratmış oluruz.

Aslında bakarsak böyle bir durumda ürünlerin (nesnelerin) hepsini ya da bir kısmını Data Cache olarak kaynaklarda tutmak daha tutarlı bir çözüm olacaktır.
Bu durumda uygulanabilecek iki çözüm ise şu şekilde olabilir;

  1. Ürünlerin hepsi database'den çekilir ve bir collection olarak Cache'e eklenir.
  2. Her bir gelen requestte Cache içerisinde ürünün olup olmadığı kontrol edilir. Yoksa databaseden çekilir ve Cache'e eklenir. Var ise direk Cache'den alınır.

Bu iki durum arasında karar vermek için sanıyorum ki en önemli kriter her bir ürünün (entity) içerdiği property sayı ve içerikleri. Yani dolaylı yoldan her bir nesnenin ortalama olarak bellekteki boyutu. Diğeri ise toplam da kaç adet ürün kaydının olduğudur.

Makalenin bu kısmında, yukarıdaki iki yöntemden hangisini kullanacağınız gibi makalenin seyrini değiştirecek konular ile devam etmeyeceğim. Ancak son geliştirdiğim bayi bazlı bir projede 200 adet gibi az sayıda ürün olduğu ve sadece bayilerin kullandığı bir sistem olduğu için 1. yöntemi tercih etmiştim.

Nitekim bu makalede ki uygulama da 2. yöntemi tercih ediyor olacağız. Senaryoyu tekrar ele alıp toparlamakta fayda var çünkü yukarıya baktıkça detaya çok girdiğimi fark ettim :)
Projemizde Northwind database'de ki kategorileri Id'sine göre çekerek detay sayfasında ilgili bilgileri veren bir web projesi hazırlarken arka tarafta da Business, Core ve Caching gibi katmanları yazıyor olacağız.

Uygulamada ki Katman ve Sınıflar

Cache katmanının kullanacağı base sınıfları barındıran Core, gerekli Cache aksiyonlarını alacak Cache, data-acess işlemini Entity Framework vasıtasıyla gerçekleştirecek Data.Model ve bu katmanı kullanacak Business katmanı olmak üzere Infrastructure da toplam 4 katman yer alırken Presentation katmanı olarak bir adet Asp.Net MVC projesi yazıyor olacağız.

Her ne kadar makale konuları arasında olmasada bir de Unit Test projesi yaratarak Cache katmanında önemli logic'leri test eden sınıf ve methodlar yazıyor olacağız.
Uygulamayı adım adım yapmak için DataCache adında bir blank solution oluşturarak aşağıdaki adımları izleyebilir ya da makalenin en altında kaynak kodları direk indirebilirsiniz.

DataCache.Core

Bu projede CacheProvider ve CacheKey olmak üzere iki farklı sınıf yazıyor olacağız.

Data Cache'te Cache'e alınacak olan objeler Key Value (Anahtar-Değer) ilişkisi içerisinde tutulur. Projede Key için elle string girmeyeceğiz. Bu işi bizim yerimize bir sınıfın generate edeceği Key üstlenecek.

CacheKey sınıfı ile öyle bir yapı oluşturacağız ki; constructor'da sadece tablo adını ve tablo içerisindeki kaydın ID'sini parametre olarak vereceğiz ve geriye generate edilmiş unique bir key alacağız.
CacheKey sınıfına neden ihtiyaç duyduğumu CacheProvider sınıfının tasarlanmasını anlatırken tekrar ele alacağım. Bahsettiğimiz CacheKey sınıfının kaynak kodu ise aşağıdaki gibi olacak;

namespace DataCache.Core
{
    public class CacheKey
    {
        private readonly string _format = "{0}-{1}";
        private readonly string _generatedKey;

        public CacheKey(EntitiesEnum entity, int entityId)
        {
            _generatedKey = string.Format(_format, entity.ToString(), entityId.ToString());
        }

        public static CacheKey New(EntitiesEnum entity, int entityId)
        {
            return new CacheKey(entity, entityId);
        }

        public override string ToString()
        {
            return _generatedKey;
        }
    }

    public enum EntitiesEnum
    {
        Categories,
        Products
    }
}

Örneğin CacheKey.New(EntitiesEnum.Products, 12) gibi bir kod yazarak bir CacheKey generate edilmesini istersek bu nesnenin ürettiği Key Products-12 şeklinde olacaktır. 

Bu katman içerisindeki diğer bir sınıf olan CacheProvider sınıfını ele alalım. CacheProvider sınıfını, Cache katmanında ki sınıf tarafından inheritance edilecek bir abstract sınıf olarak tasarlayacağız. Kaynak kodları aşağıdaki gibi olacaktır;

namespace DataCache.Core
{
    public abstract class CacheProvider
    {
        public static int CacheDuration = 60;
        public static CacheProvider Instance { get; set; }

        public abstract object Get(CacheKey key);
        public abstract void Set(CacheKey key, object value);
        public abstract bool IsExist(CacheKey key);
        public abstract void Remove(CacheKey key);
    }
}

CacheDuration her bir Cache edilecek objenin saniye olarak expiration zamanını tutarken, Instance adlı static referans ise Cache katmanında bu sınıfı inheritance eden sınıfın bir instance'ını taşıyacak. Böyle bir zahmete neden girildi derseniz ? Tüm katmanları Cache katmanından izole etmek için diyebilirim.

Cache Katmanı ile Diğer Katmanların İzole Edilmesi

Bu projede business katmanının, Cache katmanında ki sınıfa bağımlı olmaması gerekir. Diğer bir değiş ile presentation hariç diğer hiç bir katman Cache katmanını referans almak yani onu tanımak zorunda değildir. Tek bilmesi gereken bu projede bir Cache katmanının olacağı ve bu katmanın bir base sınıftan inherit edildiğidir.
Peki diğer bu katmanlar, cache katmanını tanımazken ona nasıl ulaşabilir derseniz; işte Instance adlı referansı tam da bunun için yazdık.

DataCache.Cache

Cache işlemlerini yürüteceğimiz ve diğer tüm katmanladan izole olacak olan bu katmanda sadece 1 adet sınıf tasarlayacağız.
Adına DefaultCacheProvider verdiğim bu sınıf, az önce Core katmanında tasarladığımız CacheProvider adlı sınıftan inherit edilecek ve tüm abstract methodları override edilecek. Bu sınıfın kaynak kodları da aşağıdaki gibidir;

namespace DataCache.Cache
{
    public class DefaultCacheProvider : CacheProvider
    {
        private ObjectCache _cache = null;
        private CacheItemPolicy _policy = null;

        public DefaultCacheProvider()
        {
            Trace.WriteLine("Cache Initialize Oldu!");

            _cache = MemoryCache.Default;
            _policy = new CacheItemPolicy
            {
                AbsoluteExpiration = DateTime.Now.AddSeconds(CacheDuration),
                RemovedCallback = new CacheEntryRemovedCallback(CacheRemovedCallback)
            };
        }

        private static void CacheRemovedCallback(CacheEntryRemovedArguments arguments)
        {
            Trace.WriteLine("----------Cache Expire Oldu----------");
            Trace.WriteLine("Key : " + arguments.CacheItem.Key);
            Trace.WriteLine("Value : " + arguments.CacheItem.Value.ToString());
            Trace.WriteLine("RemovedReason : " + arguments.RemovedReason);
            Trace.WriteLine("-------------------------------------");
        }

        public override object Get(CacheKey key)
        {
            object retVal = null;

            try
            {
                retVal = _cache.Get(key.ToString());
            }
            catch (Exception e)
            {
                Trace.WriteLine("Hata : CacheProvider.Get()\n"+e.Message);
                throw new Exception("Cache Get sırasında bir hata oluştu!", e);
            }

            return retVal;
        }

        public override void Set(CacheKey key, object value)
        {
            try
            {
                Trace.WriteLine("Cache Setleniyor. Key : " + key.ToString());
                _cache.Set(key.ToString(), value, _policy);
            }
            catch (Exception e)
            {
                Trace.WriteLine("Hata : CacheProvider.Set()\n" + e.Message);
                throw new Exception("Cache Set sırasında bir hata oluştu!", e);
            }
        }

        public override bool IsExist(CacheKey key)
        {
            return _cache.Any(q => q.Key == key.ToString());
        }

        public override void Remove(CacheKey key)
        {
            _cache.Remove(key.ToString());
        }
    }
}

Sanırım bu örnekteki en uzun sınıf bu olacak :)
Kısaca yukarıda tanımladığım 2 adet field'a da değinelim. ObjectCache aslında sınıfın hatta tüm projenin bel kemği diyebiliriz. Cache'e alacağımız tüm objeler bu nesne içerisinde tutulacak.
CacheItemPolicy ise ObjectCache'e bir nesne ekleneceği zaman ihtiyaç duyulan bir tip. Bu tip Cache'e eklenecek olan her bir CacheItem hakkında expiration gibi baz bilgileri barındırmakta. Projemizde her nesnenin ortak Caching özelliklerine sahip olmasını istediğimiz için her Set işleminde aynı Policy nesnesini kullanıyor olacağız.

CacheItemPolicy tipinin bize sunduğu belkide en güzel özellik, herhangi bir Cache Item'ın silinmesi halinde tetiklenen bir delegate'e sahip olması. CacheItemPolicy içerisinde bulunan RemovedCallback adlı delegate ile sınıfın içerisindeki CacheRemovedCallback adlı methodumuzu işaret ediyoruz. Bu sayede beklenen ya da beklenmeyen herhangi bir silinme işleminde silinen CacheItem'ın Key-Value gibi baz bilgilerinin yanında, silinme nedenine de ulaşabiliyoruz.

Get methodunda parametre olarak gelen CacheKey nesnesinin generate ettiği Key ile eşleşen nesneyi ObjectCache içerisinden alarak return ediyoruz. Key ile eşleşen bir nesne olmaması halinde geriye null dönecektir. Diğer unexpected durumların catch edilmesi için de yazdığımız try-catch bloklarında hatayı Trace ediyoruz. (Debug anında bir gözümüz Output Window'da olmalı smiley)

Set methodu da Get gibi CacheKey ve yanında bir de Cache'e eklenecek nesnenin kendisini alarak, gerekli Set aksiyonunu ObjectCache tipi üzerinden gerçekleştiriyor.

IsExist methodumuz ise IEnumerable interface'inin ObjectCache tarafından implemente edilmesinin güzel yanlarından birini kullanıyor. Any methodu ile parametre olarak gelen Key'e ait bir nesnenin Cache içerisinde olup olmadığını kontrol ediyor ve sonucu geriye döndürüyor.

Remove methodu ise parametre olarak gelen key'e ait bir CacheItem varsa silinme işlemini gerçekleştiriyor. 

DataCache.Data.Model

Uygulama için baştan bir database ve database ile ilişki kuracak bir dizi sınıf yazmaktansa, hali hazırda mevcut Northwind tablolarını ve Entity Framework kütüphanelerini kullanıyor olacağız. Buraya tıklayarak Northwind database'inin create script'lerini ve backup dosyalarını indirebilirsiniz. 

Uygulama örneğini kendiniz yazıyorsanız; Northwind'i restore edip, bu katman içinde Entity Framework Context'ini oluşturmalısınız. Bu işlem için database first methodunu kullanmanız çok daha kolay olacaktır. smiley

DataCache.Business

Bu katman içerisinde örnek için bir adet sınıf yazıyor olacağız. Yazacağımız CategoryManager sınıfı aşağıdaki gibi olacaktır;

namespace DataCache.Business
{
    public class CategoryManager
    {
        public Category GetById(int categoryId)
        {
            Category retVal = null;

            CacheKey key = CacheKey.New(EntitiesEnum.Categories, categoryId);
            if (CacheProvider.Instance.IsExist(key))
            {
                retVal = CacheProvider.Instance.Get(key) as Category;
            }
            else
            {
                using (NorthwindEntities context = new NorthwindEntities())
                {
                    retVal = context.Categories.SingleOrDefault(q => q.CategoryID == categoryId);
                }

                CacheProvider.Instance.Set(key, retVal);
            }

            return retVal;
        }
    }
}

Sınıfımızın içerisindeki GetById methodu, aldığı Id parametresi ile geriye istenen Category nesnesini döndürecektir.

Method içerisinde öncelikle nesnenin daha önce Cache'e eklenip eklenmediğini kontrol ediyoruz. Bunun için öncelikle bir adet CacheKey nesnesi yani istenen nesneye özel hazırlanmış bir Cache Key generate ediyoruz. Proje içerisinde kullandığımız CacheKey kalıbına göre Id değeri 5 olan bir Category için oluşturulacak key Categories-5 şeklinde olacaktır.

Oluşturduğumuz CacheKey ile daha önce bir CacheItem'ın eklenip eklenmediğini kontrol ediyor ve akışa ona göre devam ediyoruz. Cache'e eklenmiş olması durumunda geriye direk Cache'den aldığımız nesneyi geriye döndürüyoruz. Ancak daha önce bu nesne Cache'e alınmadıysa nesneyi Entity Framework yardımıyla database'den çekiyor hem return ediyor hem de Cache'e ekliyoruz.

DataCache.Web

Uygulamamıza son ekleyeceğimiz bu katman için bir Asp.Net MVC projesi yaratıyoruz. Proje içerisine CategoryController adından bir adet Controller sınıfı ekliyor ve uygulamanın Application_Start methoduna aşağıdaki gibi tasarlıyoruz;

protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);

            Core.CacheProvider.Instance = new Cache.DefaultCacheProvider();
        }

 

namespace DataCache.Web.Controllers
{
    public class CategoryController : Controller
    {
        CategoryManager categoryManager = new CategoryManager();

        public ActionResult Detail(int id)
        {
            var category = categoryManager.GetById(id);

            return View(category);
        }
    }
}

Detail action'ı için bir adet View ekliyoruz. Bu view'in kaynak kodları ise aşağıdaki gibi olacaktır;

@model DataCache.Data.Model.Category
@{
    ViewBag.Title = "Kategori Detay";
}

<h2>@Model.CategoryID - @Model.CategoryName</h2>
<p>@Model.Description</p>

Mvc projemiz IIS Pool tarafından tetiklendiği anda Core katmanı içerisindeki CacheProvider base sınıfının Instance adlı property'sine yeni bir DefaultCacheProvider instance'ı atanacak ve tüm Caching işlemleri bu referans üzerinden gerçekleşecektir.
Controller içerisinde ki Action üzerine bir breakpoint koyarak akışı test edebilir, sonraki request'lerde nesnenin direk cache'den geldiğini gözlemleyebilirsiniz.

Projenin kaynak kodlarına GitHub üzerinden ulaşabilirsiniz.

Bir sonraki makalemde görüşmek üzere. Umarım arayı bir daha bu kadar uzatmam :)
H.Burak TUNGUT

Elinize sağlık Burak Bey güzel bir yazı çok işime yaradı.
Etkileyici anlatım.. Sade ve anlaşılır.. Teşekkür ederim Burak bey.
Burak bey, makalenizi sıkmadan ve akıcı anlatımınızla ayrı bir güzellik katmışsınız. Bu yazıdan sonra keşke daha önce bloğunuzu farketseymişim dedim :) Teşekkürler.
Çok aydınlatıcı oldu. Teşekkürler
Yorum Bırak

Facebook
Son Yorumlar