Налаштування механізму кешування на основі Redis у вашому проекті Node.js (Частина 02)

Кешування в Node.js

У попередній статті ми виконали всі необхідні установки та налаштування. Тепер перейдемо до основної частини.

pic

кешування з node.js та redis

У цьому посібнику ми використаємо прототипне наслідування JavaScript для впровадження ефективного механізму кешування, який підходить для застосувань на рівні продакшн.

Наш підхід

  1. Перевизначення mongoose.Query.prototype.exec:
    Ми перевизначимо функцію exec, щоб виконати кастомну логіку перед запитом до бази даних MongoDB.
  2. Додавання функції кешування:
    Ми розширимо існуючий прототип, додавши нову функцію, що буде відповідати за кешування конкретних запитів.

Якщо ви не знайомі з mongoose.Query.prototype.exec та його призначенням, ось коротке пояснення. Однак, якщо ви вже розумієте його функціональність, можете пропустити цей розділ.

Розуміння mongoose.Query.prototype.exec

exec використовується всередині mongoose для виконання запитів до бази даних MongoDB. Перевизначивши його, ми можемо перехопити будь-яке виконання запиту та інтегрувати додаткову функціональність, для якої ми будемо використовувати кешування.

Почнемо.

Оскільки ми дотримуємося правильної структури папок, для кожної функціональності будемо використовувати окрему папку. Отже, для додавання логіки кешування використовуватимемо папку services, а для маршрутів — папку routes, як зазвичай.

У головному файлі: /app.js

# ви можете додавати додаткові елементи самостійно, я просто намагаюся залишити все чистим

const express = require("express");  
const app = express()  

const blogRoutes = require("./routes/blogRoutes.js")  

app.use("/api/blogs", blogRoutes)  

app.listen(3000, ()=> console.log("app listening to 3000"))

У файлі: /routes/blogRoutes.js

Припустимо, що в нас є кілька API, які будуть отримувати та відправляти блоги.
Іншим способом, скажімо, це блогова аплікація

const express = require("express")  
const router = express.Router()  

// створення нового поста в блозі  
router.post("/create", async (req, res, next) => {  
 const newBlog = new Blog({  
 title,  
 content   
 })   
 await newBlog.save();  
 return res.json(newBlog);  
})  

// отримання всіх блогів  
router.get("/getAll", async (req, res, next) => {  
 const blogs = await Blog.find({})  
 return res.json({blogs});  
})  

// отримати блог за id  
router.get("/blog/:id", async (req, res, next) => {  
 const blogId = req.params.id;  
 const blog = await Blog.findById(blogId)  
 return res.json({blog});  
})

У файлі: /services/cacheManager.js

 // імпортуємо необхідні модулі  
const mongoose = require("mongoose")  

// налаштовуємо redis  
const redis = require("redis")  

 // переконайтесь, що redis-сервер запущено перед виконанням у терміналі ubuntu:   
// $ redis-server  

const client = await redis.createClient({  
 host: "127.0.0.1",  
 port: 6379  
})  

(async () => {  
 try{  
 await client.connect();  
 }catch(error){  
 throw Error(`err: ${error.message}`)  
}  
})()

Одна важлива примітка: Читайте документацію, написану в коді, оскільки я намагався зробити все якомога простішим для розуміння того, що насправді відбувається.

Створення нового методу mongoose.Query.prototype.cache

 // реалізація логіки кешування  

const exec = mongoose.Query.prototype.exec; // створення копії функції exec  

// створення нового методу cache для екземпляра mongoose Query  
mongoose.Query.prototype.cache = async function(options = {}) {  
 const this.useCache = true; // включення кешування  

 // створення hashKey, щоб ми могли зробити вкладену пару ключ-значення для кожного індивідуального запиту користувача  
 const this.hashKey = JSON.stringify(options.key || "");   
 return this;  
}

Перевизначення mongoose.Query.prototype.exec

 // Перевизначення методу exec для екземпляра mongoose Query   
