.Net Core MVC Web Uygulamalarında Redis Kullanımı

2 ay kadar önce tamamladığım halde yayına almayı unuttuğum Redis konulu yazımı, giriş metnini hiç bozmadan sizlerle paylaşıyorum. Keyifli okumalar 🙂

Merhaba okumayı / araştırmayı seven yazılımcı arkadaşlarım. Büyümekte olan bir start-up’ın taze kan sayılabilecek Back-End Developer’ı olmanın vermiş olduğu motivasyon ile sizlere bu satırları yazmaktayım. Start-up’ları bilenler bilir.. Proje mümkün olan en hızlı şekilde görücüye çıkar. 3’üne 5’ine bakılmaz, mevla ne verdiyse yazılır. Eğer proje tutarsa vites daha da yükseltilir, hatta nitro’ya yüklenilir. Yeni özellikler eklendikçe eklenir.. Veri tabanları da bu düzene ayak uydurur. Data boyutları giderek artar ve, ilk zamanlarda canavar gibi çalışan veri tabanı sunucuları artık isteklere yanıt verememeye başlar. Hele ki, ana uygulamanızın yanı sıra çalışan başka servis uygulamalarınızda var ise cehenneme hoş geldiniz demektir..

Şuan okumakta olduğunuz yazının hikayesi tam olarak buradan başlıyor.

Uzatmıyorum ve konuyu hemen Redis‘e getiriyorum. Kısaca bahsetmek gerekirse; Redis, NoSQL bir veri tabanıdır. Verileri bellekten okumaktadır, ve isteklere çok hızlı yanıt verebilmektedir. (Redis‘in ayrıntıları yazının konusu olmadığı için merak eden arkadaşlar şuradan devam edebilir.)

Bu yazı da işleyeceğimiz senaryo şu şekilde olacak;

  • Ürünlerin kullanıcılara gösterildiği bir e-ticaret (Asp.Net Core MVC) web uygulamamız mevcut,

  • Depo ekibi; giriş-çıkış yapılan ürünlerin yönetimini bir (Asp.Net Core) console uygulaması üzerinden yapmaktadır.

Projenin gelişticisi olarak bizlerden beklenen ise; web uygulamasında görüntülenen ürünlerin Redis kullanılarak önbellekleme yapılması ve böylece web uygulamasının performansının artırılmasıdır. Redis üzerinde önbellekleme yapılan ürün bilgilerinin güncellenme işlemi ise depo ekibi tarafından; yani console uygulaması üzerinden gerçekleşecektir.

Projenin Redis‘ten önceki halini buradan indirebilirsiniz.

Başlıyoruz… 😉

Redis ile ilgili işlemleri ayrı bir katman üzerinden yürüteceğiz. İlk olarak Cache katmanını oluşturalım ve solution‘a dahil edelim.

dotnet new classlib -n Cache
dotnet sln add Cache/Cache.csproj

Projemiz ile Redis arasındaki iletişimi sağlamak için StackExchange.Redis kütüphanesini Cache katmanına dahil edelim.

dotnet add Cache/Cache.csproj package Microsoft.Extensions.Caching.StackExchangeRedis --version 3.1.0

Redis üzerinde sakladığımız verileri StackExchange ile almak istediğimizde; bu veriler bize string yada byte[] olarak dönecektir. Redis‘ten gelen string yada byte[] tipindeki verileri; bu iki veri tipi dışındaki başka normlarda (örneğin Product nesnesi olarak) kullanabilmek için deserialize / serialize işlemleri gerçekleştirmemiz gerekmektedir.

Yaygın olarak kullanılan yöntem; gelen verinin Json.NET – Newtonsoft paketi kullanılarak deserialize / serialize edilmesi şeklindedir. Biz de aynı yöntemi kullanacağız.

