Bir İçeriğin Görüntülenme Sayısını Redis, Signalr ve Bulk DB İşlemleri ile Bulma ve Anlık Raporlama

Selamlar,

Geçenlerde code review yaparken, her kayıt için DB’ye gidildiğini ve büyük bir performans kaybı yaşandığını gördüm. Ben de bu konuya eğlenceli ve farklı teknolojileri bir araya getirerek makaleleştirme kararı aldım. Ayrıca yeni neslin Sql üzerinde bussines çözümlerinden mümkün olduğunca kaçınıp herşeyi code ile çözmeye çalışması beni üzdü. Bunun üzerine bu makalede, peformans arttırma amaçlı Sql procedurelerden nasıl faydalanabileceğimize dair gerçekçi bir örnek yazmak istedim.

Seneryo: Bu makalede farklı teknolojiler kullanılarak, belirli bir portalda ilgili içeriğin impression yani, kullanıcı tarafından görüntülenme sayısı bulunacaktır. Kısaca amaç, son 1 dakika içerisinde okunan ya da izlenen içeriklerden, en yüksek impressiondan en azına doğru anlık bir raporun oluşturulmasıdır. Böylece ilgili içeriklerin önem sırasına göre portaldaki yeri değiştirilebilecek ve bunun sonucu olarak kullanıcı deneyimi daha da arttırılacaktır.

Kullanılacak Teknolojiler:

  • Redis ilgili son 1 dakkalık içerik sayılacaktır. Yani ilgili içerik yok ise redis’e atılıp ilk değeri 1 verilecek, ya da var ise ilgili içerik bulunup değeri 1 arttırılacaktır. Yoğun trafikte en çok yük bu yapının üstünde olacağından Redis tercih edilmiştir.
  • Dapper ile son 1 dakika tamamlandığında, ilgili data redisten çekilip bir DB’ye yazılacaktır. Yine performans amaçlı Dapper tercih edilmiştir.
  • SignalR ile ilgili son 1 dakikalık data, monitor işlemi yapan admin ekranlarına real time olarak push edilecektir.
  • AngularJS signalR ile gönderilen data front tarafda angularJs bir modele doldurulup, değişen datanın ayrıca bir kod yazılmadan ekrana basılması sağalanacaktır.

Şimdi gelin öncelikle Modellerimizi oluşturalım:

CounterItem: Herhangi bir içeriğe tıklandığında DB’ye yazılacak ilgili data aşağıdaki gibidir.

CounterViewItem: Front tarafda ilgili içeriğe ait gösterilecek data aşağıdaki gibidir. Yani bir çeşit view modeldir.

  • ContentID: İlgili içeriğin ID’sidir.
  • ViewCount: 1 dakikalık görüntülenme sayısıdır.
  • PlatformID: Görüntülendiği ortamdır. Web mi yoksa Mobile mi gibi.
  • PageNumber : Özellikle PhotoGallery’de görüntülenen resmi temsil etmektedir.
  • ContentTypeID: İçerik tipidir. Makale mi, Resim mi yoksa Video mu.
  • TotalViewCount: En başından beri sayılan toplam görüntülenme sayısıdır.
  • Title: İlgili içeriğe ait başlık.
  • Url: İlgili içeriğe ulaşılacak adresdir. Kısaca title’a tıklanıldığında gidilecek yoldur.
  • ViewsDate: En son alınan 1 dakikalık zamanı gösterir.

Enum ContentType:

Enum PlatformType:

Öncelikle gelin içerik sayfasına konucak olan script’i belirleyelim: Burada amaç portalda girilen içeriğin görülme değerini +1 arttırmaktır. Kısaca “Counter”, farklı bir Mvc projesindeki actiondır. Ve tek amacı kendisine gelen request’e göre ilgili içeriği saymaktır.

  • platformID= platformID: İçeriğin bakıldığı mecra belirlenmiştir. Örneğin bu sayfada “Web” seçilmiştir.
  • id= Gelen içeriğin “ID”‘si alınmıştır.
  • ctID= contentTypeID  Daha sonra içerik tipi seçilmiştir. [Article,PhotoGallery,VideoGallery]
  • pg= Son olarak özellikle PhotoGallery için ihtiyaç olacak PageID belirlenmiştir. PhotoGallery’de page’in amacı, gallerydeki herbir resimin toplam görümtülenme sayısının belirlenmesidir. Böylece galery içinde en çok beyenilen resim ya da gallery de hangi resime kadar tıklanılıp kapatıldığı belirlenebilecektir.

Şimdi ilgili içeriği sayacak yeni bir proje oluşturalım.

redislist

