Кэширование представляет собой процесс хранения копий файлов в кеше — временном хранилище, доступ к которому гораздо быстрее, чем у других доступных в системе способов хранения.
При разработке Node.js-приложений это актуально — запросы к базе данных могут занимать гораздо больше времени, чем извлечение информации из временного хранилища.
Например, нет никакого смысла загружать HTML-разметку веб-страницы при каждом запросе пользователя к серверу — это добавит несколько (иногда десятков) миллисекунд к времени ответа. Гораздо правильнее разместить страницу (или JSON-данные для вывода на странице SPA-приложения) в кэше.
Проще говоря, кэширование — это про оптимизацию.
В этой статье будет рассмотрен способ кеширования данных приложения Node.js с помощью Redis с использованием фреймворка Express.
Redis (remote dictionary server, удаленный сервер словарей) — это резидентная база данных (in-memory database/store) с открытым исходным кодом (open source), которая работает с простыми структурами «ключ — значение».
Это лишь вопрос терминологии — называть 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
vds
Прежде чем начать разбираться в принципах взаимодействия с 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
В этом примере приложение будет использовать фейковое 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».
Однако зачем? С рациональной точки зрения в этом нет необходимости.
Данные, полученные в первый раз, имеет смысл закешировать, чтобы сократить количество операций и время ожидания пользователя. Это актуально только в том случае, если предполагается статичность получаемых данных некоторый промежуток времени — то есть кэшировать можно только те данные, которые меняются относительно нечасто.
Давайте модифицируем ранее написанный пример, чтобы наше приложение «научилось» кэшировать данные.
Для этого сначала подключим клиент 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 на своем VDS-сервере
Эта статья показала, как временное хранилище, размещенное в оперативной памяти, может служить неким «медиатором» между непосредственно обработкой данных и их хранением на твердотельном накопителе.
Все это является разновидностью кеширования, сокращающего лишние вычислительные (и сетевые) операции, тем самым улучшая производительность приложения и снижая нагрузку на сервер.
Как видно, такое хранилище можно быстро развернуть с помощью Redis с использованием клиента на Node. В нашем случае это было импровизированное API, возвращающее тривиальные JSON-данные. В одном случае они запрашивались каждый раз, в другом случае размещались в кэше — иногда с установленным сроком действия.
Продемонстрированные примеры — лишь начальный уровень. Больше информации об использовании Redis, как и всегда, можно получить из официальной документации. Впрочем, то же самое касается документации Express и Axios.