Давайте дружить в Телеграме: рассказываем про новые фичи, общаемся в комментах, прислушиваемся к вашим идеям Подписаться

Как кэшировать приложения Node.JS с помощью Redis

Миша Курушин
Миша Курушин
Технический писатель
24 мая 2023 г.
1171
12 минут чтения
Средний рейтинг статьи: 5

Кэширование представляет собой процесс хранения копий файлов в кеше — временном хранилище, доступ к которому гораздо быстрее, чем у других доступных в системе способов хранения.

При разработке Node.js-приложений это актуально — запросы к базе данных могут занимать гораздо больше времени, чем извлечение информации из временного хранилища.

Например, нет никакого смысла загружать HTML-разметку веб-страницы при каждом запросе пользователя к серверу — это добавит несколько (иногда десятков) миллисекунд к времени ответа. Гораздо правильнее разместить страницу (или JSON-данные для вывода на странице SPA-приложения) в кэше.

Проще говоря, кэширование — это про оптимизацию.

В этой статье будет рассмотрен способ кеширования данных приложения Node.js с помощью Redis с использованием фреймворка Express.

Что такое Redis?

Redis (remote dictionary server, удаленный сервер словарей) — это резидентная база данных (in-memory database/store) с открытым исходным кодом (open source), которая работает с простыми структурами «ключ — значение».

Это лишь вопрос терминологии — называть Redis базой данных, инструментом кэширования или как-то еще. Главное, что Redis размещает данные не на жестком диске, а в оперативной памяти — отсюда и более высокая производительность. Собственно, именно поэтому база и называется «резидентной».

Несмотря на то, что данные находятся в оперативной памяти, периодически они сохраняются на жесткий диск в виде снимков.

Установка сервера Redis

Установка Redis под разные операционные системы отличается — на официальном сайте можно найти подробную инструкцию под каждую из них.

В этой статье рассматривается вариант с использованием Ubuntu или Debian. Поэтому, последнюю версию Redis мы возьмем из официального APT (Advanced Packaging Tool) репозитория — packages.redis.io:

sudo apt update
sudo apt install redis

После этого сервер Redis готов к работе. На локальной машине им можно управлять с помощью команд, которые будут рассмотрены ниже — запускать, останавливать, добавлять и удалять ключи.

Для Windows необходимо загрузить установщик с официального репозитория GitHub. После установки сервер Redis запускается с помощью CLI-команды:

redis-cli

Если же вы используете macOS, то установить Redis можно с помощью пакетного менеджера Homebrew:

brew install redis

После установки сервер выпускается так:

redis-server

Конфигурация проекта Node.js

Прежде чем начать разбираться в принципах взаимодействия с Redis через Node-приложение, давайте сперва создадим отдельный рабочий каталог и перейдем в него:

mkdir node_redis
cd node_redis

Как и всегда, создадим конфигурационный файл package.json с минимальным набором данных:

{
"name": "node_redis",
  "version": "1.0.0",
  "description": "Simple example of using Redis by TimeWeb",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
     "express": "latest",
     "axios": "latest",
     "redis": "latest"
  }
}

Обратите внимание на указанные зависимости. Для проекта нам потребуются последние версии популярного сетевого фреймворка Express и официального клиента клиент Redis на NPM — это отдельная библиотека Node.js, содержащая высокоуровневое API (классы и функции) для взаимодействия с сервером Redis.

Модуль Axios поможет парсить JSON-данных, которые будет возвращать удаленный сервер в ответ на API-запросы.

Для установки зависимостей нам потребуется пакетный менеджер NPM. Если на вашей машине он отсутствует, установить его можно с помощью команды:

sudo apt install npm

О том, как установить последнюю версию Node.js в операционной системе Ubuntu, вы можете прочитать в отдельной инструкции. Так как в коде нашего приложения будет использоваться async/await-синтаксис, минимальная версия Node.js — 8.

Теперь, когда все зависимости указаны, их можно установить:

npm install

Приложение на Express без кэширования

В этом примере приложение будет использовать фейковое API сервиса JSONPlaceholder — он был создан как раз для этих целей. Мы будем отправлять запрос по адресу https://jsonplaceholder.typicode.com/posts/1, получая в ответе импровизированные данные в JSON-формате:

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
 "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

Соответственно, последующая загрузка данных из кеша (вместо выполнения повторных запросов к удаленному серверу) увеличит скорость работы приложения.

Однако сперва мы реализуем сам процесс обработки пользовательских запросов без использования кэша — его мы добавим позднее.