mongoose.Query.prototype.exec = async function (){  

 // перевірка, чи увімкнено кешування для поточного запиту  
 // якщо ні, виконуємо оригінальну функцію 'exec' mongoose   
 if(!this.hashKey){  
 return exec.apply(this, arguments);   
 }  

 // СТВОРЕННЯ КЛЮЧА (методи пояснені нижче)  
 const key = JSON.stringify( Object.assign({}, this.getQuery(), {collection: this.mongooseCollection.name}) )  

 // ПЕРЕВІРКА, ЧИ ВЖЕ БУВ ВИКОНАНИЙ ЦЕЙ ЗАПИТ  
 const cacheValue = await client.hGet(this.hashKey, key);  

 // ЯКЩО ВЖЕ ВИКОНАНИЙ, ПОВЕРТАЄМО ЗАСТЕРЕЖЕНЕ РЕЗУЛЬТАТ НЕГАЙНО  
 if(cacheValue){  

 const doc = JSON.parse(cacheValue);  
 // оскільки doc — це JSON, нам потрібно модель mongoose у відповіді, а також перевірка, чи є відповідь масивом об'єктів  
 return Array.isArray(doc)  
 ? doc.map(d => new this.model(d))  
 : new this.model(doc);  
 }  

 // ЯКЩО НІ, ВИКОНУЄМО ОРИГІНАЛЬНИЙ ЗАПИТ MONGOOSE ДО MONGODB  
 const response = await exec.apply(this, arguments);  

 // ЗБЕРІГАЄМО РЕЗУЛЬТАТ У КЕШІ (REDIS)  
 await client.hSet(this.hashKey, key, JSON.stringify(response), "EX", 3600); // час життя кешу є необов'язковим  

 // ПОВЕРТАЄМО РЕЗУЛЬТАТ  
 return response;  


}

Деякі утиліти:

  1. this.getQuery: повертає об'єкт поточного запиту, що виконується до mongoDB
  2. this.mongooseCollection.name: повертає назву колекції, для якої виконується запит (наприклад: Blog, User і т.д.)
  3. Object.assign({}, {…}): створює новий об'єкт і об'єднує інший об'єкт в нього.

Тепер давайте оновимо обробники маршрутів:

У нашому методі mongoose.Query.prototype.cache ми очікуємо options = {} як параметр функції, який буде передаватися з методів запитів, що до нього прикріплені.

Оновлений обробник blogRoute.js

 // отримання всіх блогів  
