Merhabalar bu yazımızda .Net Core tarafında caching ve disturbed cachingin ne olduğuna kısaca değindikten sonra çok bilinen, kullanılan ve popüler NoSQL veri tabanlarından biri olan Redisin avantajlarından bahsedeceğiz ve sonrasında Redis ile .Net Core web api’de cache işlemlerimizi gerçekleştiriyor olacağız.
Caching
Uygulamalarımızda sık kullanılan verilere her ihtiyaç duyulduğunda veri kaynaklarına bağlanıp gerekli işlemlerin ve hesaplamaların gerçekleştirerek getirilmesinin, performans kayıplarına sebep olacağını söylemek zor olmayacaktır. Bu durumda geçici bellekte bu sık kullanılan verilerin tutularak ihtiyaç anında veri kaynağına gidilmek yerine belleğe giderek getirilmesi makinanın sabit diskine ulaşma maliyetini ortadan kaldıracaktır, bu durumda uygulamamıza performans artışı olarak yansıyacaktır.
Çoğu küçük uygulamalada veya tek makina üstünde çalışan uygulamada cache datalarını ugyulama sunucusu içerisinde barındırmak makul ve yeterli olacaktır. Ancak orta veya büyük çaplı projelerde bu yeterli bir çözüm olmaktan çıkarak, distributed caching (dağıtık cacheleme) yapısını kullanmak gerekecektir.
Distributed Caching
Birden çok sunucu üzerinde çalışan bir uygulamalarda için cacheleme işlemleri bu uygulama makinelerinin paylaştığı harici bir servis gibi yapılandırma işine kısaca distrubuted caching(dağıtık önbellekleme) diyebiliriz. Bu sayede cachelerimiz uygulama sunucularında tutulmayacağından, uygulama sunucularının performansında bir iyileşme imkanı sağladığı gibi, sistemin ölçeklenebilirliğini de artırmış oluruz. Diğer taraftan herhangi bir sebepten ötürü uygulama sunucularından biri veya birkaçı down olduğunda, cache işlemleri harici bir servis üzerinden sunulduğu için uygulamanın çalışması etkilenmeyecektir, tabiki bu noktada farklı önlemler ve senaryolarda uygulamak gerekmektedir fakat konumuz bu olmadığından temel bir tanımla burada bırakabiliriz.
Caching için temel işlemleri kısaca tanımladıktan sonra Redisten söz etmeye başlayabiliriz. Redis, isminin açılımı “REmote DIctonary Server” olan 2015 yılından bu yana Redis Labs şirketi tarafından açık kaynak olarak geliştirilen(daha önce Piwol ve VMware) bir anahtar-değer veri tabanıdır, rediste veriler sabit disk yerine önbellekte(RAM) tutularak ihtiyaç duyulduğunda çağırılabilir veya manipüle edilebilirler aynı zamanda veriler disk üzerinde de saklanarak olası durumlarda veri kayıplarının da önüne geçilebilir. Redisi alternatiflerinin arasından ayıran en önemli özelliklerinden biri ise, yüksek seviye veri yapılarını(hash, list, set, map) saklama ve bu veri yapıları üzerinde atomik yapıda işlemleri desteklemesi yeteneğidir, bu noktada Redisi sadece in-memory bir nosql aracı olarak görmek eksik olacaktır aynı zamanda queues, pub/sub, session store, counters gibi operasyonlar içinde sıklıkla tercih edilmektedir. Birçok büyük şirket tarafından (Twitter: timelinedaki twitlerin yönetiminde redis kullanıyorlar, herhangi bir kullanıcının son 800 tweetine kadar Redis üzerinde saklanıyor. Pinterest: kullanıcıların ve panoların takipçi verileri Redis üzerinde saklanıyor. Github, Craiglist, Snapchat gibi şirketlerle örnekleri uzatabiliriz.) güncel olarak veya geçmişte çeşitli problemlere çözüm olarak kullanılmıştır.
Tüm bu tanımlamalardan kısaca söz ettikten sonra biraz .Net Core üzerinde Redis kullanımına geçebiliriz.
Öncelikle Redisin 3.0.504 (Windows için) sürümünü şuradan indirerek bilgisayarımıza kuralım, kurulum esnasında ekstra bir konfigürasyon yapmayacağımız içi next, next şeklinde ilerleyebilirsiniz fakat, “Destination Folder” aşamasında “Add the Redis installation folder to the PATH environment variable.” seçeneğini cli üzerinden redis-cli’a ulaşmak için işaretlemeyi unutmayınız, diğer taraftan redisin default portu 6377 olarak kurulacaktır, bu noktada da herhangi bir değişiklik yapmamıza gerek yok daha sonra Redisi başka bir port üzerinde çalıştırmak istersek;
redis-server --port <port-number>
komutunu kullanabiliriz. Default portu değiştirmek istersekte Redisin kurulu olduğu konumdaki redis.conf içerisinden portu değiştirmek mümkündür.
Redis kurulumu başarıyla sonlandığında;
redis-server komutu ile Redisi çalıştıralım ve sonrasında redis-cli ile Redisin bize sunmuş olduğu command line interface’i açalım eğer kurulum başarılı oldu ise redis-cli üzerinden “ping” komutunu girdiğimizde Redis “PONG” olarak geri dönüş yapacaktır.
Redis kurulumunu tamamladıktan ve çalıştığını gördükten sonra web apimizi yazmaya başlayabiliriz. Öncelikle “RedisCache” adında bir solution oluşturup, ardından “RedisCache.Api” ismiyle bir web api projesi ekleyelim (örnek uygulamada kullanılacak .Net versiyonu: .Net Core 3.1).
Yapacağımız örnekte kodlayacağımız web apinin temel işlevi ürün verileri için Redis üzerinde create, read, delete gibi işlemleri gerçekleştirmek ve bu sayede Redis üzerinde temel işlevleri gerçekleştirmek ve incelemek olacak. Rediste yapılacak işlemlerin sağlamak üzere Stack Overflow geliştiricileri tarafından geliştirilen, senkron ve asenkron işlemlerin basit bir şekilde gerçekleştirilmesine izin veren Stackexchange.Redis kütüphanesini kullanıyor olacağız, web api projenize visual studio kullanıyorsanız, NuGet packages üzerinden;
veya visual studio code için .Net cli üzerinden;
dotnet add package StackExchange.Redis --version 2.2.4
komutuyla projenize ekleyebilirsiniz.
Projeye Redis işlemlerini gerçekleştirecek kütüphaneyi ekledikten sonra, ürün sınıfımızı yazacağımız “Models” klasörü oluşturalım ve aşağıdaki gibi ürünlerimizin id, isim, kategori, adet fiyatı ve stok miktarı özelliklerinin yer aldığı Product classımızı aşağıdaki gibi ekleyelim.
namespace RedisCache.Api.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public int UnitPrice { get; set; }
public int UnitsInStock { get; set; }
}
}
Ürün sınıfımızı oluşturduktan sonra, caching işlemlerini gerçekleştirmek üzere merkezi bir yapı (servis) tasarlayabiliriz. Bunun için öncelikle web api projemizin kök dizininde “Services” adında bir klasör ve bu klasör altında cache yönetim işlemlerini gerçekleştireceğimiz, “CacheService” adında bir klasör oluşturalım.
Artık Redis Server bağlantısını gerçekleştirebilmemiz için gerekli olan host ve port tanımlamalarını appsettings.json içine aşağıdaki gibi yerleştirebiliriz.
"RedisConfiguration": {
"Host": "localhost",
"Port": 6379
},
Redis için bağlantı tanımlamalarımızı gerçekleştirdiğimize göre, Redis konfigürasyonu nu CacheService klasörü altında RedisServer classımız içinde ile şöyle oluşturabiliriz;
namespace RedisCache.Api.Services.CacheService
{
public class RedisServer
{
private ConnectionMultiplexer _connectionMultiplexer;
private IDatabase _database;
private string _configurationString;
private int _currentDatabaseId = 0;
public RedisServer(IConfiguration configuration)
{
CreateRedisConfigurationString(configuration);
_connectionMultiplexer = ConnectionMultiplexer.Connect(_configurationString);
_database = _connectionMultiplexer.GetDatabase(_currentDatabaseId);
}
public IDatabase Database
{
get
{
return _database;
}
}
public void FlushDataBase()
{
_connectionMultiplexer.GetServer(_configurationString).FlushDatabase(_currentDatabaseId);
}
private void CreateRedisConfigurationString(IConfiguration configuration)
{
string host = configuration.GetSection("RedisConfiguration:Host").Value;
string port = configuration.GetSection("RedisConfiguration:Port").Value;
_configurationString = $"{host}:{port}";
}
}
}
CreateRedisConfigurationString(): bu metod ile appsettings.json içerisine yazdığımız redis server bilgilerinizi okuyoruz ve ConnectionMultiplexer ile kullanmak üzere _connectionString propertymize atıyoruz.
ConnectionMultiplexer: Redis server ile bağlantı işlemlerimizi gerçekleştirmemizi ve yönetmemizi sağlayan sınıftır.
IDatabase: bu interface ile connectionmultiplexer ile id değeri aracılığıyla, referansını aldığımız database üzerinden okuma, yazma gibi işlemleri gerçekleştireceğimiz sınıftır.
FlushDataBase(): bu metod ile ilgili veri tabanı üzerindeki tüm verileri silme işlemlerini gerçekleştiriyoruz.
Yine CacheService klasörü altında cache işlemlerinin(metodlarının) imzalarını içeren ICacheService interfaceimizi aşağıdaki gibi oluşturalım,
namespace RedisCache.Api.Services.CacheService
{
public interface ICacheService
{
void Add(string key, object data, int keyExpire);
T Get<T>(string key);
void Remove(string key);
bool IsExist(string key);
void Clear();
}
}
Cache yönetimini gerçekleştirecek metodlarımızın imzalarınıda yazdıktan sonra redis üzerindeki işlemleri gerçekleştirecek, ICacheService metodunu implente eden RedisCacheManager classımızı aşağıdaki gibi oluşturup metodlarımızı şöyle yazabiliriz.
namespace RedisCache.Api.Services.CacheService
{
public class RedisCacheManager : ICacheService
{
private readonly RedisServer _redisServer;
public RedisCacheManager(RedisServer redisServer)
{
_redisServer = redisServer;
}
public void Add(string key, object data, int keyExpire)
{
string cacheData = JsonConvert.SerializeObject(data);
_redisServer.Database.StringSet(key, cacheData);
_redisServer.Database.KeyExpire(key,TimeSpan.FromSeconds(keyExpire));
}
public void Clear()
{
_redisServer.FlushDataBase();
}
public T Get<T>(string key)
{
if (IsExist(key))
{
string cacheData = _redisServer.Database.StringGet(key);
return JsonConvert.DeserializeObject<T>(cacheData);
}
return default;
}
public bool IsExist(string key)
{
return _redisServer.Database.KeyExists(key);
}
public void Remove(string key)
{
if (IsExist(key))
{
_redisServer.Database.KeyDelete(key);
}
}
}
}
RedisCacheManager classı ve metodları için kısaca açıklamaları yapacak olursak:
Bir adım önce yazdığımız RedisServer classımızı dependency injection yöntemiyle contructordan inject ederek redis üzerinde ki işlemleri gerçekleştiriyor olacağız.
Add(string key, object data, int keyExpire): Metoduyla metoda parametre olarak gelen string tipindeki key, object tipindeki data’mızı redis üzerine key-value çifti olarak tutacağız. Bunun için öncelikle datamızı json serialization işlemi gerçekleştiriyoruz, daha sonra RedisServer üzerindeki Database’in içerdiği StringSet(key, cacheData); olarak Redis server üzerine eklemiş oluyoruz, yine KeyExpire metoduyla redis database’e eklediğimiz key valuesine göre verimize bir expiration time tanımlıyoruz.
Clear(): Metoduyla database üzerindeki tüm dataların silinmesini biraz önce RedisServer classımız içinde yazdığımız FlushDatabase() metoduyla gerçekleştiriyoruz.
Get<T>(string key): Generic metoduyla database üzerindeki verimize elimizdeki key değeriyle ulaşmak istiyoruz, bunun için öncelikle gelen key değerli verinin database üzerinde olup olmadığını kontrol ediyoruz, bu işlem true döndüğünde ise, database üzerinden elimizdeki key değerine karşılık gelen veriye StringGet(key) metoduyla ulaşıyoruz ve json deserialize işlemini gerçekleştiriyoruz.
IsExist(string key): metoduyla elimizde bulunan key değerine karşılık database tarafında bir verinin bulunup bulunmadığını kontrol ediyoruz.
Remove(string key): metoduyla elimizdeki key değerine karşılık gelen verinin database üzerinden silinmesi işlemini gerçekleştiriyoruz.
RedisCacheManager classımızı oluşturduktan sonra startup.cs içinde ICacheManager interface’i (Cache işlemlerini bu interface üzerinden gerçekleştireceğiz ve bu interface’i classlarımıza inject ettiğimizde senaryomuza göre RedisCacheManager’in bir instance’i gelmeli.) ve RedisServer() classı (RedisCacheManager içinde dependency injection ile kullandığımız için IoC containerdan bir instance’ını almalıyız.) .Net Core’un in-built olarak sunmuş olduğu IoC’a startup.cs üzerinden şöyle set ediyoruz.
services.AddSingleton<RedisServer>();
services.AddSingleton<ICacheService, RedisCacheManager>();
Not: Örnek projede api tarafına istekleri kolayca göndermek ve okumak için Swagger kullandım sizde isterseniz şuradan Swagger’i projenize nasıl implemente edeceğinize göz atabilir ya da Postman gibi araçlarla web apinize isteklerde bulunabilirsiniz veya hiç birine kullanmak istemezseniz tarayıcı üzerinden web apinin ilgili endpointlerine get isteklerinde bulanabilirsiniz.
RedisCacheManager classımızı oluşturduktan sonra artık Redis üzerindeki işlemlerimizi controller üzerinden gerçekleştirebiliriz, bunun için product controller’ımızı şöyle yazabiliriz;
namespace RedisCache.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private List<Product> _products;
private ICacheService _cacheService;
public ProductsController(ICacheService cacheService)
{
_cacheService = cacheService;
CreateProducts();
}
[HttpGet("/addcacheproducts")]
public IActionResult CreateProductCache()
{
_cacheService.Add("products",_products,10);
return Ok();
}
[HttpGet("/deletecacheproducts")]
public IActionResult DeleteProductCache()
{
_cacheService.Remove("products");
return Ok();
}
[HttpGet]
public IActionResult Get()
{
var cacheData = _cacheService.Get<List<Product>>("products");
if (cacheData == null)
{
return BadRequest("cache data is null");
}
return Ok(cacheData);
}
private void CreateProducts()
{
if (_products == null)
{
_products = new List<Product>
{
new Product{ Id=1, Category = "Computers", Name = "Macbook", UnitPrice = 1200, UnitsInStock = 50 },
new Product{ Id=2, Category = "Electronics", Name = "Camera", UnitPrice = 1700, UnitsInStock = 5 },
new Product{ Id=3, Category = "Books", Name = "Romeo&Juliet", UnitPrice = 10, UnitsInStock = 80 },
new Product{ Id=4, Category = "Gift Cards", Name = "Google Play Store Gift 50$ Card", UnitPrice = 49, UnitsInStock = 750 },
new Product{ Id=5, Category = "Cell Phones", Name = "Iphone 12 Pro", UnitPrice = 1000, UnitsInStock = 90 },
};
}
}
}
}
Projede herhangi bir ilişkisel veritabanı bağlantısı kurmadığımızdan controller içinde statik olarak bir product list oluşturan CreateProducts() metodunu ve bu product listi tutan bir List<Product> _products propertysi tanımlıyoruz, daha sonra consctuctor içinde CreateProducts() metodunu çalıştırarak _products propertymizin içine ürünleri eklemiş oluyoruz.
ICacheManager arayüzümüzü constructor ile controllerımız içine inject ediyoruz bu sayede ICacheManager çağırıldığında az önce startup.cs.’te belirttiğimiz üzere RedisCacheManager’imiz containerdan gelmiş olacak.
CreateProductCache(): Metoduyla controller içerisinde oluşturduğumuz statik productlarımızı 10 saniye boyunca redis cachete kalacak şekilde iletiyoruz.
DeleteProductCache(): Metoduyla “product” key değerine sahip verinin (varsa) cache’ten kaldırılmasını sağlıyoruz.
Get(): Metoduyla cache üzerinden “products” key değerine sahip verileri çağırıyoruz.
Uygulamamızı çalıştırdığımızda;
Apimize bir get isteğinde bulunursak, redis üzerinde herhangi bir data henüz cachelenmediği için aşağıdaki gibi yanıt alıyoruz.
Apimizin “localhost:<port>/api/addcacheproducts” endpointine bir get isteğinde bulunduğumuzda ise redis cache tarafına, productscontroller içerisinde oluşturduğumuz ürün listesi eklenmiş oluyor. Cache’e eklenen veriyi bunu kontrol etmek için tekrar “localhost:<port>/api/products” endpointine get isteği gönderdiğimizde dönen sonuç şöyle oluyor, ayrıca addcacheproducts’a get isteği attığımızda redis-cli üzerinde “KEYS *” ile rediste tutulan verilerin key değerlerine ve “GET products” komutuyla product keyiyle Redis cache’te tutulan veriye ulaşabiliriz.
“localhost:<port>/api/addcacheproducts” endpointine istekte bulunduktan sonra “localhost:<port>/api/deletecacheproducts” enpointine istekte bulunursak yine “cache data is null” mesajıyla redis cache üzerinden verimizin silindiğini görebiliriz.
Source Code: https://github.com/kdrkrgz/RedisCache
Sources:
https://redis.io/documentation
https://stackexchange.github.io/StackExchange.Redis/
https://devnot.com/2020/stackexchange-redis-ile-distributed-caching/
https://blog.eduonix.com/web-programming-tutorials/what-is-redis-and-why-is-it-so-popular/
Be First to Comment