Давайте сначала создадим наш index.js-файл и отредактируем его. В скрипте по возможности будет использоваться современный синтаксис JS (ES6) с применением операторов async/await:

const express = require("express"); // импортируем фреймворк Express
const axios = require("axios"); // импортируем модуль Axios для работы с JSON-данными

const app = express(); // создаем экземпляр приложения

// создаем асинхронную функцию для запроса данных с удаленного сервера с помощью axios

async function getRemoteData() {
  const information = await axios.get(`https://jsonplaceholder.typicode.com/posts/1`); // отправляем  запрос на удаленный сервер с API
  console.log("There was a request to a remote server"); // выводим информационное сообщение в консоль
  return information.data; // возвращаем полученные JSON-данные в сыром виде
}

// создаем асинхронную функцию обработки пользовательских запросов

async function onRequest(req, res) {
  let results = await getRemoteData(); // вызываем ранее созданную функцию запроса данных с удаленного сервера

if(results.length === 0) throw "API error"; // обрабатываем пустой ответ ошибкой

  res.send(results); // отвечаем на пользовательский запрос сырыми JSON-данными
}

app.get('/', onRequest); // вешаем ранее созданную функцию на хук GET-запроса
app.listen(8080); // запускаем обработку входящих запросов на стандартном порту HTTP-сервера

Теперь можно запустить данный скрипт, открыть localhost в браузере и увидеть на веб-странице вывод полученных JSON-данных:

node index.js

Каждый запрос к локальному серверу будет в свою очередь вызывать запрос к удаленному серверу. Например, если вы 3 раза обновите страницу в браузере, то в терминале приложения Node.js (запущенный сервер) 3 раза выводится указанное в коде сообщение «There was a request to a remote server».

Однако зачем? С рациональной точки зрения в этом нет необходимости.

Данные, полученные в первый раз, имеет смысл закешировать, чтобы сократить количество операций и время ожидания пользователя. Это актуально только в том случае, если предполагается статичность получаемых данных некоторый промежуток времени — то есть кэшировать можно только те данные, которые меняются относительно нечасто.

Приложение на Express с использованием кэша

Давайте модифицируем ранее написанный пример, чтобы наше приложение «научилось» кэшировать данные.

Для этого сначала подключим клиент Redis — добавим новую строку в начало index.js:

...
const redis = require("redis");
...

Теперь закономерно нам необходимо подключиться к ранее запущенному серверу Redis, после чего мы сможем устанавливать и получать ключи. Добавим еще несколько строк кода:

...
(async () => {
  client = redis.createClient();

  client.on("error", (error) => console.log('Что-то пошло не так', error)); // вешаем хук на ошибку подключения к серверу Redis

 await client.connect(); // подключаемся к серверу
})();
...

Обратите внимание, что подключение к серверу Redis происходит в безымяной самовызывающейся асинхронной функции. Это нужно для последовательного выполнения всех предварительных конфигураций нашего приложения. К тому же, функция connect возвращает обещание (promise), которое обрабатывается либо с помощью операторов then/catch, либо внутри асинхронной функции.

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

Давайте модифицируем функцию (middleware) обработки запросов onRequest:

...
async function onRequest(req, res) {
  let results; // заранее объявляем переменную для результата

  const cacheData = await client.get(“post”); // пытаемся получить переменную post из базы данных Redis

  if(cacheData) {
      results = JSON.parse(cacheData); // парсим данные из формата сырой строки в формат структуры
    } else {
      results = await getRemoteData(); // вызываем функцию получения данных с удаленного сервера
      if(results.length === 0) throw "API error"; // обрабатываем пустой результат ошибкой
      await client.set(“post”, JSON.stringify(results)); // кэшируем полученные данные
    }

  res.send(results); // отвечаем на запрос JSON-данными
}

Обратите внимание, что функция get возвращает null в том случае, если по данному ключу в Redis нет никаких сохраненных значений. Соответственно, если это так, то выполняется API-запрос к удаленному серверу. А если нет — пользователю отправляются данные из кеша.

При этом функция set непосредственно отвечает за кэширование — она устанавливает указанное значение по названию ключа таким образом, что впоследствии его можно получить через get.

Полный код приложения на данном этапе выглядит так:

const express = require("express"); // импортируем фреймворк Express
const axios = require("axios"); // импортируем модуль Axios для работы с JSON-данными
const redis = require("redis"); // импортируем клиент Redis

const app = express(); // создаем экземпляр приложения