StackExchange‘i Cache katmanına ekledikten sonra; işlemleri gerçekleştirecek olan RedisCacheManager sınıfını yazıyoruz.

    public class RedisCacheManager
    {
        private RedisCacheOptions options;
        public RedisCacheManager()
        {
            options = new RedisCacheOptions
            {
                Configuration = "127.0.0.1:6379",
                InstanceName = ""
            };
        }
        public T Get<T>(string key)
        {
            using (var redisCache = new RedisCache(options))
            {
                var valueString = redisCache.GetString(key);
                if (!string.IsNullOrEmpty(valueString))
                {
                    var valueObject = JsonConvert.DeserializeObject<T>(valueString);
                    return (T)valueObject;
                }

                return default(T);
            }
        }

        public void Set(string key, object valueObject, int expiration)
        {
            var cacheOptions = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(expiration)
            };

            using (var redisCache = new RedisCache(options))
            {
                var valueString = JsonConvert.SerializeObject(valueObject);
                redisCache.SetString(key, valueString, cacheOptions);
            }
        }

        public void Remove(string key)
        {
            using (var redisCache = new RedisCache(options))
            {
                redisCache.Remove(key);
            }
        }

        public void RemoveByPattern(string pattern)
        {
            using (var redisConnection = ConnectionMultiplexer.Connect(options.Configuration))
            {
                var redisServer = redisConnection.GetServer(redisConnection.GetEndPoints().First());
                var redisDatabase = redisConnection.GetDatabase();

                foreach (var key in redisServer.Keys(pattern: pattern))
                {
                    redisDatabase.KeyDelete(key);
                }
            }
        }
    }

RedisCacheOptions sınıfı ile, bağlanacağımız Redis sunucusunun ayarlarını belirtiyoruz. Benim deneme yaptığım pc’m için host adresim; varsayılan adres olan 127.0.0.1, port ise yine varsayılan olarak 6379‘dur. Dolayısıyla Configuration değerini “127.0.0.1:6379” olarak belirttim.

Yine RedisCacheOptions içerisinde bulunan InstanceName alanını ise boş bıraktım. Eğer InstanceName‘e herhangi bir değer verirsek; verdiğimiz bu değer, veri kaydederken kullandığımız key’lerin önüne otomatik olarak eklenecektir. Yazının ilerleyen kısmında InstanceName değerini örneklendireceğim.

RedisCacheManager sınıfında; Get(), Set(), Remove() ve RemoveByPattern() olmak üzere 4 metot bulunmaktadır. Metotların adlarından da anlaşılacağı gibi; data okuma ( Get() ), data düzenleme ( Set() ) ve data silme işlemleri ( Remove() ve RemoveByPattern() ) işlemleri RedisCacheManager sınıfı ile gerçekleştirilecektir.

RedisCacheManager sınıfını Console ve Web uygulamamızda kullanabilmek için Cache katmanını bu iki uygulamaya dahil ediyoruz.

dotnet add WebApplication/WebApplication.csproj reference Cache/Cache.csproj 
dotnet add ConsoleApplication/ConsoleApplication.csproj reference Cache/Cache.csproj

Cache katmanını Console ve Web uygulamalarına ekledikten sonra; web uygulamasında ürünlerin kullanıcıya gösterildiği HomeController.cs/Products metodunu yeniden düzenliyoruz.

        [Route("products")]
        public IActionResult Products(int page = 1, int size = 3)
        {
            if (page < 1) page = 1;
            if (size < 3) size = 3;
            if (size > 10) size = 10;

            var productQuery = dbContext.Products
                .Where(x => x.InSales);

            var totalProductCount = productQuery.Count();

            var redisManager = new RedisCacheManager();
            var pagedProductListKey = $"products|page={page}|size={size}";

            var products = redisManager.Get<List<Product>>(pagedProductListKey);

            if (products == null)
            {
                products = productQuery
                    .Skip((page - 1) * size)
                    .Take(size)
                    .ToList();

                redisManager.Set(pagedProductListKey, products, 60);
            }

            var pagedProductList = new PagedProductList
            {
                Products = products,
                TotalPage = (int)Math.Ceiling((double)totalProductCount / size),
                CurrentPage = page
            };

            return View(pagedProductList);
        }

Yaptığımız düzenlemeye göre bir kullanıcı Products sayfasını görüntülemek istediğinde;

  • İlk olarak, sayfalama parametrelerinin de (page ve size) yer aldığı bir key oluşturuluyor. (Diyelim ki ürünler bölümün 2. sayfasında 5 tane ürün gösterilecek. Bu durumda key değerimiz “products|page=2|size=5” şeklinde olacaktır.)

  • RedisManager aracılığıyla bu oluşturulan key’e ait veri Redis sunucusundan çekiliyor.

  • Redis sunucusunda; oluşturulan key‘e ait veri bulunmuyorsa (ki bu durumda null dönecektir.) sayfada gösterilecek ürünler veri tabanından çekiliyor ve Redis sunucusuna aynı key ile (60 dakika sonra Redis’ten silinecek şekilde) kaydediliyor.

Böylece, aynı sayfa herhangi bir kullanıcı tarafından tekrar çağrıldığında; sayfada gösterilecek ürün verileri Redis sunucusundan getirilecektir. Bu durum da, aynı veriler için tekrar tekrar veri tabanına istek atılmasının önüne geçmiş oluyoruz.

