1. Mongoose представляет специальную ODM-библиотеку (Object Data Modelling) для работы с MongoDB, которая позволяет сопоставлять объекты классов и документы коллекций из базы данных. 

  2. Redis (Remote Dictionary Server)- это быстрое хранилище данных типа «ключ‑значение» в памяти, активно используемое в разработке с целью повышения производительности сервисов

  3. В рамках данного гайда мы рассмотрим связку 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, которые позволяют получить основную структуру запроса:

  1. mongooseCollection.name  - наименование коллекции

  2. getQuery - возвращает список фильтров запроса ( например, where)

  3. op - наименование операции (например, find)

  4. options - опции запроса ( например, limit

const key = JSON.stringify(
  {
    ...this.getQuery(),
    collection: this.mongooseCollection.name,
    op: this.op,
    options: this.options
  }
)

Шаг 4. Основная логика

Теперь, определив паттерн формирования ключей и установив соединение с Redis, можно перейти к имплементации основной логики кэширования:

  1. Пытаемся получить данные по ключу

  2. Если удалось, то возвращаем десериализованные данные

  3. Если же данные еще не были закэшированы, то выполняем запрос в базу данных

  4. Кэшируем полученные данные 

  5. Возвращаем результат

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 для локального запуска сервисов

Для тестирования данного функционала нам нужны два сервиса:

  1. MongoDB 

  2. Redis

  3. Mongo Express - система администрирования MongoDB

  4. 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)


  1. napa3um
    21.09.2021 05:34
    +2

    await client.set(key, JSON.stringify(result)) - тут, наверное, можно и без эвейта. Хотя это вряд ли значительно повлияет на производительность в общем случае, но в случае (притянутого за уши в качестве аргумента) дидоса подсетки с кешем, возможно, позволит сохранить сайт доступным для пользователей :). Впрочем, там и в других строчках нужно делать зависимсоть от кеша менее жёсткой. Мне кажется это важным, кеш должен легко и прозрачно "отсоединяться" (админом/девопсом без каких-то дополнительных взаимодействий с приложением, для обновления серверов, например). В общем, валить приложение/запрос полностью при проблемах с кешем обычно не принято.

    Ну и самое главное - а как инвалидировать кеш-то? Где задаётся таймаут или условия стирания ключа? Пока пригодно только для ReadOnly- или AppendOnly-хранилищ, кажется.

    И исследовали ли интернет на предмет аналогов? Например, https://github.com/AkhilSharma90/mongooserediscache выглядит вполне себе рабочей версией (поддерживает инвалидацию только по таймауту, правда). Думаю, вам после доведения до "коробки" механики инвалидации (было бы круто, чтобы запросы на запись инвалидировали соответствующие кеши на чтение, а не только по таймауту, если такой менеджмент не станет оверхедом в самом приложении, требуя от программиста указания кучи параметров) тоже стоит оформить это в виде пакета, а не туториала :).

    P.S.: У программистов, как говорят, вообще всего две проблемы в жизни - инвалидация кеша и выбор имени переменной :). Всё остальное тривиально.


    1. Alexander-Kiryushin Автор
      21.09.2021 12:43
      -1

      Добрый день.

      По поводу await - да, соглашусь: выглядит избыточно.

      Аналоги. Признаюсь, не смотрел. Наверное, одной из идей этой статьи было желание обратить внимание читателя на возможности "залезания под капот" большим пакетам и на примере показать, как это можно сделать. Если говорить про конкретный аналог, который Вы привели, то вижу там уязвимость в том, что важная часть ключа кэша может быть потеряна, потому что автор решения не включает туда наименование операции ( count может быть спутан с find), а так же игнорируются опции запроса( например, limit), что негативно скажется на тех же пагинированных запросах.

      А теперь про "самое главное" :) Инвалидация кэша - тема вообще очень холиварная. Все-таки это очень сильно упирается в особенности системы, которую Вы разрабатываете, поэтому сложно дать конкретную рекомендацию, будь то таймер или поднятие эвента на сброс. В этом случае стоит рассматривать эту статью, как некий вводный материал.


  1. korsetlr473
    22.09.2021 14:43

    для nodejs ? а для других языков как это будет выгляять и имеется такое? например java C#


    1. Alexander-Kiryushin Автор
      22.09.2021 16:04

      Добрый день. Данное решение использует особенности JS и написанных под него пакетов, поэтому - да, применимо только в рамках NodeJS.