Как ускорить отклик и повысить производительность при помощи кэширования Redis

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

У сайта может быть превосходное содержимое, но будут ли люди ждать, чтобы его увидеть, если оно загружается очень медленно?

Согласно нескольким исследованиям, до 50% пользователей ждет не более трех секунд. Для коммерческих сайтов это настоящая проблема, которая ведет к потере потенциальных продаж.

Кэширование данных веб-приложения может иметь решающее значение для решения этой проблемы и обеспечения высокой производительности при масштабировании.

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

Что такое Redis?

Удаленный сервер словарей (Remote Dictionary Server, Redis)  —  это высокопроизводительная база данных NoSQL с открытым исходным кодом, которую применяют для кэширования в приложениях различного типа.

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

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

  • чаты и мессенджеры;
  • списки последних элементов;
  • счетчики в реальном времени и статистика;
  • управление корзинами для онлайн-покупок;
  • хранение пользовательских сеансов в приложении;
  • поддержка кэширования веб-страниц.

Что такое кэширование?

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

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

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

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

Кэширование с Node.js и Redis

Начнем с разработки приложения. Наша цель  —  выполнить запросы ко внешнему API и измерить время отклика.

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

Установка Redis

Установим Redis в локальном окружении. Для этого можно воспользоваться следующими руководствами:

Установка Node.js

Создадим проект Node.js, начав со ввода команды для создания package.json:

npm init

Установим необходимые для Node.js-приложения зависимости с помощью следующих команд:

npm install express redis axios
  • Express для создания сервера.
  • Redis для соединения приложения с Redis и выполнения запросов.
  • axios для отправки REST-запросов.
npm install nodemon response-time -D
  • nodemon автоматически запускает приложение после каждого изменения.
  • response-time дает возможность измерить время отклика на каждый запрос.

В файле package.json создадим внутри ключа scripts новый скрипт, который будет использоваться для запуска приложения.

"dev": "nodemon src/app.js"

Создание приложения Node.js

Настроим начальный шаблон для Node.js-приложений, подобных этому. Создадим файл app.js и добавим внутрь него следующие строчки:

const express = require('express')
const responseTime = require('response-time')
const redis = require('redis')
const axios = require('axios')const app = express()

app.listen(process.env.PORT || 3000, () => {
console.log("Node server started")
})

Эта команда создаст сервер Express:

npm run dev

В консоль будет выведено сообщение: “Node server started”.

Получение данных от внешнего API

В этом руководстве мы воспользуемся REST-версией API Rick and Morty, где есть следующие конечные точки:

{
"characters": "https://rickandmortyapi.com/api/character",
"locations": "https://rickandmortyapi.com/api/location",
"episodes": "https://rickandmortyapi.com/api/episode"
}

С помощью axios сделаем запрос к эндпойнту /character.

const express = require('express')
const responseTime = require('response-time')
const redis = require('redis')
const axios = require('axios')
const app = express()
app.use(responseTime())app.get("/character", async (req, res) => {
try {
const response = await axios.get('https://rickandmortyapi.com/api/character')

await client.set('characters', JSON.stringify(response.data))
return res.status(200).json(response.data)
} catch (err) {
return res.status(err.response.status).json({ mmessage: err.mmessage })

}
});
app.listen(process.env.PORT || 3000, () => { console.log("Node server started")})

В ответ на запрос API возвращает объект со всеми персонажами. Здесь нам интересно время отклика (response-time), ушедшее на запрос.

Благодаря тому, что мы добавили пакет response-time в качестве промужеточного ПО, в заголовках запроса мы увидим один новый заголовок под названием X-Response-Time, который и укажет нужное время.

Этот запрос занял 238,831 мс.

Реализация кэша Redis для эндпойнта

Посмотрим, как можно повысить производительность приложения с помощью кэширования.

Во-первых, нужно подключиться к серверу Redis через приложение. Для этой задачи воспользуемся установленным ранее пакетом Redis.

По умолчанию redis.createClient() будет использовать 127.0.0.1 и 6379 в качестве имени хоста и порта соответственно. Если у вас другой хост/порт, вы можете задать их так:

