NodeJs Üzerinde Redis Kullanımı ve Refactoring
Selamlar,
https://youtu.be/Uvg7Sp7TVaQ
Bu makale, NodeJS ve Angular Üzerinde Authentication ve Security makalesinin devamıdır. MongoDB’den çekilen data, ilk etapta Redis Memory bir Cache’a atılacaktır. Amaç, NodeJs üzerinde performans’ın arttırılmasıdır. Gerçekte MongoDB kullanılan bir projede, ayrıca Redis kullanmak çok da mantıklı değildir. Ama örnek amaçlı bu projede, DB’nin mongoDB değil de, Sql veya Oracle gibi relational bir DB olduğu farz edilmektedir. Servise çoklu bir data isteği geldiğinde, eğer listelenecek datada anlık değişim çok sık değil ise, araya Distributed bir Cache’in konması, gayet mantıklıdır.
Bu makalede, NodeJs üzerinde Redis nasıl kullanılıyor detaylıca inceleyeceğiz. Redis’i ilk kez duyanlar için detaylı makaleye, buradan erişebilirsiniz. NodeJs üzerine Redis kütüphanesi, aşağıdaki komut ile kurulur.
1 |
npm install redis |
Var olan NodeJs projeye, Redis aşağıdaki komut ile eklenir.
1 2 |
var redis = require('redis'); var client = redis.createClient(); //creates a new client |
Redis client’a başarı ile bağlanma durumunda veya bağlanamama durumunda, ilgili hata aşağıdaki gibi console yazdırılır.
1 2 3 4 5 6 7 |
client.on('connect', function () { console.log('Redis client bağlandı'); }); client.on('error', function (err) { console.log('Redis Clientda bir hata var ' + err); }); |
Uygulama çalıştırılma anında, aşağıdaki gibi bir mesaj ile karşılaşılır.
service/people : Örnek amaçlı sadece, ana sayfanın başlangıcında çekilen tüm çalışan listesi, öncelikle araya konan Redis cache tarafında arandıktan sonra, eğer bulunamaz ise MongoDB’den çekilip ve Redis’e doldurulur. Ve çekilen Person Listesi, geriye dönülür. Eğer istenen data Redis üzerinde var ise, tüm data Redis’den çekilip geriye dönülür. Böylece hem DB üzerindeki yük alınmış, hem de çok daha hızlı ve performanslı sonuç geriye dönülmüş olunur.
Image Source : https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2015/10/1444132453algorithm-basic.png
- “if (redisClient.connected) {” : Redis’e connect olunup olunmadığına bakılır.
- “redisClient.keys(‘User*’, function (err, keys) {” : Her bir person kaydı, “User” keywordü ile başlıyan bir key ile Redis’e atılır. Doğal olarak, “User*” pattern’i ile yazılan query’de Redis’e atılan tüm person dataların sadece Keyleri çekilir.
- “if (keys.length > 0) {/”: Redis’de, User ile başlıyan kayıt var ise işleme devam edilir.
- “var personList = [];” : Çekilecek datanın, atılacağı dizidir.
- “for (var i = 0, len = keys.length; i < len; i++) {” : Redis üzerindeki, tüm “User” keyword’ü ile başlıyan keyler, tek tek gezilir.
- “redisClient.get(keys[i], function (err, user) {” Her bir key’e ait user kaydı, Redis’den çekilir.
- “personList.push(JSON.parse(user))” : Çeklen her bir person kaydı”personList []” dizisine atılır.
-
“if (personList.length == keys.length – 1) { res.send(personList); }” Burası önemli. Burada durup, biraz düşünelim. Javascript her zaman senkron ve tek thread olarak çalışır. Ama burada, Redis’den her bir user için ilgili kaydın çekilmesi ==>”redisClient.get(keys[i]” zaman almaktadır. İlgili kayıdın Redis’den gelmesi esnasında, kod çalışmaya devam etmektedir. Eğer ilgili koşul konmasa idi, “res.send(personList)” listesi boş olarak gönderilirdi. İşlem, for döngüsü içinde tamamlandıktan sonra, ilgili koşul sağlanmaktadır. Bu nedenle, Redis’de bulunan tüm kayıtların personList‘e atılıp atılmadığı kontrol edilmiş, tüm kayıdın atılması durumunda person[] listesi geri dönülmüştür.
- “else {“: İlgili kayıdın Redis’de olmaması durumudur, bu koşul çalıştırılır.
- “Human.find(function (err, doc) {” : Redis cachede ilgili kaydın olmaması durumunda ,MongoDB’den Users collection’ına ait tüm dökümanlar çekilir.
- “counter += 1” : Her bir kayıt için, Redis’de kullanılmak üzere atanmış bir ID değeridir.
- “item.fullName = item._doc.name.first + ‘ ‘ + item._doc.name.last”: MongoDB’de olmayan bir kolon, manuel olarak oluşturulur ve dönen modele dinamik olarak eklenir.
- “var userName = ‘User’ + counter” : Redis’e atılacak her bir kayıt için, dinamik key oluşturulur.
- “redisClient.get(userName, function (err, user) {” : Redis’de son bir kere aynı key’e ait, kayıt var mı diye kontrol edilir.
- “if (user == null) {” : Eğer Redis’de ilgili kayıt yok ise, işleme devam edilir.
- “var data = JSON.stringify(item._doc);” : MongoDB’den dönen döküman, Redis’e atılmak üzere string Json’a çevrilir.
- “redisClient.set(userName, data, function (err, res) { });” : Çekilen döküman Redis’e, yaratılan dinamik key ile atılır. Örnek “User1“
- ” res.send(doc);” MongoDB’den çekilen tüm data, geri dönülür.
- ” else {” : Eğer Redis’e bağlanılamıyor ise, bu koşul çalışır.
- “Human.find(function (err, doc) {” : Tüm Person kaydı Redis’e bağlanılamadığı için, mongoDB’den çekilerek 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 |
app.get("/people", token, function (req, res) { //res.send("Günaydın Millet"); if (redisClient.connected) { //Redis Search redisClient.keys('User*', function (err, keys) { if (err) return console.log(err); if (keys.length > 0) {//Redis'de ilgi kayıtlar var ise ordan gelir. var personList = []; for (var i = 0, len = keys.length; i < len; i++) { //console.log("i:" + i); redisClient.get(keys[i], function (err, user) { //console.log(user); personList.push(JSON.parse(user)); //console.log('len:' + keys.length) if (personList.length == keys.length - 1) { res.send(personList); //console.log(personList); } }) } } else { //Read All Data From MongoDB. There is no record on Redis. var counter = 0; Human.find(function (err, doc) { doc.forEach(function (item) { counter += 1; item.fullName = item._doc.name.first + ' ' + item._doc.name.last; //Write All Data To Redis var userName = 'User' + counter; redisClient.get(userName, function (err, user) { if (user == null) { var data = JSON.stringify(item._doc); console.log(data); redisClient.set(userName, data, function (err, res) { }); } else { console.log(JSON.parse(user).name.first); } }); //End Redis Write }); res.send(doc); }) } }); //--- } else //If there is no connection to Redis, get all data from MongoDB. { Human.find(function (err, doc) { doc.forEach(function (item) { item.fullName = item._doc.name.first + ' ' + item._doc.name.last; }); res.send(doc); }) } }) |
Redis Update (Refactoring):
Şimdi gelin bir kaydın güncellenmesini, sadece MongoDB’den değil Redis’de de yapalım. Var olan sistemle bunu yapmak çok zordur. Nedeni, Redise atanan herbir kayıdın keyinin, tabloda olmayan “User*” şeklinde atanmasıdır. Doğal olarak güncelenen kayıdın Redisden bulunması için, ilgili tüm kayıtlar gezilmesi ve Unique olan “UserName“‘e göre bulunup güncellenmesi gerekmektedir. Bir diğer yol da, Lua Script kullanılarak, Redis’deki User Json içerisinde query ile igüncellenecek datanın çekilmesidir. 2’si de yüksek trafikte tercih edilmemesi gereken, zahmetli ve sunucuya büyük yük getiren yöntemlerdir. Bunun yerine gelin, yukarıdaki kodda refactoring yapalım.
1-)Herbir kaydın Redis Key’inin “User:Username” olarak değiştirelim. Böylece seçilen bir kayda, redis üzerinden ayrıca bir işlem yapılmadan erişilebilecektir.
2-)Tüm User* kelimesi ile başlıyan kayıtlar, Redisden aşağıdaki kod ile silinir.
1 |
redis-cli --scan --pattern 'User*' | xargs redis-cli DEL |
3-) Redis’de kayıt yok ise, MongoDB’den çekilip ilgili datanın Redise atandığı kısım, aşağıdaki gibi değiştirilir.
- “//counter += 1;” : Counter değişkenine ihtiyaç olmadığı için kaldırılır.
- “//var userName = ‘User’ + counter;” : Redis keyin “User1” şeklinde tanımı kaldırılır.
- “var userName = ‘User:’ + item._doc.username;” : Yeni kayıt “User:bigbear866” gibi olacak şekilde tanımlanır.
Not: Redis’de Key atamalarında 2 kelime arasına “:” işaretinin konması, yeni bir folder yapısı olarak anlaşılır. Örneğin yukarıdaki resimde görüldüğü gibi, tüm kayıtlara ait keylerin başındaki “User“, Redis tarafında ana folder olarak atanmıştır. Ve oluşturulan tüm keyler, bu “User” folderın altına toplanmıştır. Bu da Redis’de okumanın kolaylaşmasına ve aynı dökümana ait kayıtların tek bir gurup altında toplanmasına imkan sağlamıştır.
service.js/People:
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 |
else { //Read All Data From MongoDB. Redis'de kayıt yok. var counter = 0; Human.find(function (err, doc) { doc.forEach(function (item) { //counter += 1; item.fullName = item._doc.name.first + ' ' + item._doc.name.last; //Write All Data To Redis //var userName = 'User' + counter; var userName = 'User:' + item._doc.username; redisClient.get(userName, function (err, user) { if (user == null) { var data = JSON.stringify(item._doc); console.log(data); redisClient.set(userName, data, function (err, res) { }); } else { console.log(JSON.parse(user).name.first); } }); //End Redis Write }); res.send(doc); }) } |
Şimdi sıra geldi güncellenen kaydın, Redis tarafında da değiştirilmesine:
Eğer Redis tarafında güncelleme işlemi yapılmaz ise, sadece MongoDB’de güncellenen kayıt sayfa yenilendiğinde, Redisden dolacağı için eski halini alır. Taa ki Redis Cache TimeOut olana ve ilgili kayıdın Redis’den değil de, mongoDB’den çekilerek doldurulmasına kadar devam eder.
service.js/updatePeople: Aşağıdaki methodda görüldüğü gibi, MongoDB’de güncellenen user, Redis’de de güncellenmesi için aşağıdaki adımlar izlenir.:
- “if (redisClient.connected) {“: Redis’e bağlanılmış mı diye bakılır.
- “var userName = ‘User:’ + updatePerson.username;” : Username, yeni sisteme göre oluşturulur.
- “var data = JSON.stringify(updatePerson)” : Güncellenen Person data, Redis’e atmak için string’e çevrilir.
- “redisClient.set(userName, data, function (err, res) { })” : İlgili UserName’e göre güncellenen user data, direk redisdeki kayıdın üzerine ezilir. Böylece MongoDB’de güncellenen data, Redis’de de güncellenmiş olunur.
Redisin güncellenmesi ile, artık sayfa yeniden yüklense bile, datanın son halinin Redis’e atılmasından dolayı, değişen son data ekrana 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 |
app.post('/updatePeople', token, async (req, res) => { console.log("req.username : " + req.body.username); try { var updatePerson = new Human(req.body); const person = await Human.findOne({ username: updatePerson.username }); await person.updateOne(updatePerson); //Redis Update if (redisClient.connected) { var userName = 'User:' + updatePerson.username; var data = JSON.stringify(updatePerson); redisClient.set(userName, data, function (err, res) { }); } //Redis Update Finish-------------- /* return res.send("succesfully saved"); */ /* return true; */ return res.status(200).json({ status: "succesfully update" }); } catch (error) { res.status(500).send(error); } }) |
service.js/insertPeople: Aşağıdaki methodda görüldüğü gibi, MongoDB’ye kaydedilen user’ın, Redis’e de kaydedilmesi için aşağıdaki adımlar izlenir.:
- “if (redisClient.connected) {“: Redis’e bağlanılmış mı diye bakılır.
- ” var userName = ‘User:’ + req.body.username;” : Redis’de key olarak tanımlanan custom Username, yeni sisteme göre oluşturulur.
- “var data = JSON.stringify(person)” : Insert edilen Person data, Redis’e kaydedilmek için string’e çevrilir.
- “redisClient.set(userName, data, function (err, res) { })” : İlgili UserName’e göre yeni kaydedilen user data redise eklenir. Böylece MongoDB’de yeni kaydedilen data, Redis’e de atılmış olunur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
app.post('/insertPeople', token, async (req, res) => { console.log("req.body : " + req.body.username); console.log("req.body.last : " + req.body.name.last); try { var person = new Human(req.body); var result = await person.save(); //Redis Insert if (redisClient.connected) { var userName = 'User:' + req.body.username; var data = JSON.stringify(person); redisClient.set(userName, data, function (err, res) { }); } //-------------- res.send(result); } catch (error) { res.status(500).send(error); } }) |
service.js/deletePeople: Aşağıdaki methodda görüldüğü gibi, MongoDB’den silinen user’ın, Redis’den de kaldırılması için aşağıdaki adımlar izlenir:
- “if (redisClient.connected) {“: Redis’e bağlanılmış mı diye bakılır.
- ” var userName = ‘User:’ + req.body.username;” : Redis’de key olarak tanımlanan custom Username, yeni sisteme göre oluşturulur.
- ” redisClient.del(userName);” : İlgili UserName’e göre, Redis’de kayıtlı olan user silinir. Böylece MongoDB’den silinen kayıt, Redis’den de silinmiş olunur.
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 |
app.post('/deletePeople', token, (req, res) => { console.log("req.username : " + req.body.username); try { var deletePerson = new Human(req.body); Human.findOneAndRemove({ username: deletePerson.username }) .then(function (doc) { if (doc) { //return res.send("succesfully delete"); //Redis Update if (redisClient.connected) { var userName = 'User:' + req.body.username; redisClient.del(userName); } //----------------- return res.status(200).json({ status: "succesfully delete" }); } else { return res.send("no document found!"); } }).catch(function (error) { throw error; }); } catch (error) { res.status(500).send(error); } }) |
Bu makalede, NodeJS üzerinde Redis entegrasyonunu hep beraber inceledik. DB üzerine gelen yükün, distributed bir cache kullanılarak, Redis’e yüklenilmesi esas makalenin konusudur. Kayıt ilk kez alınıyor ise, data MongoDB’den çekilip, Redis cache doldurulmaktadır. Kayıt 2. kez çağrılındığında, artık tüm data Redis’den alınır. Böylece DB üzerindeki yük, kaldırılmış olunur. Ayrıca kayıtlardan biri güncellenir ise ilgili data, hem MongoDB’de hem de, Redis üzerinde güncellenerek, sayfanın yenilenmesi durumunda ilgili kayıdın son halinin gelmesi sağlanır.
Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Sources:
Bora hocam selamlar, öncelikler makaleniz için elinize sağlık.
Redis tarafı ile ilgili aşağıdaki gibi bir durum yaşıyorum ve Redis’i bir canlı ortamda performanslı kullanmak için bazı kavramlara dikkat etmek gerekiyormuş :) Bu konuda önerilerinizi alabilir miyim?
Redis’i yüksek trafikli sitelerde hızlı bir şekilde cache üzerinden merkezi bir yerden cevap vermek için kullanıyoruz, buraya kadar her şey tamam.
Fakat Redis üzerinde tuttuğumuz json ya da byte tipindeki dataların boyutuyla ilgili optimum değer sizce nedir nasıl olmalıdır?
Örneğin tamamen dinamik ayarlardan oluşan bir portal uygulamasında sayfa geçişlerinde ve ya post aksiyonlarında sürekli role vb kontrollerin yapılması ve ya sayfa üzerindeki dinamik bileşenlerin ayarlarıyla ilgi bilgileri Redis üzerinde tutmak/okumak mantıklı mıdır?
Konu biraz daha derinleştiğinde Azure üzerinde Redis kullanılacağı durumlarda bu sefer region kavramı ortaya çıkıyor, bu sefer en yakın lokasyondan data çekmek gerekiyor ve bazı durumlarda timeout oluşabiliyor.
Özellikle büyük cache datalarında timeout hataları sıkça ortaya çıkabiliyor, bu durumda bu tarz konfigurasyon datalarını redis üzerinde tutmak yerine başka alternatifler var mıdır?
Bu konuyla ilgili şöyle bir aksiyon alıyorum; NLB arkasındaki 4 node üzerinde çalışan .net web uygulaması üzerinde cache’te tutacağım dataları inmemory olarak tutup, eğer konfigurasyon datasında bir değişiklik var ise pub/sub ile 4 makine üzerinde bir trigger işlemiyle cache temizletip dataları tekrar node bazlı cache’e atıyorum. Yönetilebilirlik açısından kötü ve RAM üzerinde data tuttuğu için ram üzerindeki kapasiteyi düşürüyor fakat performans olarak en hızlı bu çözüm bu oldu, bunun yerine önerebileceğiniz daha mantıklı bir çözüm var mıdır?
Selamlar Hüseyin,
Öncelikle teşekkürler. Redis bir Memory DB’dir. DB burada kilit kelime:) Yoksa bir key’e atılan upuzun bir json container’ı değil. Kısaca ver optimizasyonu yapılması ilgili datalar mesela Hash tables’da tutulabilir. Config ayarlarının düzgün yapılması ve Redis’in manuel değil, scale edilip örnek en az 1 Master 2 Slave şeklinde tek sayı olacak şekilde 3-5-7 gibi arttırılması gerekir.
İyi çalışmalar.
Ne zamandır bookmark list-de kalmıştı. Şimdi güzel oldu.
Teşekkürler