Adı da “Statistics” olsun: Yukarıda tanımlanan, haber detay sayfalarına konan script’in, request çektiği  asenkron “Counter” action’ı aşağıda tanımlanmıştır.

  1. “int id, int platformID, int ctID, int? pg” ilgili parametreler  içeride tanımlı değişkenlere atanır.
  2. Önceden yaratılan Redis bir sunucuya bağlanılır. İlgili connection dış bir sistemden alınır(Web.config veta DB gibi) “using (IRedisClient client = new RedisClient(new Uri(AppConfig.ListeningRedisConnectionString)))
  3. “StatisticKey()” methodu ile ilgili content’e göre tekil bir key üretilir.
  4. Oluşturulan bu static key ile redis’e gidilip ServiceStack’in “IncrementValue()” methodu çağrılır ve içeriği yok ise 1 atanır, var ise +1 arttırılır. Ben bu projede redis için ServiceStack kütüpahanesinden faydalandım.
  5. Last1MinutePeriod” property’si ile(hemen aşağıda anlatılmıştır.) belirlenen 1 dakikalık sürenin dolup dolmadığına bakılır.
  6. Eğer bir dakikalık süre dolmuş ise (var threeminsub = DateTime.Now.Subtract(Last1MinutePeriod)) Redis’den sayılan bu content’e ait tüm keyler çekilir. Burada önemli nokta, statick bir key oluşturulurken, bu sayım için tüm keylerin “Statistic:” anahtar kelimesi ile başlamasıdır. Buna göre yarıtılan tüm keyler “client.SearchKeys(“Statistic:*”)” methodu ile çeklir. Daha sonra ilgili tüm keyler gezilerek pars edilir ve content için gerekli alanlar [“platformID,contentID,contentTypeID”]  ilgili değişkenlere atanır.  Son olarak value yani toplam sayı(count) değerleri de alınarak “CounterItem“(aşağıda anlatılmıştır) modeli doldurulur ve List of CounterItem nesnesine eklenir. Kısaca son 1 dakika içinde sayılan tüm içerik bir liste altında toplanır.

StatisticKey: Gelen içeriğin ID’sine, tipine ve geldiği platforma bakılarak, redis’de tanımlanacak unique yani tekil bir key üretilir.

Last1MinutePeriod: Burada amaç son 1 dakikadaki sayılan içeriğin, admin ekranlarına signalR ile push  edilmeden önceki 60sn’lik sürenin sayılıp, Global Application’a atılmasıdır. Bunu için ilk sayımın başladığı anın, tüm clientlar adına aynı olması ve Global olarak bir yerde saklanması adına Application’da saklanır. İstendiğinde ilgili zamanın kontrol edilip, var olan değer geri dönülür. Yani kısacası son 1 dakikalık süre içinde anlık olarak redisden bilgi çekilebilir. Sürenin dolması durumunda da, yeni zaman application’a tekrardan atılarak yenilenir.

CounterItem:

Şimdi sıra geldi Database İşlemlerine: Öncelik ilgili kayıtların tutulacağı 2 tablo oluşturulacaktır. Bunlardan biri Son 1 dakikanın kayıtlarını tutan CounterItem. Diğeri de o zamana kadar ilgili içeriğin toplam görüntülenme sayısını tutan CounterItemTotal tablolarıdır.

CounterItem Table: Son 1 dakika ile alakalı kaydın tutulduğu tablo.

CounterItemTotal Table: Baştan sona içeriklerin toplam görüntülenme sayılarının tutulduğu tablo.

Şimdi sıra geldi Database işlemlerine: Eğer beklenen 1 dakikalık süre dolar ise önce son 1 dakikalık data “CounterItem“‘a daha sonra da toplam görüntülenme sayıları “CounterItemTotal” tablosuna kaydedilir.

İlgili static dapper servisi using ile kullanılır. Redisden gelen son 1 dakikalık tüm data “dataList” listesine doldurulur. İlgili liste önce “InsertCounterItem” procedure ile “CounterItem” tablosuna Insert edilmiştir. Daha sonra “InsertCounterTotal” procedure ile “CounterItemTotal” tablosunda olmayan kayıtlar insert, olan kayıtlarda update edilmiştir.

Datebase’de Bulk İşlemler: Şimdi sıra geldi bu makaleyi yazmamdaki esas amaca :). Database işlemlerinde Insert veya Update yapılacak tüm kayıdın tek tek gönderilmesi demek, büyük bir performans kaybı ve işlem maliyeti demektir. 1 milyon haber son bir dakika içinde okunmuş olsa, herbir makalenin tek tek varsa update, yoksa insert amacı ile db’ye gönderilmesi, 1 milyon connection’ın açılıp her bir datanın işlem görmesi demektir. Bunun için öncelikle ilgili işlem yapılacak data kümesi “dataList“, yukarıda yapıldığı gibi  bir liste içerisinde toplanır. İlerde yazacağımız procedure, bir table parametresi beklemektedir. Bu da bizim data kümemize karşılık gelmektedir.

InsertCounter ve InsertCounterTotal Procedure: Öncelikle bir “DataTable” oluşturulur. İlgili liste gezilerek, yeni oluşturulan datatable doldurulur. Ben database işlemlerinde Dapper kullandım. Dapper ile ilgili kütüpahanelere bu makalede deyinmeyeceğim. Sadece ilgili dapper nesnesi için kendi oluşturduğum “_redisConnectionFactory.GetConnection” sınıfıı kullanacağım.

