Mongoose представляет специальную ODM-библиотеку (Object Data Modelling) для работы с MongoDB, которая позволяет сопоставлять объекты классов и документы коллекций из базы данных.
Redis (Remote Dictionary Server)- это быстрое хранилище данных типа «ключ‑значение» в памяти, активно используемое в разработке с целью повышения производительности сервисов
В рамках данного гайда мы рассмотрим связку Mongoose + Redis и посмотрим, как обеспечить максимально удобное взаимодействие между ними
Шаг 1. Установка пакетов
yarn add redis mongoose
Шаг 2. Конфигурация Redis.
const redisUrl = process.env.REDIS_CACHE_URL
const client = redis.createClient(redisUrl)
client.get = util.promisify(client.get)
Шаг 3. Формирование ключа
Одна из важных задач кэширования - составление ключа, под которым кэшировать данные. В контексте mongoose мы можем очень эффективно использовать свойства и методы прототипа Query, которые позволяют получить основную структуру запроса:
mongooseCollection.name - наименование коллекции
getQuery - возвращает список фильтров запроса ( например, where)
op - наименование операции (например, find)
options - опции запроса ( например, limit )
const key = JSON.stringify(
{
...this.getQuery(),
collection: this.mongooseCollection.name,
op: this.op,
options: this.options
}
)
Шаг 4. Основная логика
Теперь, определив паттерн формирования ключей и установив соединение с Redis, можно перейти к имплементации основной логики кэширования:
Пытаемся получить данные по ключу
Если удалось, то возвращаем десериализованные данные
Если же данные еще не были закэшированы, то выполняем запрос в базу данных
Кэшируем полученные данные
Возвращаем результат
const cacheValue = await client.get(key)
if ( cacheValue ) return JSON.parse(cacheValue)
const result = await exec.apply(this, arguments)
if ( result ) {
client.set(key, JSON.stringify(result))
}
return result
Шаг 5. Собираем все вместе
Последним шагом, чтобы собрать весь функционал вместе, будет переопределение метода exec, чтобы кэширование применялось «из коробки»:
module.exports =
{
applyMongooseCache() {
const redisUrl = process.env.REDIS_CACHE_URL
const client = redis.createClient(redisUrl)
client.get = util.promisify(client.get)
const exec = mongoose.Query.prototype.exec
mongoose.Query.prototype.exec = async function () {
const key = JSON.stringify(
{
...this.getQuery(),
collection: this.mongooseCollection.name,
op: this.op,
options: this.options
}
)
const cacheValue = await client.get(key)
if ( cacheValue ) return JSON.parse(cacheValue)
const result = await exec.apply(this, arguments)
if ( result ) {
client.set(key, JSON.stringify(result))
}
return result
}
},
}
Теперь остается только вызвать функцию applyMongooseCache при старте Вашего приложения.
Шаг 6. Docker-compose для локального запуска сервисов
Для тестирования данного функционала нам нужны два сервиса:
MongoDB
Redis
Mongo Express - система администрирования MongoDB
Redis admin - система администрирования Redis
version: '3'
services:
mongo:
image: mongo
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: root
ports:
- 27017:27017
mongo-express:
image: mongo-express:0.54
restart: always
ports:
- 8081:8081
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: root
depends_on:
- mongo
redis:
image: redis
ports:
- 6379:6379
redis-admin:
image: erikdubbelboer/phpredisadmin
ports:
- 8085:80
depends_on:
- redis
environment:
REDIS_1_HOST: redis
REDIS_1_NAME: redis
REDIS_1_PORT: 6379
Используя данную базовую конфигурацию, Вы сможете получить к сервисам локальный доступ на 27017(mongo) и 6379 (Redis) портах.
Соответственно, системы администрирования будут доступны на 8081 ( mongo-express) и 8085 (Redis admin) портах.
Ссылка на репозиторий: https://github.com/IAlexanderI1994/mongoose-redis
Благодарю Вас за прочтение. Буду рад Вашим вопросам и комментариям.
Комментарии (4)
korsetlr473
22.09.2021 14:43для nodejs ? а для других языков как это будет выгляять и имеется такое? например java C#
Alexander-Kiryushin Автор
22.09.2021 16:04Добрый день. Данное решение использует особенности JS и написанных под него пакетов, поэтому - да, применимо только в рамках NodeJS.
napa3um
await client.set(key, JSON.stringify(result))
- тут, наверное, можно и без эвейта. Хотя это вряд ли значительно повлияет на производительность в общем случае, но в случае (притянутого за уши в качестве аргумента) дидоса подсетки с кешем, возможно, позволит сохранить сайт доступным для пользователей :). Впрочем, там и в других строчках нужно делать зависимсоть от кеша менее жёсткой. Мне кажется это важным, кеш должен легко и прозрачно "отсоединяться" (админом/девопсом без каких-то дополнительных взаимодействий с приложением, для обновления серверов, например). В общем, валить приложение/запрос полностью при проблемах с кешем обычно не принято.Ну и самое главное - а как инвалидировать кеш-то? Где задаётся таймаут или условия стирания ключа? Пока пригодно только для ReadOnly- или AppendOnly-хранилищ, кажется.
И исследовали ли интернет на предмет аналогов? Например, https://github.com/AkhilSharma90/mongooserediscache выглядит вполне себе рабочей версией (поддерживает инвалидацию только по таймауту, правда). Думаю, вам после доведения до "коробки" механики инвалидации (было бы круто, чтобы запросы на запись инвалидировали соответствующие кеши на чтение, а не только по таймауту, если такой менеджмент не станет оверхедом в самом приложении, требуя от программиста указания кучи параметров) тоже стоит оформить это в виде пакета, а не туториала :).
P.S.: У программистов, как говорят, вообще всего две проблемы в жизни - инвалидация кеша и выбор имени переменной :). Всё остальное тривиально.
Alexander-Kiryushin Автор
Добрый день.
По поводу await - да, соглашусь: выглядит избыточно.
Аналоги. Признаюсь, не смотрел. Наверное, одной из идей этой статьи было желание обратить внимание читателя на возможности "залезания под капот" большим пакетам и на примере показать, как это можно сделать. Если говорить про конкретный аналог, который Вы привели, то вижу там уязвимость в том, что важная часть ключа кэша может быть потеряна, потому что автор решения не включает туда наименование операции ( count может быть спутан с find), а так же игнорируются опции запроса( например, limit), что негативно скажется на тех же пагинированных запросах.
А теперь про "самое главное" :) Инвалидация кэша - тема вообще очень холиварная. Все-таки это очень сильно упирается в особенности системы, которую Вы разрабатываете, поэтому сложно дать конкретную рекомендацию, будь то таймер или поднятие эвента на сброс. В этом случае стоит рассматривать эту статью, как некий вводный материал.