Azure Üzerinde Redis Pub/Sub İle Microservis Mimarisinde Data Consistency Sağlamak
Selamlar,
Bu makalede, günlük hayatta daha çok Cache yönetimi için kullanılan Redis’in, Azure üzerinde bir başka özelliği olan Publisher/Subscriber platform’u sayesinde haberleşerek, Azure Function Microservice üzerinden değişen kur bilgilerini asenkron olarak yani Net Core WebApi servisinden bağımsız olarak nasıl MsSQL bir DB’de güncellediğini ve Redis Cache Memory üzerinde data tutarlılığını sağlandığını hep beraber inceleyeceğiz. Bu işlemleri yaparken, .Net Core 3.1 ile beraber kullanılan çeşitli kütüphanelerden ve katmanlı mimaride kullanılan bir kaç yöntemden bahsedeceğiz. Okurken, adım adım bir iş sürecinin halkalar halinde nasıl bir araya gelip birlikte çalıştığını inceleyeceğiz.
“Tutarlı olmak değişmez olmak değil, kararlı olmak inatçı olmak değildir, disiplinli olmak sert olmak değildir, kar yavaş ve devamlı yağarsa tutar.” ―Nevzat Tarhan
Azure Üzerinde Redis Cache Yaratma:
Öncelikle gelin Azure üzerinde Redis Cache yaratalım.
1-)Azure üzerinde “Cache Redis” şeklinde bir arama yapıldıktan sonra, gelen arama sonucundan aşağıdaki seçenek seçilir.
2-)Create buttonuna basıldıktan sonra, aşağıdaki gibi bir ekran ile karşılaşılır. Resource Group, Redis DNS adı, Lokasyonu ve cache type yani makina tipi bütçeye göre seçilir :)
3-) En son onay ekranı geçildikten sonra ilgili Redis, Azure üzerinde aşağıdaki gibi oluşturulur: Console buttonuna basılır ise Redis Console ekranına ulaşılır.
Not: Non-SSL port disabled iken default port 6380’dir. Ama ben bu şekilde ilgili Azure Redis’e uzaktan erişemediğim için Nob-SSL port(6379)’u enabled hale getirdim.
Aşağıdaki gibi Redis Console’a ping yazılınca, “PONG” cevabı alınıyor ise herşey yolunda demektir.
Azure üzerinde kurulu Redis Cache’e, kendi local makinanızda “Redis Desktop Manager” ile bağlanabilirsiniz. Redis Desktop Manager’ı buradaki linkden indirilebilirsiniz.
Redis Desktop Manager üzerinden, Azure Redis Cache’e bağlanılırken, aşağıdaki gibi bir ekran ile karşılaşılır.
.Net Core WebApi Servis Yaratma:
Şimdi gelin .Net Core 3.1 ile, güncel kur bilgilerini gösteren ve güncelleyen bir WebApi servisi yazalım.
Bu makalenin yazıldığı an itibari ile geçerli .Net Core (3.1.403) versiyonu aşağıdaki gibidir.
.Net Core ExchangeService WebApi servisi aşağıdaki gibi yaratılır.
.Net Core üzerinde Redis’in kullanılabilmesi için ServiceStack, kullanılabilecek kütüphanelerden biridir. İlgili kütüphane aşağıdaki komut satırı ile projeye eklenir.
1 |
dotnet add package ServiceStack.Redis.Core --version 5.9.2 |
Model klasörü oluşturulup, aşağıdaki gibi Exchange sınıfı yaratılır.
ExchangeService/Models/Exchange: Sayfaya basılacak Exchange(Kur) modeli.
1 2 3 4 5 6 7 8 |
public class Exchange { public int ID {get; set;} public string Name{ get; set; } public double Value { get; set; } public System.DateTime CreatedDate { get; set; } public System.DateTime? UpdatedDate { get; set; } } |
Bu makalede ayrıca bir arayüz yazılmayacağı için, ilgili methodlara dışardan erişilebilmesi amacı ile Swagger .Net Core ortamına aşağıdaki komut ile eklenir. Siz isterseniz Postman de kullanabilirsiniz.
1 |
dotnet add package Swashbuckle.AspNetCore |
Swagger için, Actionların üstünde açıklama girilebilmesi için, aşağıdaki kütüphanenin eklenmesi gerekmektedir.
1 |
dotnet add package Swashbuckle.AspNetCore.Annotations |
Sıra geldi, .Net Core Ortamında Swagger’ın “Startup.cs’de tanımlanmasına.
ExchangeService/Startup.cs/ConfigureServices(1):
- “services.AddSwaggerGen(c =>” : Swagger .Net Core ortamında belli configurasyonlar ile tanımlanır.
- “c.EnableAnnotations()” ile Actionlar üzerinde swagger açıklaması tanımlanabilir.
- “c.SwaggerDoc(“CoreSwagger”, new OpenApiInfo” : CoreSwagger adı ile Başlık, versiyon, açıklama ve iletişim özellikleri ile service’e eklenir. Burada “CoreSwagger” tanımlaması çok önemlidir. Dinamik oluşacak döküman ismi burada tanımlanır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public void ConfigureServices(IServiceCollection services) { services.AddSwaggerGen(c => { c.EnableAnnotations(); //Amaç Swagger'da Açıklama Girmek /* CoreSwagger adı ile Başlık, versiyon, açıklama özellikleri ile service’e eklenir. */ c.SwaggerDoc("CoreSwagger", new OpenApiInfo { Title = "Swagger on ASP.NET Core", Version = "1.0.0", Description = "VBT Web Api", TermsOfService = new Uri("http://swagger.io/terms/") }); }); services.AddControllers(); } |
ExchangeService/Startup.cs/Configure:
- app.UseSwagger(): .Net Core projesine swagger eklenmiştir.
- .UseSwaggerUI(c => {c.SwaggerEndpoint(“/swagger/CoreSwagger/swagger.json”, “Swagger Test .Net Core”);}): Burada swagger için kullanılacak “json” dosyasının kaydedileceği yer tanımlanmaktadır.
NOT: “/CoreSwagger” olarak yazılan kısım ==> yukarıda ConfigureServices’de tanımlanan “c.SwaggerDoc(“CoreSwagger“, new OpenApiInfo” ile aynı olmak zorundadır. Swagger’ın isim parametre olarak kullanılmaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { . . app.UseSwagger() .UseSwaggerUI(c => { //TODO: Either use the SwaggerGen generated Swagger contract (generated from C# classes) c.SwaggerEndpoint("/swagger/CoreSwagger/swagger.json", "Swagger Test .Net Core"); //TODO: Or alternatively use the original Swagger contract that's included in the static files // c.SwaggerEndpoint("/swagger-original.json", "Swagger Petstore Original"); }); . . } |
ExchangeService/Controllers/ExchangeController.cs(1): Aşağıda görüldüğü gibi, kayıtlı kur listesi çekilecek method şimdilik tanımlanmıştır. Swagger açıklaması olarak ilgili text, [SwaggerOperation] attribute’ü ile tanımlanır.
1 2 3 4 5 6 7 |
[HttpGet] //Amaç Swagger'da açıklama olarak girilir. [SwaggerOperation(Summary = "Kayıtlı tüm Kur bilgilerinin çekilmesi.", Description = "<b>Azure SqlDB üzerinden:</b> </br>Tüm Kur Bilgileri Listelenir.")] public string Get() { return "Hello Exchange World.."; } |
Örnek ekran görüntüsü aşağıdaki gibidir.
“Yükü paylaşmanın en iyi yolu, organize olmaktır. Onu da yönetmek için bir orchestrator’a ihtiyaç vardır. Tek özelliği, herkesi ile ortak dilde buluşmasıdır.” ―
Azure Üzerinde SQL DB Yaratma:
Azure üzerinde Sql Database aşağıdaki gibi aranarak yaratılır:
Aşağıda görüldüğü gibi ilgili alanlar doldurulup, istenen donanım seçildikten sonra SqlDB Azure üzerinde yaratılır.
Azure üzerindeki SQL DB Connection aşağıdaki gibi ilgili alan tıklanarak alınır:
Exchange tablosu, aşağıdaki Sql Scriptin çalıştırılması ile oluşturulur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[Exchange]( [ID] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](50) NULL, [Value] [decimal](18,6) NULL, [CreatedDate] [datetime] NOT NULL, [UpdateDate] [datetime] NULL, CONSTRAINT [PK_Exchange] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY] ) ON [PRIMARY] GO |
Şimdi gelin, manuel birkaç kur datasını Insert edelim.
1 2 |
INSERT INTO [dbo].[Exchange] ([Name],[Value],CreatedDate,UpdateDate) VALUES('Dolar',8.3574,GETDATE(),NULL) INSERT INTO [dbo].[Exchange] ([Name],[Value],CreatedDate,UpdateDate) VALUES('Euro',9.8574,GETDATE(),NULL) |
Project Entity:
Şimdi gelin önce DBContext katmanını yaratalım. Aşağıdaki komut ile yeni bir .Net Core Console Entity projesi oluşturulur.
1 |
dotnet new console -o Entity |
Bu yazıda Database First kullanılarak, Azure üzerindeki Exchange Tablosu ve BlackJack DBContext(Database) projeye dahil edilir.
Sıra, var olan bir SQL DB’den, DBContext ve Model sınıflarını oluşturmaya geldi. Açılan Entity projesine, EntityFrameworkCore’a ait SqlServer, SqlServer.Design ve Tools kütüphaneleri aşağıdaki gibi eklenir.
1 2 3 |
dotnet add package Microsoft.EntityFrameworkCore.SqlServer.Design; dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools |
Bu uygulamada, Azure üzerinde BlackJack Sql DB’ye bağlanılıp, BlackJackContext sınıfı ve Exchange Pocosu aşağıdaki komut ile otomatik olarak oluşturulur. .Net Core projesinde bu işlemin yapılabileceği bir arayüz malesef henüz yoktur. Belki 3th party toollar olabilir. Ama bu makalede, aşağıdaki komut satırılı ile yetinilmiştir. İlgili komut satırı çalıştırıldıktan sonra, proje içinde “Models/DB” şeklinde klasörler oluşturulmuş ve ilgili DBContext ve Poco dosyaları burada yaratılmıştır.
1 |
dotnet ef dbcontext scaffold "Server=tcp:bkasmer.database.windows.net,1433;Database=BlackJack; User ID=*****; Password=****;" Microsoft.EntityFrameworkCore.SqlServer --output-dir Models/DB |
ExchangeService:
Şimdi gelin, Kur Datasını çektiğimiz ve güncellediğimiz servisi yazalım. Aşağıdaki komut ile ilgili webapi servisi yaratılır.
1 |
dotnet new webapi -o ExchangeService |
Öncelikle aşağıdaki kod çalıştırılıp, yukarıda yazılan Entity.dll’i ExchangeService projesine dahil edilir.
1 |
dotnet add reference ../DB/Entity/Entity.csproj |
ExchangeService/Startup.cs/ConfigureServices(2):
Aşağıdaki komut ile EntityFrameworkCore.SqlServer kütüphanesi projeye dahil edilir.
1 |
dotnet add package Microsoft.EntityFrameworkCore.SqlServer |
Aşağıdaki “AddDbContext()” methodu ile Azure Üzerindeki SQL DB’ye bağlı dbContext sınıfı Dependency Injection ile ayağa kaldırılır.
1 2 3 4 5 6 |
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<BlackJackContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); . . } |
ExchangeService/appsettings.json: Ayrıca Azure üzerindeki Sql server’ın connection string’i, appsettings’e aşağıdaki gibi yazılması gerekmektedir.
1 2 3 |
"ConnectionStrings": { "DefaultConnection": "Server=tcp:bkasmer.database.windows.net,1433;Database=BlackJack; User ID=*****; Password=*****;" }, |
Şimdi sıra geldi ExchangeService’in yazılmasına.
Not: Database işlemleri gibi tüm bussines işler, doğrudan servis arkasında yazılmamalıdır. Bunun için ayrı bir bussines katmanı yapılmalıdır. Bu projede, Database ve Redis cache ile alakalı, Exchange() Action’ın arkasında hiçbir kod bulunmamaktadır. Bussines katmanı olan ExchangeServices sınıfında, tüm bu bussines işleri çözülmektedir.
Öncelikle Servisden dönecek Global Response Modeli yazalım: Böyle bir genel model tipine gidilmesindeki amaç, projenin tamamında yani hem backend hem de front-end tarafda aynı model tipinin beklenmesinin sağlanması ile işlerin kolaylaşması ve ihtiyaç duyulan ortak propertylerin tekbir model altında toplanmasıdır.
ExchangeService/Service/IServiceResponse :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System.Collections.Generic; public interface IServiceResponse<T> { //“List” : Kayıt kümesi dönüleceği zaman kullanılır. Örneğin bir sayfa üzerindeki tüm yetkiler liste şeklinde bu property’e atanabilir. IList<T> List { get; set; } //“Entity” : Tek bir kaydın dönülmesi durumunda, kullanılır. T Entity { get; set; } //“Count” : Dönen toplam kayıt sayısıdır. int Count { get; set; } //“IsSuccessful” : İşlemin başarılma durumunu gösterir.Insert, Update, Delete lerdeki success. Bu property'e Mobil tarafta da ihtiyaç duyulmakta. bool IsSuccessful { get; set; } string ExceptionMessage { get; set; } } |
ExchangeService/Service/ServiceResponse : Aşağıda görüldüğü gibi, geri dönülecek model tek bir kayıt ise Entity<T>, List ise List<T> propertysine atanarak geri dönülmüştür. Count paging için, IsSuccessful da işin başarılıp başarılmadığını göstermek için kullanılmaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; public class ServiceResponse<T> : IServiceResponse<T> { [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string ExceptionMessage { get; set; } public IList<T> List { get; set; } [JsonProperty] public T Entity { get; set; } public int Count { get; set; } public bool IsSuccessful { get; set; } public ServiceResponse(HttpContext context) { } } |
Öncelikle AutoMapper kütüphanesi aşağıdaki komut ile yüklenir: Amaç DBModel ile ViewModelleri birbiri ile otomatik eşleştirmektir.
1 |
dotnet add package AutoMapper |
AutoMapper’ın amacı : Uygulamada client’a gönderilen ViewModel’den Entity’e veya Entity’den ViewModel’e eşleme amacı ile kullanılan kolonların, belirtilen konfigürasyonlar ile otomatik olarak atanması sağlayan kütüphanedir.
ExchangeService/Startup.cs/ConfigureServices(3): Automapper sınıfı, birazdan ilgili Entity ve ViewModeller ile eşlenecek olan MappingProfile sınıfı ile, .Net Core tarafında aşağıdaki gibi ayağa kaldırılır.
1 2 3 4 5 6 7 |
//Automapper var mappingConfig = new MapperConfiguration(mc => { mc.AddProfile(new MappingProfile()); }); IMapper mapper = mappingConfig.CreateMapper(); services.AddSingleton(mapper); |
1 2 3 4 5 6 7 8 9 10 11 |
using AutoMapper; using Entity.Models.DB; public class MappingProfile : Profile { public MappingProfile() { CreateMap<Exchange, ExchangeModel>(); CreateMap<ExchangeModel, Exchange>(); } } |
ExchangeService/Service/IExchangeServices(1): Tüm kurların liste halinde çekileceği method tanımlanır.
1 2 3 4 5 6 7 8 9 |
using System.Collections.Generic; namespace Services.Exchanges { public interface IExchangeServices { ServiceResponse<ExchangeModel> GetAllExchange(); } } |
ExchangeService/Service/ExchangeServices(1): Dependency injection ile Automapper ve DBContext constructor’da eklenir.
GetAllExchange() : Methodunda Azure üzerindeki SQL DB üzerindeki tüm kur kayıtları çekilir.
- “new ServiceResponse<ExchangeModel>(null)”: Geriye global ServicesResponse tipinde bir model dönülecektir. Baştan boş olarak yaratılır. Amaç oluşabilecek null hatasını önceden önlemek.
- “_context.Exchange.ToList()”: DBContext ile Azure üzerinden, “List of Exchange” tipinde kayıtlar çekilir.
- “_mapper.Map<IList<ExchangeModel>>(exchangeResult)”: Çekilen Exchange listesi, “ExchangeModel” tipine, AutoMapper yardımı ile dönüştürülür.
- “response.List = model”: Response modelin List property’sine, ilgili result atanır ve geri dönülür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
using Entity.Models.DB; using System.Linq; using System.Collections.Generic; using AutoMapper; namespace Services.Exchanges { public class ExchangeServices : IExchangeServices { private readonly BlackJackContext _context; private readonly IMapper _mapper; public ExchangeServices(BlackJackContext context, IMapper mapper) { _context = context; _mapper=mapper; } public ServiceResponse<ExchangeModel> GetAllExchange() { var response = new ServiceResponse<ExchangeModel>(null); var exchangeResult = _context.Exchange.ToList(); if (exchangeResult != null) { var model = _mapper.Map<IList<ExchangeModel>>(exchangeResult); response.List = model; response.IsSuccessful = true; } return response; } } } |
ExchangeService/Startup.cs/ConfigureServices(3): Yazılan ExchangeServices, Dependency Injection ile.Net Core ortamında RunTime’da ayağa kaldırılır.
1 2 3 4 5 6 7 8 |
public void ConfigureServices(IServiceCollection services) { . . services.AddTransient<IExchangeServices, ExchangeServices>(); . . } |
ExchangeService/Controllers/ExchangeController.cs(2): Şimdi gelin ilgili ExchangeService’i kullanarak, Client’dan istenen tüm kur listesini geriye dönelim. Aşağıda görüldüğü gibi “IExchangeServices service“, constructor’da Dependency Injection ile projeye dahil edilmiştir.
- “public ServiceResponse<ExchangeModel> Get()”: Get() methodunda, yukarıda yazılan services sınıfının “GetAllExchange()” methodu çağrılıp ==> “ServiceResponse<ExchangeModel>” tipinde model geri dönülür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Services.Exchanges; using Swashbuckle.AspNetCore.Annotations; namespace ExchangeService.Controllers { [ApiController] [Route("[controller]")] public class ExchangeController : ControllerBase { private readonly IExchangeServices _service; public ExchangeController(IExchangeServices service) { _service = service; } [HttpGet] //Amaç Swagger'da açıklama olarak girilir. [SwaggerOperation(Summary = "Kayıtlı tüm Kur bilgilerinin çekilmesi.", Description = "<b>Azure SqlDB üzerinden:</b> </br>Tüm Kur Bilgileri Listelenir.")] public ServiceResponse<ExchangeModel> Get() { return _service.GetAllExchange(); //return "Hello Exchange World.."; } } } |
Gelin şimdi benzer işlemleri belirlenen bir Kur’un çekilmesi için yapalım. Yani seçilen bir kurun detayına gidelim.
ExchangeService/Model/ExchangeEnum.cs: Öncelikle istenen kur tipi için, ExchnageEnum sınıfı aşağıdaki gibi yaratılır. Amaç, ne olduğu bilinmeyen ID ler ile çalışmaktan kaçınmaktır.
1 2 3 4 5 6 7 8 9 |
public class ExchangeEnum { public enum ExchangeType { Dolar = 1, Euro = 2, Pound = 3 } } |
ExchangeService/Service/IExchangeServices(2): “GetExchangeByName(ExchangeType exchangeType)” methodu aşağıdaki gibi eklenir.
1 2 3 4 5 6 7 8 9 10 11 |
using System.Collections.Generic; using static ExchangeEnum; namespace Services.Exchanges { public interface IExchangeServices { ServiceResponse<ExchangeModel> GetAllExchange(); ServiceResponse<ExchangeModel> GetExchangeByName(ExchangeType exchangeType); } } |
ExchangeService/Service/ExchangeServices(2): “GetExchangeByName(ExchangeType exchangeType)” methodunda, belirlenen Kur tipine göre detay bilgisi Azure üzerindeki SqlDb’den çekilip geri dönülür.
- “var exchangeResult = _context.Exchange.Where(ex => ex.Name == exchangeType.ToString()).FirstOrDefault()”: Azure SQL server üzerinden, Linq ile “ExchangeType“‘a göre filitreleme yapılarak ilgili kur detayı çekilir.
- “var model = _mapper.Map<ExchangeModel>(exchangeResult)”: Çekilen Exchange Entity, AutoMapper ile ExchangeModel tipine cast edilir.
- “response.Entity = model”: geriye tek bir değer döndüğü için, “ServiceResponse” modelinin Entity property’si set edilir. Ve geri dönülür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
. . public ServiceResponse<ExchangeModel> GetExchangeByName(ExchangeType exchangeType) { var response = new ServiceResponse<ExchangeModel>(null); var exchangeResult = _context.Exchange.Where(ex => ex.Name == exchangeType.ToString()).FirstOrDefault(); if (exchangeResult != null) { var model = _mapper.Map<ExchangeModel>(exchangeResult); response.Entity = model; response.IsSuccessful = true; } return response; } . . |
ExchangeService/Controllers/ExchangeController.cs(3): İstenen kur detayını dönen Action’ı, gelin hep beraber yazalım.
- “[HttpGet(“GetExchangeByName/{exchangeType}”)]” : İlgili attribute ile routing tanımlaması yapılmıştır.
- [SwaggerOperation(Summary ve Description): methodunun ne işe yaradığı ve parametre olarak beklenen ExchangeType Enum’unun sayısal değer karşılığı açıklanmıştır.
- “return _service.GetExchangeByName(exchangeType)”: Yukarıda yazılan “_service.GetExchangeByName()“, servis çağrılarak “ServiceResponse<ExchangeModel>” tipi geri dönülmüştür.
1 2 3 4 5 6 7 |
[HttpGet("GetExchangeByName/{exchangeType}")] //Amaç Swagger'da açıklama olarak girilir. [SwaggerOperation(Summary = "İstenen tek bir Kur bilgisinin çekilmesi.", Description = "<b>Azure SqlDB üzerinden:</b> </br>istenen Kur bilgisi adından filitrelenerek çekilir. </br>1-) Dolar</br>2-) Euro</br>3-) Pound")] public ServiceResponse<ExchangeModel> GetExchangeByName(ExchangeType exchangeType) { return _service.GetExchangeByName(exchangeType); } |
Şimdi Gelin Çekilen Kur Listesi ve Kur Detay Bilgisini, Performansı Arttırmak Amacı ile Azure Üzerindeki Redis’den Alalım:
ExchangeService/Service/IRedisCacheService: Aşağıda görüldüğü gibi redis işlemleri için ayrı bir servis yazılmıştır. Get<>(), Set<>() generic methodları tanımlanmıştır.
1 2 3 4 5 6 7 8 |
using System; using System.Collections.Generic; public interface IRedisCacheService { T Get<T>(string key); void Set(string key, object data, DateTime time); } |
ExchangeService/Service/RedisCacheService: Generic olarak tanımlı methodlar, istenen tipde datayı redis’de saklamak ve redis’den almak için kullanılır. Azure üzerindeki Redis connection, burada config’den almak yerine string değişkenler ile tanımlanmıştır.
- “public T Get<T>(string key)”: Belirlenen key’e göre, Redisdeki data model çekilir.
- “public void Set(string key, object data, DateTime time)”: Belirlenen Expire suresi kadar, tanımlanan string key’e ilgili model Azure üzerindeki Redis’e atılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
using Newtonsoft.Json; using Microsoft.Extensions.Options; using ServiceStack.Redis; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Dashboard.Core.Caching { public class RedisCacheService : IRedisCacheService { #region Fields int RedisPort = 6379; string RedisEndPoint = "******.redis.cache.windows.net"; string RedisPassword = "********"; private readonly RedisEndpoint conf = null; #endregion public RedisCacheService() { conf = new RedisEndpoint { Host = RedisEndPoint, Port = RedisPort, Password = RedisPassword }; } public T Get<T>(string key) { try { using (IRedisClient client = new RedisClient(conf)) { return client.Get<T>(key); } } catch { throw new RedisNotAvailableException(); //return default; } } public void Set(string key, object data, DateTime time) { try { using (IRedisClient client = new RedisClient(conf)) { var dataSerialize = JsonConvert.SerializeObject(data, Formatting.Indented, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects }); client.Set(key, Encoding.UTF8.GetBytes(dataSerialize), time); } } catch { throw new RedisNotAvailableException(); } } } } |
“Mühendislik, yüz durumdan sadece biri bile kıritik duruma sebebiyet veriyorsa, o durum için hazırlanmaktır.” ―Bora Kaşmer
Custom Redis Exception:
ExchangeService/Model/RedisNotAvailableException: Redise bağlanılamaması durumunda, fırlatılacak Custom Redis hata mesajı aşağıda görüldüğü gibi tanımlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using System; public class RedisNotAvailableException : Exception { public string _errorCode = "431"; public override string Message { get { return "Redis is not available."; } } public string ErrorCode { get { return _errorCode; } } } |
ExchangeService/Startup.cs/ConfigureServices(4): İlgili Redis Servis, .Net Core Projede aşağıdaki gibi tanımlanır.
1 2 3 4 5 6 7 8 |
public void ConfigureServices(IServiceCollection services) { . . services.AddTransient<IRedisCacheService, RedisCacheService>(); . . } |
ExchangeService/Service/ExchangeServices(3): Aşağıda görüldüğü gibi, GetAllExchange() ve GetExchangeByName() methodlarında doğrudan DB’ye gitmek yerine öncelikle Redis’e bakılmış yok ise, DB’ye gidilip çekilen data, 1 dakika expire süreli Redis Cache atılmıştır.
- “_redisCacheManager = redisCacheManager“: RedisCacheManager, sınıfa Dependency Injection ile dahil edilir.
- “GetAllExchange() => var cacheKey = “AllExchange”; var result = _redisCacheManager.Get<List<ExchangeModel>>(cacheKey)“: Tüm kur bilgisi, Redis’de var mı diye bakılır?
- “GetAllExchange() => if (result != null)“: Varsa Redis’den alınan List of ExchangeModel => ServiceResponse modelinin, “List” propertysine atanarak geri dönülür. Redis’de yok ise, Azure üzerindeki SqlDB’den tüm kur bilgileri çekilerek Redise’e 1 dakikalık Expire süresi ile atanıp, geri dönülür.
- “GetExchangeByName() => var cacheKey = “Exchange:” + (int)exchangeType; var result = _redisCacheManager.Get<ExchangeModel>(cacheKey)“: İstenen bir kur detay bilgisi alınması sırasında da, önce Redis’e bakılır. Redis key olarak “Exchange:”+ “Enum ExchangeType”‘ın int değeri alınır. Yani mesela Dolar için => “Exchange:1” key’i oluşturulur. İlgili data Redisde varsa çekilir.
- “GetExchangeByName() =>if (exchangeResult != null)” Redis’de Kur detay bilgisi var ise ExchangeModel => ServiceResponse modelinin, “Entity” propertysine atanarak geri dönülür. Redis’de yok ise, Azure üzerindeki SqlDB’den Kur Detay bilgisi çekilerek Redise’e 1 dakikalık Expire süresi ile atanıp, geri dönülür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
using Entity.Models.DB; using System.Linq; using System.Collections.Generic; using AutoMapper; using static ExchangeEnum; using System; namespace Services.Exchanges { public class ExchangeServices : IExchangeServices { private readonly BlackJackContext _context; private readonly IRedisCacheService _redisCacheManager; private readonly IMapper _mapper; public ExchangeServices(BlackJackContext context, IMapper mapper, IRedisCacheService redisCacheManager) { _context = context; _mapper = mapper; _redisCacheManager = redisCacheManager; } public ServiceResponse<ExchangeModel> GetAllExchange() { var response = new ServiceResponse<ExchangeModel>(null); //Check Redis var cacheKey = "AllExchange"; var result = _redisCacheManager.Get<List<ExchangeModel>>(cacheKey); //------------------------------- if (result != null) { response.List = result; return response; } else { var exchangeResult = _context.Exchange.ToList(); if (exchangeResult != null) { var model = _mapper.Map<IList<ExchangeModel>>(exchangeResult); response.List = model; response.IsSuccessful = true; //SET Redis _redisCacheManager.Set(cacheKey, response.List, DateTime.Now.AddMinutes(1)); } return response; } } public ServiceResponse<ExchangeModel> GetExchangeByName(ExchangeType exchangeType) { var response = new ServiceResponse<ExchangeModel>(null); //Check Redis var cacheKey = "Exchange:" + (int)exchangeType; var result = _redisCacheManager.Get<ExchangeModel>(cacheKey); //------------------------------- if (result != null) { response.Entity = result; return response; } else { var exchangeResult = _context.Exchange.Where(ex => ex.Name == exchangeType.ToString()).FirstOrDefault(); if (exchangeResult != null) { var model = _mapper.Map<ExchangeModel>(exchangeResult); response.Entity = model; response.IsSuccessful = true; //SET Redis _redisCacheManager.Set(cacheKey, response.Entity, DateTime.Now.AddMinutes(1)); } } return response; } } } |
Şimdi Sıra Geldi Bir Kur Bilgisi Güncellenirken, Redis Pub/Sub ile hem SqlDB hem de Redis Cache’in Asenkron Olarak Güncellenmesine:
Burada esas amaç, performansı arttırmak amacı ile, SQL DB ve RedisCache güncelleme işlemlerinin arka tarafda yani WebApi servisinden bağımsız asenkron bir şekilde, hatta başka sunucularda yapılmasını sağlamaktır.
ExchangeService/Service/IRedisCacheService(2): Öncelikle gelin RedisCacheManager’a, Publish yapabilme özelliğini katalım. Aşağıda görüdüğü gibi Publish methodu interface’e eklenmiştir.
1 2 3 4 5 |
. . void Publish(string channel,object data); . . |
ExchangeService/Service/RedisCacheService(2): Aşağıda görüldüğü gibi publish() methodunda, parametre olarak gelen object data(bu bizim örnekte “ExchangeModel” olacak), ilgili channel’a Serialize edilip, string olarak gönderilmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
. . public void Publish(string channel, object data) { try { using (IRedisClient client = new RedisClient(conf)) { var dataJson = JsonConvert.SerializeObject(data); client.PublishMessage(channel, dataJson); } } catch { throw new RedisNotAvailableException(); //return default; } } . . |
ExchangeService/Service/IExchangeServices: Aşağıdaki method, ilgili Exchange interface’e eklenir.
1 |
ServiceResponse<bool> UpdateExchange(ExchangeModel exchangeData); |
ExchangeService/Service/ExchangeServices(4): Aşağıda bir kur güncelleneceği zaman, çağrılan servis tanımlanmaktadır. Güncellenecek Kur Datası, yani exchangeData ==> “Exchange” channel’ına, işlenmek üzere gönderilir. Burada yapılan tek iş budur. Herhangi bir DB veya Redis güncellemesi yapılmamaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public ServiceResponse<bool> UpdateExchange(ExchangeModel exchangeData) { var response = new ServiceResponse<bool>(null); try { _redisCacheManager.Publish("Exchange", exchangeData); response.IsSuccessful = true; } catch (Exception ex) { response.IsSuccessful = false; Console.WriteLine(ex.Message); } return response; } |
ExchangeService/Controllers/ExchangeController.cs(4): Gönderilen Kur’un, Sql DB ve Redis’de güncellenmesi için, burada bir işlem yapılmayıp doğrudan servise gönderilir.
1 2 3 4 5 6 7 8 9 10 11 |
. . [HttpPost("UpdateExchange")] //Amaç Swagger'da açıklama olarak girilir. [SwaggerOperation(Summary = "Amaç Gönderilen Kur'un güncellenmesidir.", Description = "<b>Azure SqlDB üzerinde:</b> </br>gönderilen Kur bilgisi güncellenir. </br>1-) Dolar</br>2-) Euro</br>3-) Pound")] public ServiceResponse<bool> UpdateExchange(ExchangeModel exchangeData) { return _service.UpdateExchange(exchangeData); } . . |
Azure Functions:
Gelin yeni bir Azure Functions Oluşturalım: Amaç Redisdeki “Exchange” channel’ının dinlenip, gelen ExchangeModel’in hem Sql DB’de hem de Redis’de güncellenmesinin sağlanması.
Redis Pub/Sub:
RedisWorker:
1-)Öncelikle aşağıdaki komut ile Worker Service projesi oluşturulur.
1 |
dotnet new worker -o RedisWorker |
2-)Sonra aşağıdaki paketlerin kurulumu yapılır: Redis için ServiceStack, DB işlemleri için Entity Projesindeki BlackJackContext ve Sql işlemleri için EntityFrameworkCore kütüphaneleri eklenir.
1 2 3 |
dotnet add package ServiceStack.Redis.Core dotnet add reference ../DB/Entity/Entity.csproj dotnet add package Microsoft.EntityFrameworkCore.SqlServer |
3-RedisWorker/Worker.cs(1): Redisden alınacak Kur bilgisi, aşağıda tanımlı model’e dönüştürülür.
1 2 3 4 5 6 7 8 |
public class ExchangeModel { public int ID { get; set; } public string Name { get; set; } public double Value { get; set; } public System.DateTime CreatedDate { get; set; } public System.DateTime? UpdatedDate { get; set; } } |
Resimin Kaynağı: https://miro.medium.com/max/1400/1*Mc3xijLnyBbiAPEGonlqtg.gif
4-RedisWorker/Worker.cs(2): Aşağıda görüldüğü gibi Azure üzerindeki Redis’deki “Exchange” channel’ını dinleyen Subscriber, kendisine güncelleme amaçlı gönderilen son güncel “ExchangeModel“‘i, Azure üzerindeki Sql ve Redis’de günceller.
- Worker proje template ile default olarak kodda gelen “while(){ }” döngüsü,bu örnekte gerek olmadığı için kaldırılmıştır. Zaten Redis Subscriber her an ilgili channel’ı dinlemektedir. Bu neden ile tekrar eden bir döngüye ihtiyaç yoktur.
- “using (IRedisClient client = new RedisClient(conf))”: Azure üzerindeki Redis’e, belirtilen config ile bağlanılır.
- “using (sub = client.CreateSubscription())”: İlgili Redis’i dinleyen, subscriber yaratılır.
- “sub.OnMessage += (channel, exchange) =>” : Exchange channel’ının dinlendiği ve gönderilen ExchangeModel’in yakalandığı method, burasıdır.
- “ExchangeModel _exchange = JsonConvert.DeserializeObject<ExchangeModel>(exchange)”: Yakalanan string data => yukarıda tanımlanan ExchangeModel’e deserialize edilir.
- “using (IRedisClient clientServer = new RedisClient(conf))” : Redis’e güncelleme yapılabilmesi için yeni bir Redis Client’ın yaratılması gerekmektedir. Eğer Subscription için hemen yukarısında yaratılmış olan Redis Client kullanılmaya çalışılır ise, aşağıdaki gibi bir hata aile karşılaşılır.
- “string redisKey = “Exchange:” + _exchange.ID”: Gelen Exchange modele göre, Redis key oluşturulur.
- “clientServer.Set<ExchangeModel>(redisKey, _exchange)”: Redis üzerindeki ExchangeModel, yeni gelen ExchangeModel ile ezilir. Yok ise yeni yaratılır.
- “using (BlackJackContext context = new BlackJackContext())”: Azure üzerindeki Sql DB’ye ait BlackJackContext, yukarıda yazılan Entity projesinin import edilmesi ile yaratılır.
- “var exchangeModel = context.Exchange.FirstOrDefault(ex => ex.Id == _exchange.ID)”: SqlDb üzerindeki güncellenecek, eski kur bilgisi çekilir.
- “exchangeModel.Value = (decimal)_exchange.Value; exchangeModel.UpdateDate = DateTime.Now”: Kur değeri ve son güncellenme tarihi, update edilir.
- “context.SaveChanges()”: Entity üzerinde yapılan değişiklikler, kaydedilir.
- “sub.SubscribeToChannels(new string[] { “Exchange” })”: Redis’in dinlediği channel, “Exchange” olarak tanımlanır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); await Task.Delay(1000, stoppingToken); var conf = new RedisEndpoint() { Host = "****.redis.cache.windows.net", Port = 6379, Password = "****" }; Console.WriteLine("Services Start..."); using (IRedisClient client = new RedisClient(conf)) { IRedisSubscription sub = null; using (sub = client.CreateSubscription()) { sub.OnMessage += (channel, exchange) => { try { ExchangeModel _exchange = JsonConvert.DeserializeObject<ExchangeModel>(exchange); Console.WriteLine(_exchange.Name + ": " + _exchange.Value); //Redis UPDATE using (IRedisClient clientServer = new RedisClient(conf)) { string redisKey = "Exchange:" + _exchange.ID; clientServer.Set<ExchangeModel>(redisKey, _exchange); } //Sql UPDATE using (BlackJackContext context = new BlackJackContext()) { var exchangeModel = context.Exchange.FirstOrDefault(ex => ex.Id == _exchange.ID); exchangeModel.Value = (decimal)_exchange.Value; exchangeModel.UpdateDate = DateTime.Now; context.SaveChanges(); } } catch (Exception ex) { Console.WriteLine(ex.Message); } }; sub.SubscribeToChannels(new string[] { "Exchange" }); } } } } |
Geldik bir makalenin daha sonuna. Bu makalede, baştan sona ufak bir .Net Core Projenin ayağa kaldırılması sırasında nelere dikkat edimesi gerektiğine, bazı pratik ip uçlarına, ve Azure üzerinde yaratılan SqlDB ve Redis Cache gibi yapıların entegrasyonuna değindik. Performans amaçlı Redis sadece bir Memory Cache Tool’u değil, aynı zamanda hem bir DB hem de Socket desteği olan tam bir isviçre çakısıdır. Biz bu makalede, yüksek trafik anında, WebServisi üzerindeki yükü, SqlDB ve Redis güncellemelerini başka bir sunucu üzerindeki Worker Servis üzerinden yaparak azalttık. Worker Servis üzerindeki Redis Subscriber, ilgili “Exchange” channel’ını sürekli dinleyerek kendisine gelen güncel kur paketlerini asenkron olarak SqlDB ve Redis’de güncellemiştir. Microservice mimarisinde bu şekilde dağıtık çalışmak, yükün dağıtmasına, farklı teknolojilerin bir arada kullanılmasına, teste ve debug işlemlerine olanak sağlasa da, ciddi bir maintenance getirmektedir. Publish ve paketlerin dağıtılması için Devops, Monitor için de çeşitli toolara mutlak ihtiyaç vardır.
Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Source Code: https://github.com/borakasmer/AzureWorker-RedisPub-Sub
Source:
Son Yorumlar