“ExecNonQueryStoredProc” dapper ile ilgili procedure’ü çağran komuttur.

  • İlgili procedureler “InsertCounterItem ve InsertCounterTotal”‘dir.
  • Parametre olarak ilgili DataTable “dt” “new { Dt = dt.AsTableValuedParameter(“dbo.UDT_CounterItem”) }” şeklinde gönderilmiştir.
  • MsSql’de bir procedure’ün parametre olarak data table alabilmesi için, önceden bir “User-Defind Table“‘ın yaratılması gerekmektedir. Bu bir çeşit Web tarafındaki ViewModel’e karşılık gelmektedir. Burada “dbo.UDT_CounterItem” adında bir table yaratılmıştır. İlgili tablo aşağıdaki gibi tanımlanmıştır.
  • Önemli bir nokta Sql tarafında yaratılan tablonun kolonları ile Sql’e parametre olarak  gönderilen DataTable’ın kolonlarının tip ve ad olarak birebir örtüşmesi gerekmektedir.

InsertCounter ve InsertCounterTotal:

dbo.UDT_CounterItem :

Şimdi sıra geldi ilgili procedurelerin yazılmasına:

[dbo].InsertCounterItem: Parametre olarak bir tablo beklemektedir. Bu tablo tipi Sql tarafından önceden “[dbo].[UDT_CounterItem]” adı ile tanımlanmıştır. Parametre olarak gelen bu data table toplu olarak “CounterItem” tablosuna insert edilmektedir. Ayrıca işlemler “Transaction” açılarak yapılmıştır. Böylece bir hata durumunda, tüm yapılan işlemler “ROLLBACK” ile geri alınabilmektedir. Bu işlem sonunda son 1 dakikalık tüm kayıt, redis memory cache’den MsSql bir DB’ye aktarılmış olunur.

[dbo].InsertCounterTotal: Bu procedure’de, en başından beri ilgili içeriğe ait toplam okunma sayısının tutulduğu “CounterItemTotal” tablosuna, insert veya update işlemleri yapılmaktadır. Esasında işlem yapılan 2 tablo bulunmaktadır. İlki bir çeşit temp olan son 1 dakikayı tutan bir tablo. Bize bu procedure’de parametre olarak gelmektedir. İkincisi yukarıda bahsedilem content’in geçmişini tutulduğu, toplam görüntülenme sayısının kaydedildiği tablodur. Aşağıdaki procedurede iki tablo arasında “MERGE” işlemi yapılarak yani iki tablo birbiri ile kıyaslanarak var olan ve var olmayan kayıtlar belirlenmektedir. Var olan kayıtlar “MATCHED” tanımlaması ile güncellenmekte ve var olmayan kayıtlar “NOT MATCHED” tanımlaması ile Insert edilmektedir. Böylece toplu olarak gönderilen kayıtlar en performanslı şekilde Insert veya Update olmaktadır. Burada “SOURCE”olarak adlandırılan gelen parametrik @Dt tablosudur. “TARGET” ise işlem yapılacak CounterItemTotal tablosudur. Tüm işlemler Transaction altında yapılmakta ve herhangi bir hata anında “ROLLBACK” ile istendiğinde kolaylıkla geri alınabilmektedir.

Bu ana kadar ilgili içeriğin içine konan script ile nasıl bir Mvc projesine request atıldığını, böylece ilgili içeriğin impression yani görüntülenme sayısının redis kullanılarak nasıl arttırıldığını, herkes için belirlenen global süre dolunca nasıl database’de bulk yani toplu kayıt işlemleri yapıldığını, “Sql Match” ile var olan içeriğin güncellenip olmayan içeriğin nasıl Insert edildiğini gördük.

Bu makalenin devamında ilgili içeriği AngularJs ile bootstrap kullanılmış bir admin ekranda göstereceğiz. Ayrıca ilgili içeriği her 1 dakikada bir signalR kullanarak push edeceğiz. Son olarak angularJs ile custom directive yazarak timer nesnesi oluşturup istenirse anlık sayım bilgisini redisten çeken ve diğer tüm clientlara push eden bir başka yapıyı da admin ekranına ekleyeceğiz. Bu işleri UI tarafında Bootstrap Tabs kullanarak gerçekleştireceğiz.

Bir sonraki makalede görüşmek üzere hoşçakalın.

 

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

4 Cevaplar

  1. Hakan dedi ki:

    Çok güzel bir makale hocam. Böyle değişik telkolojilerin bir arada kullanıldığı başka makalelerde yazın. Saygılar

    • borsoft dedi ki:

      Selamlar Hakan,

      Teşekkürler. Zaten bu yazının devamı da var :) Çok tutarsa 2.sini de yayımlayacağım :)

      İyi çalışmalar.

  2. haşim dedi ki:

    hocam cok teşkurler

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.