Gördüğünüz üzere; tek bir key içerisinde tüm ürünlerin bilgisini saklamak yerine, sayfalama parametrelerini de key‘e dahil ederek sadece sayfada gösterilecek ürünlerin bilgisini bu key‘e kaydettik. Ve bunu yapmak için haklı sebeplerimiz vardı 🙂

Redis sunucunuzdaki verileri büyük parçalar halinde tutmaya kalkarsanız; (yani bir key’de yüksek boyutlu veriler saklarsanız) Redis sunucunuzdan verilerin uygulamanıza aktarılma süresi uzayacaktır. Ayrıca Redis sunucusundan byte olarak aktarılan verileri objelere dönüştürebilmek için serialize / deserialize işlemleri yapılması gerekmektedir, ve bu işlemlerin gerçekleşme süresi; veri boyutu büyüklüğü ile paralel olarak artış gösterecektir.

Özetle, Redis mekanizmasından istediğiniz performansı almak istiyorsanız; verileri küçük parçalar halinde saklamaya özen göstermelisiniz.

Serialize / deserialize işlemlerini daha hızlı bir şekilde gerçekleştirmek için Json.Net & Newtonsoft yerine ProtoBuf kullanabilirsiniz. ProtoBuf ile ilgili olarak; Bora Kaşmer’in ProtoBuf Nedir ? yazısını tavsiye ederim.

Web uygulamasında gerekli düzenlemeyi yaptıktan sonra Console uygulamasında da bir düzenleme yapmamız gerekiyor.

        static void UpdateStockQuantity(string barcode, int quatity)
        {
            using (var context = new ApplicationDbContext(GetDbContextOptions()))
            {
                var product = context.Products.SingleOrDefault(x => x.Barcode == barcode);

                if (product == null)
                {
                    Console.WriteLine("\n!!! Ürün bulunamadı.\n");
                    return;
                }

                product.Quantity += quatity;
                product.ModifiedDate = DateTime.Now;

                if (context.SaveChanges() > 0)
                {
                    var redisManager = new RedisCacheManager();
                    redisManager.RemoveByPattern("products*");

                    Console.WriteLine($"\n!!! Stok başarıyla güncellendi: {product.Name}\n");
                }
            }
        }

ConsoleApplication‘da Program.cs içerisindeki UpdateStockQuantity() metodunu yukarıdaki gibi güncelliyoruz. Burada ise; eski kodlara ek olarak, herhangi bir güncelleme işlemi yapıldığında; Redis sunucusundaki products ile başlayan tüm key’lerin RedisManager‘daki RemoveByPattern() metodu ile silinmesini sağladık.

Ürünlerde herhangi bir güncelleme yapıldığında products ile başlayan tüm key‘ler (ve içerisindeki veriler) Redis‘ten silindiği için Redis’ten ürün bilgisi gelmeyecektir. Dolayısıyla, bilgiler veri tabanından tekrar çekilip Redis‘e tekrar kaydedilecektir.

Böylece kullanıcılar; ürün bilgilerine en güncel halleriyle erişebilecekler.

Web uygulamamızı ayağa kaldıralım ve ürün bilgileri Redis‘e nasıl kaydediliyor görelim

Web uygulaması üzerinden ürünler bölümdeki 1. , 3. ve 4. sayfaları dolaştım

Redis Desktop Manager programından da gördüğümüz üzere; sayfalama sırasında görüntülenen ürün bilgileri Redis‘e, sayfa numarası (page) ve görüntülenen ürün sayısı (size) değerleriyle birlikte kaydedilmiştir. (“products|page=3|size=3”)

Eğer, RedisCacheOptions sınıfını oluştururken, herhangi bir InstanceName değeri belirtseydik; oluşturduğumuz key değerleri “InstanceName değeriproducts|page=3|size=3“ gibi olacaktı. Yani belirttiğimiz InstanceName değeri, atanan key’lerin önüne otomatik olarak eklenecekti.

Son olarak, Console uygulaması üzerinden bir ürün güncellemesi yapalım ve Redis‘teki verilerin durumunu gözleyelim

Console uygulamasından 901234567 barkodlu ürünün stok adedini 2 olarak güncelledim. Ve sonuç

Herhangi bir güncelleme işleminde; Redis‘e kaydolan tüm ürün bilgileri temizlendi. Web uygulamasından ürünler görüntülendiğinde sayfalanmış güncel ürün bilgileri, Json formatında Redis‘e tekrar kaydedilecektir.

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

Mutlu kodlamalar. 🙂

Last updated