(async () => {
  client = redis.createClient();

  client.on("error", (error) => console.log('Что-то пошло не так', error)); // вешаем хук на ошибку подключения к серверу Redis


  await client.connect(); // подключаемся к серверу
})();

// создаем асинхронную функцию для запроса данных с удаленного сервера с помощью axios

async function getRemoteData() {
  const information = await axios.get(`https://jsonplaceholder.typicode.com/posts/1`); // отправляем  запрос на удаленный сервер с API
  console.log(“There was a request to a remote server”); // выводим информационное сообщение в консоль
  return information.data; // возвращаем полученные JSON-данные в сыром виде
}

// создаем асинхронную функция обработки пользовательских запросов

async function onRequest(req, res) {
  let results; // заранее объявляем переменную для результата

  const cacheData = await client.get(“post”); // пытаемся получить переменную post из базы данных Redis

  if(cacheData) {
      results = JSON.parse(cacheData); // парсим данные из формата сырой строки в формат структуры
    } else {
      results = await getRemoteData(); // вызываем функцию получения данных с удаленного сервера
      if(results.length === 0) throw "API error"; // обрабатываем пустой результат ошибкой
      await client.set(“post”, JSON.stringify(results)); // кэшируем полученные данные
    }

  res.send(results); // отвечаем на запрос JSON-данными
}

// запускаем HTTP-сервер с необходимыми настройками

app.get('/', onRequest); // вешаем ранее созданную функцию на хук GET-запроса
app.listen(8080); // запускаем обработку входящих запросов на стандартном порту HTTP-сервера

Установка срока действия кэша

Данные, записанные в кэш, должны периодически обновляться, чтобы не устаревать.

API реальных проектов часто сообщают дополнительную информацию о том, с какой частотой необходимо обновлять кэшированные данные. Эта информация используется для установки своего рода таймаута — срока, в течение которого данные в кэше действительны. По прошествии этого времени приложение выполняет повторный запрос, получая свежие обновления.

В нашем случае мы пойдем по более простому пути, который тоже встречается на практике. Мы установим константный срок действия кэша — 60 секунд. По прошествии этого времени мы будем делать повторный запрос к удаленному серверу за свежими данными.

Важно, что функция срока действия кэша выполняется на стороне Redis. Это возможно с помощью указания дополнительных параметров при использовании функции set.

Для этого модифицируем вызов этой функции с помощью дополнительной структуры в качестве третьего аргумента. Таким образом строка кода:

...
await client.set(“post”, JSON.stringify(results)); // кэшируем полученные данные

...

преобразуется в следующий вид:

...
await client.set(“post”, JSON.stringify(results), { EX: 60, NX: true }); // кэшируем полученные данные

...

В данном случае мы обновили ранее написанную строчку кода, установив параметр EX — срок истечения кэша в секундах. NX при этом гарантирует, что ключ установится только в том случае, если его еще нет в базе данных Redis. Последний параметр действительно важен, ведь в противном случае повторная установка ключа будет приводить к обновлению таймаута кеша, не позволяя ему полностью истечь.

Теперь значение ключа post будет храниться в базе данных 60 секунд — далее оно стирается. Это значит, что в коде нашего приложения переменная cacheData будет получать значение null каждую минуту — это приведет к API-запросу к удаленному серверу и повторному кэшированию полученного результата.

Заключение

Эта статья показала, как временное хранилище, размещенное в оперативной памяти, может служить неким «медиатором» между непосредственно обработкой данных и их хранением на твердотельном накопителе.

Все это является разновидностью кеширования, сокращающего лишние вычислительные (и сетевые) операции, тем самым улучшая производительность приложения и снижая нагрузку на сервер.

Как видно, такое хранилище можно быстро развернуть с помощью Redis с использованием клиента на Node. В нашем случае это было импровизированное API, возвращающее тривиальные JSON-данные. В одном случае они запрашивались каждый раз, в другом случае размещались в кэше — иногда с установленным сроком действия.

Продемонстрированные примеры — лишь начальный уровень. Больше информации об использовании Redis, как и всегда, можно получить из официальной документации. Впрочем, то же самое касается документации Express и Axios.

Зарегистрируйтесь и начните пользоваться
сервисами Timeweb Cloud прямо сейчас

15 лет опыта
Сосредоточьтесь на своей работе: об остальном позаботимся мы
165 000 клиентов
Нам доверяют частные лица и компании, от небольших фирм до корпораций
Поддержка 24/7
100+ специалистов поддержки, готовых помочь в чате, тикете и по телефону