router.get("/getAll", async (req, res, next) => {  

 // оновлення тут......
// ви можете використовувати будь-що як ключ,  
 const blogs = await Blog.find({})  
 .cache({key: req.user.id })   

 return res.json({blogs});  
}

Ми можемо прикріпити .cache( {….} ) до нашого запиту, оскільки ми створили новий метод для екземпляра Query нашого mongoose у cacheManager.js.

Тепер підключимо цей cacheManager у app.js, щоб оновити модифікацію глобально.

Оновлений app.js

const express = require("express");  
const app = express()  

// підключаємо cacheManger.js глобально  
require("../services/cacheManager.js")  

const blogRoutes = require("./routes/blogRoutes.js")  

app.use("/api/blogs", blogRoutes)  

app.listen(3000, ()=> console.log("app listening to 3000"))

Тепер, коли ми реалізували логіку для налаштування та отримання кешу, давайте розглянемо, як оновлювати кеш, коли нові дані додаються або змінюються в базі даних.

Наприклад, уявімо, що ми кешуємо велику кількість постів з блогу і подаємо їх безпосередньо з кешу. Якщо до бази даних додаються нові пости, кешовані дані застарівають, оскільки вони містять тільки раніше збережені записи.

Щоб наша аплікація завжди подавала найновіші дані, нам потрібно перевіряти та оновлювати кеш щоразу, коли база даних оновлюється.

Почнемо з cacheManager.js, де ми експортуємо функцію, яка очищатиме кеш для конкретного користувача конкретного запиту:

module.exports = {  
 clearCache(hashKey){  
 client.del(JSON.stringify(hashKey))  
 }  
}

Тепер давайте створимо проміжне ПЗ (middleware), яке виконуватиме цю функцію щоразу, коли буде створено новий пост або оновлено базу даних.

const {clearCache} = require("./services/cacheManager.js");  

module.exports = async (req, res, next) => {  
 await next();  

 await clearCache(req.user.id)  
}

Ось важлива концепція, чому я спочатку виконував next(), а потім функцію clearCache.

Express не дозволяє виконувати проміжне ПЗ після обробника запиту. Однак, щоб очищати кеш тільки після успішного виконання обробника запиту (наприклад, POST), ми використовуємо await next(). Це забезпечує завершення операції в базі даних перед викликом clearCache. Якщо під час оновлення бази даних виникає помилка, кеш залишатиметься незмінним, що дозволить уникнути передчасного видалення кешу.

Оновлений обробник blogRoute.js

const cleanCache = require("./middlewares/clearCache.js")  

// отримання всіх блогів  
router.get("/getAll", cleanCache ,async (req, res, next) => {  
// оновлення тут......
// ви можете використовувати будь-що як ключ,  
 const blogs = await Blog.find({})  
 .cache({key: req.user.id })   
 return res.json({blogs});  
}

Остаточна структура cacheManager.js

 // імпортуємо необхідні модулі  
const mongoose = require("mongoose")  

// налаштовуємо redis  
const redis = require("redis")  

 // переконайтесь, що перед запуском сервер redis запущений у терміналі Ubuntu:   
// $ redis-server  

const client = await redis.createClient({  
 host: "127.0.0.1",  
 port: 6379  
})  

(async () => {  
 try{  
 await client.connect();  
 }catch(error){  
 throw Error(`err: ${error.message}`)  
}  
})()  

// реалізація логіки кешування  

const exec = mongoose.Query.prototype.exec; // створення копії функції exec  

// створення нового методу cache для екземпляра mongoose Query  
mongoose.Query.prototype.cache = async function(options = {}) {  
 const this.useCache = true; // активуємо кешування  

 // створення hashKey, щоб мати можливість створювати вкладену пару ключ-значення для кожного окремого запиту користувача  
 const this.hashKey = JSON.stringify(options.key || "");   
 return this;  
}  


// Перевизначення функції exec для екземпляра mongoose Query   
mongoose.Query.prototype.exec = async function (){  

 // перевіряємо, чи активовано кешування для поточного запиту  
 // якщо ні, виконуємо оригінальну функцію mongoose 'exec'   
 if(!this.hashKey){  
 return exec.apply(this, arguments);   
 }  

 // СТВОРЕННЯ КЛЮЧА (методи описані нижче)  
 const key = JSON.stringify( Object.assign({}, this.getQuery(), {collection: this.mongooseCollection.name}) )  

 // ПЕРЕВІРЯЄМО, ЧИ БУВ ВИКОНАНИЙ ЦЕЙ ЗАПИТ РАНІШЕ  
 const cacheValue = await client.hGet(this.hashKey, key);  

 // ЯКЩО ВЖЕ ВИКОНАНО, ПОВЕРТАЄМО ЗАСТОСОВАНИЙ РЕЗУЛЬТАТ НЕГАЙНО  
 if(cacheValue){  

 const doc = JSON.parse(cacheValue);  
 // оскільки doc є JSON, нам потрібно створити модель mongoose для відповіді і перевірити, чи є відповідь масивом об'єктів  
 return Array.isArray(doc)  
 ? doc.map(d => new this.model(d))  
 : new this.model(doc);  
 }  

 // ЯКЩО НІ, ВИКОНУЄМО ОРИГІНАЛЬНИЙ ЗАПИТ MONGOOSE ДО MONGODB  
 const response = await exec.apply(this, arguments);  

 // ЗБЕРІГАЄМО РЕЗУЛЬТАТ У КЕШ (REDIS)  
 await client.hSet(this.hashKey, key, JSON.stringify(response), "EX", 3600); // час життя кешу - необов'язковий  

 // ПОВЕРТАЄМО РЕЗУЛЬТАТ  
 return response;  

}

Гаразд, думаю, ми майже завершили завдання кешування запитів і оновлення даних. Розумію, що це було трохи складно, але ви можете спробувати простіші методи, наприклад, просто використовуючи Redis для читання документів.

Сподіваюся, ви дізналися щось нове!!
Не соромтеся залишати відгук нижче і підписуватися для отримання ще корисних матеріалів у майбутньому
Щасливого хакінгу 🙂

Частина 01

Перекладено з: Setting Up Your Redis Powered Caching Mechanism in Your Node Js Project (Part 02)

Leave a Reply

Your email address will not be published. Required fields are marked *