Redis’de Data Consistency’yi .Net Core Ortamında Mikroservisler İle Sağlama
Selamlar,
Bu makalede, anlık data değişiminin çok olmadığı yapılarda, farklı sunucularda bulunan Redis üzerindeki datanın, tüm sunucularda eşitlenmesi yani aynı olması konusu, “redis master-slave” ilişkisi kurulmadan microservis mantığında incelenmiştir. Aslında hikaye şöyle başlıyor, master yazma, slaveler ise ‘N’ sayıdaki readonly okuma işinin yapıldığı sunucularıdır. Kısacası Master’a yazılan tüm datanın, ‘N’ sayıdaki slave’e replicaları oluşturulur. Bu tek yönlü bir iştir. Bu makalede, ben her bir redis sunucusunun, kendine ait tek bir server’a atanmasını ve master gibi çalışmasını ama herhangi bir redis sunucusunda oluşacak data değişikliğinin diğer sunuculara ait redis makinalarına da bildirilip, tüm datanın her sunucuda eşitlenmesini amaçladım. Kısacası yazma da dahil olmak üzere tüm yükü farklı makinalara atadım. Eğer Redis hakkında pek de bir fikriniz yok ise, öncelik ile bu makale serisini okumanızı tavsiye ederim.
Not: Sunuculardan birinin gitmesi ya da failover durumu ile sentinelleri bu makalenin konusu değildir. İstenir ise, herbir sunucu içinde master-slave yapısı oluşturulup, sentineller ile kontrol altına alınabilir.
Öncelikle gelin Redis neden kullanılır en temel ihtiyacı ile konuşalım. Diyelimki sitenize, anlık 10bin kişi geldi. İlgili datanın her bir client için DB’den çekilmesi, tam bir felaket senaryosudur. Bunun için Redis gibi Rem’de tutulan çok hızlı cevap veren bir cache yapısına ihtiyaç vardır. Aksi takdirde DB serverların CPU’su tavan yapacak ve bir süre sonra cevap veremiyecek bir hal alacaktır. Not: Redis config ayarları yapılır ise, ilgili datayı DB’de de tutmaktadır. Ama tabi ki hızdan feragat etmek gerekir.
Gelen client sayısı 50binlere çıktığı durumlarda, tek bir Redis sunucusu maalesef yeterli olmıyacaktır. Bugünkü makalenin konusu, her bir sunucuya bir Redis atanarak en az iki sunuculu bir yapıda, yani 2 redis makinasında, data tutarlılığını manuel olarak mikroservicesler ile sağlamaktır. Anlık data girişinin ve düzenlemenin çok olmadığı Portallar gibi yapılarda, redis sunucuları arasında data tutarlılığı microservisler ile yapılabilir. Bu makale, tamamen size farklı bir bakış açısı sağlamak amacı ile yazılmıştır. “Olaylar gerçek ama kişiler tamamen hayal ürünüdür” :)
İsterseniz gelin önce örnek amaçlı .Net Core Mvc bir haber sayfasını hızlıca kodlıyalım. Uygulamanın tamamı Macbook macOS High Sierra üzerinde yazılacaktır.
1-) Öncelikle makinanızda yok ise Homebrew, aşağıdaki komut ile kurulur.
1 |
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" |
2-) Makinanızda Redis yok ise, aşağıdaki komut ile kurulur.
1 |
brew install redis |
3-) Aşağıdaki komut ile Redis Server ayağı kaldırılır.
1 |
redis-server |
4-) Redis Test: Aşağıdaki komut ile Redis Client’da, “Pong” cevabını almanız gerekmektedir.
1 2 |
redis-cli ping |
Sıra geldi alttaki komut ile .Net Core Mvc “RedisNews” projesini oluşturmaya.
1 |
dotnet new Mvc -o RedisNews |
.Net Core Proje içinde, Redis’in kullanılabilmesi için “ServiceStack.Redis.Core” aşağıdaki komut ile eklenir. Daha farklı birçok kütüphane vardır.
1 |
dotnet add package ServiceStack.Redis.Core --version 5.1.0 |
appsettings.json: Proje içinde redis ile ilgili tanımların yapıldığı config dosya.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "RedisConfig": { "host": "127.0.0.1", "port": 6379, "name": "master" }, "AllowedHosts": "*" } |
Projede kullanılan dotnet versiyonu aşağıdaki gibidir:
Artık .NetCore’da default olarak “https” ile yayım yapılmaktadır. Benim makinada neden ise, default gelen “https://localhost/5001” portunda hata oluşmaktadır. Belki aynı hata sizde de olur diye, kendimce çözümü aşağıda paylaştım:
Program.cs: UseUrls() methodu aşağıdaki gibi eklenir.
1 2 3 4 |
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseUrls("http://localhost:5000") .UseStartup<Startup>(); |
Models/News: Sayfaya basılacak News(Haber) modeli.
1 2 3 4 5 6 7 8 9 10 |
public class News { public int ID {get; set;} public string Title { get; set; } public string Detail { get; set; } public System.DateTime CreatedDate { get; set; } public System.DateTime? UpdatedDate { get; set; } public string Image { get; set; } public bool IsError { get; set; } } |
Controllers/HomeController(Constructor):
- HomeController constructor’da dependency injection ile “(IConfiguration configuration)” almaktadır.
- “_configuration”: Redis için gerekli host ve port bilgilerinin “appsettings.json“‘den okunabilmesi için tanımlanır.
- “conf” : Redis için kullanılan, genel tanımlı “RedisEndpoint” konfigürasyonudur.
- Proje ilk kez çalıştırılıyor ise, News model boş gelecektir. ==> “if (model == null)”.
- İlgili static model boş ise, constructor’da yani proje ilk ayağa kalkar iken, 1 kerelik örnek amaçlı manuel doldurulur. UpdatedDate, en başta null olarak atanmıştır. Son olarak “IsError” değeri default olarak “false” atanmıştı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 |
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using ServiceStack.Redis; using Microsoft.Extensions.Configuration; using testredis.Models; namespace testredis.Controllers { IConfiguration _configuration; RedisEndpoint conf; public static News model; public HomeController(IConfiguration configuration) { _configuration = configuration; conf = new RedisEndpoint() { Host = _configuration.GetValue<string>("RedisConfig:Host"), Port = _configuration.GetValue<int>("RedisConfig:Port")}; if (model == null) { model = new News() { ID=1, Title = "19 MAYIS TÜM YURTTA COŞKUYLA KUTLANDI", Detail = @"19 Mayıs Atatürk'ü Anma Gençlik ve Spor Bayramı tüm yurtta coşkuyla kutlandı. Mustafa Kemal Atatürk’ün beraberindeki 18 kişiyle 19 Mayıs 1919’da Samsun’a çıkmasıyla birlikte Milli Mücadele’nin meşalesi yakılmış, birkaç yıl içinde çağdaş Türkiye Cumhuriyeti doğmuştu.", CreatedDate = DateTime.Now, Image = "ondokuz.jpg", UpdatedDate=null, IsError = false }; } } } |
Controllers/HomeController(AddRedisCache()):
- “using (IRedisClient client = new RedisClient(conf))” : İlgili Redis Client tanımlanır.
- “var redisNews = client.Get<News>(cacheKey)” : İstenen haber (cacheKey) ile redisden çekilir.
- “client.Set<News>(cacheKey, news)” : Eğer redis’de hiç kayıt yok ise, yeni girilen “News” datası tanımlı redise’e atılır.
- “return redisNews” : Redisden çekilen News model, view’da kullanılmak üzere geri dönülür.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public async Task<News> AddRedisCache(News news, string cacheKey) { using (IRedisClient client = new RedisClient(conf)) { var redisNews = client.Get<News>(cacheKey); if(redisNews == null) { client.Set<News>(cacheKey, news); redisNews = news; } return redisNews; } } |
Not: Redis’de ilgili kayıt,”RedisNews” keyword’ü ile aşağıda görüldüğü gibi “string” olarak tutulmaktadır. İlgili data Redis’den “Get” methodu ile çağrılabilmektedir.
Index(): Aşağıda görüldüğü gibi Index sayfasına gitmeden ilgili data, “AddRedisCache()” methodu ile Redisden çekilmekte ve ilgili view’a gönderilmektedir.
1 2 3 4 5 |
public async Task<IActionResult> Index() { var data = await AddRedisCache(model, "RedisNews"); return View(data); } |
Index.cshtml: Aşağıda görüldüğü gibi ilgili “News” model, Redisden alınarak ekrana basılmıştır. Haber tarihi, var ise son güncelleme tarihi(UpdatedDate), yok ise yaratılma tarihi (CreatedDate) olarak bası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 |
@model News <link rel="stylesheet" href="~/css/table.css" /> <div class="container"> <div class="jumbotron"> <h2>@Model.Title</h2> </div> @{ var date=Model.UpdatedDate==null?Model.CreatedDate:Model.UpdatedDate; } <div align="right"><b>@date</b></div> <div> <div> <h3>@Model.Detail</h3> </div> <table class="table"> <tr> <td> <div> <img src="/images/@Model.Image" width="500px"> </div> </td> </tr> </table> </div> </div> |
wwwroot/css/table.css: Sayfada kullanılan custom Tablo css’i.
1 2 3 4 5 6 7 8 9 |
.table th { text-align: center; } .table { border-radius: 5px; width: 50%; margin: auto; float: none; } |
Şimdi sıra bu haberi güncelleyen, bir Admin sayfasının yapımında:
Startup.cs/Configure: .Net Core Mvc ortamında routing, “Index” ve “Admin” sayfaları için aşağıdaki gibi yapı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 |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCookiePolicy(); app.UseMvc(routes => { routes.MapRoute("admin", "admin/{*id}", defaults: new { controller = "Home", action = "Admin" }); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } |
Admin(): Admin sayfasına gitmeden önce güncellenecek data, Redis’den “AddRedisCache()” methodu ile çekilip ilgili View’a gönderilir. Burada kullanılan “IsError” parametresi, ilerde anlatılacak olan data tutarlılığının sonucunu tutmaktadır.
1 2 3 4 5 6 |
public IActionResult Admin(int? ID = 1, bool isError = false) { var data = AddRedisCache(model, "RedisNews"); data.IsError=isError; return View(data); } |
Admin.cshtml: Aşağıda görüldüğü gibi bir <form> kontroller yapılmıştır. Sayfa post yani güncelleme işleminde, “UpdateNews()” methoduna gidilir. Alınan “@model News” datasının, güncellenmesi amacı ile Html Input elementlere basılır. Tüm Html elementlerin “name” değerleri, “News” modelde ait oldukları propertyler ile aynı olmak zorundadır. Sayfanın başındaki “@if(Model.IsError)”, ilerde anlatılacak olan data tutarlılığının bozulması durumunda, ekranda gösterilecek olan uyarı mesajının bir koşuludur. İlgili hata mesaji ekrana, “<script>alert(“Kayıtda tutarsızlık bulunmaktadır!”);</script>” komutu ile ekrana basılır.
*Dikkat edilir ise düzenlenecek haber’in resim bilgisi, ayrıca bir hidden alanda tutulmaktadır.”<input type=”hidden” name=”Image” value=”@Model.Image”>”==> Aksi takdirde, güncellenecek haberin resmi değişmez ise, haberin eski image datası elde olmadığı için boş olarak güncellenecektir.
* Sayfada yazılan UpdatedDate tarihi de, ayrıca bir hidden alanda tutulmaktadır. “<input type=”hidden” name=”UpdatedDate” value=”@Model.UpdatedDate”>”
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 |
@model News @if(Model.IsError) { <script>alert("Kayıtda tutarsızlık bulunmaktadır!");</script> } <div class="container"> <div class="jumbotron"> <h2>Borsoft News® Admin</h2> </div> <div align="right"><b>@DateTime.Now</b></div> <form asp-controller="Home" asp-action="UpdateNews" method="post" enctype="multipart/form-data"> <div> <table class="table"> <tr> <td> <b>Haber Başlığı :</b> </td> <td> <input type="text" name="Title" id="Title" value="@Model.Title" size="100"> </td> </tr> <tr> <td> <b>Haber İçeriği :</b> </td> <td> <textarea rows="5" cols=120 name="Detail" id="Detail" >@Model.Detail</textarea> </td> </tr> <tr> <td> <b>Haber Resimi : </b> </td> <td> <input type="hidden" name="Image" value="@Model.Image"> <input type="hidden" name="UpdatedDate" value="@Model.UpdatedDate"> <img src="/images/@Model.Image" width="250px"><br><br> <input type="file" name="NewsImage" id="NewsImage"/> </td> </tr> </table> <div align="center"><button type="submit" class="btn btn-primary" style="width:50%" name="Image" id="Image">Güncelle</button></div> </div> </form> </div> |
Controllers/HomeController(UpdateNews()): Amaç, değiştirilen News datasının tüm Redis sunucularında güncellenmesidir.
- “CheckDataStable(client, news.UpdatedDate)” : Güncellenmek istenen datanın, makalenin hemen devamında anlatılacak olan, :) geçerli yani tutarlı bir kayıt olup olmadığı kontrol edilmiştir. Kayıdın tutarlı olmaması durumunda, kaydetme işlemi durdurulur ve “Admin()” action’ına “isError=true” şeklinde bir atama yapılarak yönlenilir. Böylece Admin sayfasına kaydetme işlemi olmadan tekrardan geri dönülür ve yukarıda görülen uyarı mesajı alınır.
- “(NewsImage != null && NewsImage.Length > 0)” : Eğer yeni bir resim yüklenmiş ise “wwwroot/images” altına tekil bir guid adı ile Upload edilir.
- “client.Set<News>(“RedisNews”, news)” : Bulunulan sunucudaki redis, güncellenen haberin(News), “UpdatedDate” alanı da set edilerek kaydedilir.
- “var newsJson = JsonConvert.SerializeObject(news)” : Son güncel “News” datası, string’e çevrilir.
- “client.PublishMessage(“News”, newsJson)” : Değişen “News” datasını diğer redis sunucularında da değişmesi için, ileride yazılacak olan “News” kanalını dinleyen, Redis Pub/Sub Microservices’ine “Publish” edilir.
- ” return RedirectToAction(“Index”)” : Tüm güncelleme ve bildirim işlemleri bitince, “Index” sayfasına yönlenilir.
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 |
public async Task<IActionResult> UpdateNews(News news, IFormFile NewsImage) { using (IRedisClient client = new RedisClient(conf)) { if (CheckDataStable(client, news.UpdatedDate)) { //Güncellenen Resim Yüklenir if (NewsImage != null && NewsImage.Length > 0) { var fileName = ContentDispositionHeaderValue.Parse(NewsImage.ContentDisposition).FileName.Trim('"'); var imageGuidName = Convert.ToString(Guid.NewGuid()); var ImageExtension = Path.GetExtension(fileName); var PureFileName = Path.GetFileNameWithoutExtension(fileName); var newFileName = PureFileName + '_' + imageGuidName + ImageExtension; news.Image = newFileName; //Yeni resmin Unique adı. fileName = Path.Combine("wwwroot/images") + $"/{newFileName}"; using (var stream = new FileStream(fileName, FileMode.Create)) { await NewsImage.CopyToAsync(stream); } } //1-)Var olan sunucudaki Redis Güncellenir. 2-) Microservis tetiklenir. news.UpdatedDate = DateTime.Now; client.Set<News>("RedisNews", news); var newsJson = JsonConvert.SerializeObject(news); client.PublishMessage("News", newsJson); } else { Console.WriteLine("Kayıt da tutarsızlık vardır!"); // BURADA Error Log'a yazılmalı ve SMS - EMAIL Atılmalıdır. return RedirectToAction("Admin", new { isError = true }); } } return RedirectToAction("Index"); } |
*Home/ CheckDataStable(): Geldik kaydın tutarlı olmasının ne anlama geldiğine: Diyelim ki biz haber admin sayfasına girdik ve kayıtla ilgili bilgileri değiştirmeye başladık. Bizim kaydetme işleminden önce, bir başka kullanıcı aynı haberi değiştirdi ve kaydetti. Bu durumda, bizim an itibari ile güncellemeye çalıştığımız haber eski ve yanlış bir haberdir. İşte bu durumda, güncelleme amaçlı çekilen datanın “UpdatedDate”‘i ile kaydetmeden hemen önce çekilen Haberin “UpdatedDate”‘i karşılaştırılır. En başta çektiğimiz güncelleme tarihi, aslında bir başka kullanıcı tarafından yeni bir güncelleme yapılarak değiştirilmiştir. Bu iki tarih birbiri ile örtüşmediği için, elimizdeki haber datası aslında artık eski bir datadır. Bu durumda, bir başka kişinin yaptığı değişikliğin ezilmemesi adına, yapılan işleme devam edilmez ve uyarı mesajı verilir. Bu kontrol, başka sunucular üzerinden aynı haberin değiştirilmesi durumu için de geçerlidir. Çünkü Redis Pub/Sub ile ilgili kaydın, UpdatedDate’i de güncellenmiştir.
1 2 3 4 5 6 7 8 9 |
public bool CheckDataStable(IRedisClient client, DateTime? updatedDate) { var redisNews = client.Get<News>("RedisNews"); if (redisNews != null && redisNews.UpdatedDate.ToString() != updatedDate.ToString()) { return false; } return true; } |
RedisNewsServices: Yeni bir.NetCoreConsole Applicationdır. Amacı “News” kanalını dinleyip, güncellenen News datasını, diğer Redis sunucularında da “UpToDate” yani güncel hale getirmektir. Aslında bir Microservisden başka birşey değildir. İlk olarak aşağıdaki kütüphaneler, projeye eklenir.
1 2 |
dotnet add package ServiceStack.Redis.Core --version 5.1.0 dotnet add package Newtonsoft.Json |
Program.cs:
- “using (sub = client.CreateSubscription())” : İlgili “News” kanalını dinleyecek Subscriber yaratılır.
- “sub.OnMessage += (channel, news) =>” : Mesaj gelmesi durumunda, dinlenen (News)channel’ı ve alınacak News datası parametrik olarak tanımlanır.
- “News _news = JsonConvert.DeserializeObject<News>(news)” : String olarak gelen data, deserialize edilerek “News” sınıfına dönüştürülür.
- Sıra geldi güncellenen News datasının tüm Redis sunucularında da güncellemeye. İlgili tüm redis sunucularına IPleri ile erişilip güncellenir.
- “var conf2 = new RedisEndpoint() { Host = “10.211.55.9”, Port = 6379 }” ==> “1. Redis sunucusuna erişilecek config tanımlanır.” Bu ilgili local makina da olabilir.
- “using (IRedisClient clientServer = new RedisClient(conf2))” ==> Tanımlı Redis’e bağlanılır.
- “clientServer.Set<News>(“RedisNews”, _news)” ==> Gelen “News” datası “RedisNews” keyword’ü ile güncellenir.
- Bu işlemlerin aynısı bir sonraki sunucu içinde yapılır.
Not: İlgili Redis sunucularında ==>”redis-cli” yani client tarafında ==> “CONFIG SET protected-mode no” komutu çalıştırılarak, uzaktan erişim izninin verilmesi gerekmektedir.
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 |
using System; using ServiceStack.Redis; using Newtonsoft.Json; namespace redisnewsservices { class Program { static void Main(string[] args) { var conf = new RedisEndpoint() { Host = "127.0.0.1", Port = 6379 }; Console.WriteLine("Services Start..."); using (IRedisClient client = new RedisClient(conf)) { IRedisSubscription sub = null; using (sub = client.CreateSubscription()) { sub.OnMessage += (channel, news) => { try { News _news = JsonConvert.DeserializeObject<News>(news); Console.WriteLine(_news.Title); //Güncellenecek 1. Redis Server'ı var conf2 = new RedisEndpoint() { Host = "10.211.55.9", Port = 6379 }; using (IRedisClient clientServer = new RedisClient(conf2)) { clientServer.Set<News>("RedisNews", _news); } //Güncellenecek 2. Redis Server'ı var conf3 = new RedisEndpoint() { Host = "192.168.1.234", Port = 6379 }; using (IRedisClient clientServer2 = new RedisClient(conf3)) { clientServer2.Set<News>("RedisNews", _news); } } catch (Exception ex) { Console.WriteLine(ex.Message); } }; sub.SubscribeToChannels(new string[] { "News" }); } } Console.ReadLine(); } } } |
Böylece değişen haber datası tüm redis sunucuları içinde güncellenmiş olunur. Sayfaya yeni giren ya da ilgili pencereyi güncelleyen client, en son güncel haber bilgisini, kendi server’ına atanmış Redis makinası üzerinden almış olacaktır.
Geldik bir makalenin daha sonuna yeni bir makalede görüşmek üzere hoşçakalın.
Source Code : https://github.com/borakasmer/RedisDataConsistencyWithMicroservices
Selamlar hocam,
Öncelikle severek takip ediyoruz :)
Gerçekten harika işlere imza atıyorsunuz.
Videonuzu daha önce izlemiştim ama bugün tekrar izlemem gerekti. üzerine istişare edelim diye buradan yazıyorum. Haddimi asmışsam affola :)
Her iki redis sunucumuzu master yaptığımız zaman bir kısımdaki redis kapanırsa sistem durma noktasına gelebilir. Durum böyle olmaması adına hazır elimizde 2 redis sunucusu varken 1 redis sunucusunu Master, 1 redis sunucusunuda slave yaparsak bu sorunu minimize hale getirebiliriz. Önce Sentinel ip adresine istek atar master sunucuyu öğreniriz. Bir sorun olması durumunda Sentinel bizim yerimize master sunucuyu ayarladığı için herhangi bir işlem yapmadan kesintiyi minimize hale getirebiliriz diye düşünüyorum. Tabii Video epey eski olduğu için fikrinizde değişmiş olabilir :) Ben videoadaki düşüncelerinize binaen bu yorumu yapıyorum. Tekrar sevgiler.