const client = redis.createClient(port, host)
const runApp = async () => {

const client = redis.createClient()

client.on('error', (err) => console.log('Redis Client Error', err))

await client.connect()

console.log('Redis connected!')
}
runApp()

Теперь сделаем тот же запрос к API и кэшируем ответ с помощью метода set() клиента Redis, принимающего в качестве первого параметра имя ключа, который мы хотим сохранить. В качестве второго параметра используется значение этого ключа. Оно сохраняется как строка, поэтому следует перевести JSON в строку.

const runApp = async () => {
app.get('/character', async (req, res) => {

try {
const response = await axios.get('https://rickandmortyapi.com/api/character')
await client.set('characters', JSON.stringify(response.data))

return res.status(200).json(response.data)

} catch (err) {
return res.status(err.response.status).json({ mmessage: err.mmessage })
}
})
}
runApp()

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

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

npm install -g redis-commander

Этот пакет предоставит redis-commander, который при выполнении запустит веб-интерфейс базы данных на порту 127.0.0.1:8081.

Войдя в redis-commander, мы увидим, что ответ на запрос /character уже сохранен с присвоенным ему именем.

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

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

const runApp = async () => {
app.get('/character', async (req, res) => {

try {
const cacheCharacters = await client.get('characters')

if (cacheCharacters) {
return res.json(JSON.parse(cacheCharacters))
}

const response = await axios.get('https://rickandmortyapi.com/api/character')
await client.set('characters', JSON.stringify(response.data))

return res.status(200).json(response.data)
} catch (err) {
return res.status(err.response.status)
.json({ mmessage: err.mmessage })
}
})
}
runApp()

Теперь информация извлекается из кэша, и время отклика значительно сократилось  —  до 4,230 мс.

Важно отметить, что это в этом руководстве мы затронули тему довольно поверхностно, а на деле Redis может предложить гораздо больше. Ознакомьтесь с его официальной документацией.

Вот полный код, который содержит еще один пример конечной точки, получающей параметры:

const express = require('express')
const responseTime = require('response-time')
const redis = require('redis')
const axios = require('axios')

const runApp = async () => {

// подключение к redis
const client = redis.createClient()
client.on('error', (err) => console.log('Redis Client Error', err));
await client.connect();
console.log('Redis connected!')


const app = express()
// добавление response-time к запросам
app.use(responseTime())

app.get('/character', async (req, res) => {

try {

// проверяем, сохранен ли запрос в кэше, и если да, то возвращаем ответ
const cacheCharacters = await client.get('characters')
if (cacheCharacters) {
return res.json(JSON.parse(cacheCharacters))
}

// запрос к API
const response = await axios.get('https://rickandmortyapi.com/api/character')

/* Другой способ — сохранить данные с именем url запроса, со свойством
req.originalUrl, которое будет таким же, как '/character'
await client.set(req.originalUrl, JSON.stringify(response.data))
*/

// сохранение ответа в кэше
await client.set('characters', JSON.stringify(response.data))
return res.status(200).json(response.data)

} catch (err) {
return res.status(err.response.status).json({ mmessage: err.mmessage })
}

})

app.get('/characters/:id', async (req, res) => {

try {

const cacheCharacter = await client.get('cacheCharacter' + req.params.id)
if (cacheCharacter) {
return res.json(JSON.parse(cacheCharacter))
}

const response = await axios.get('https://rickandmortyapi.com/api/character/' + req.params.id)

await client.set('cacheCharacter' + req.params.id, JSON.stringify(response.data))
return res.json(response.data)

} catch (err) {
return res.status(err.response.status)
.json({ message: err.message })
}

})

app.listen(process.env.PORT || 3000, () => {
console.log(`server on port 3000`)
})

}

runApp()

Заключение

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

Теперь вы знаете, как пользоваться Redis для кэширования часто запрашиваемых данных в приложении и тем самым значительно повысить производительность.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи L Javier Tovar: Reduce Response Times and Improve Performance with Redis Caching

Предыдущая статья5 способов упростить повседневные задачи с помощью ChatGPT
Следующая статьяКак написать чистый код, который легко читать