Пошук уроків, статей та іншого контенту
Освоїте підключення до Redis, базові операції, структури даних і вибір типу для типових сценаріїв кешування.
Redis — це швидке сховище даних у пам’яті. Його часто використовують як кеш між застосунком і основною базою даних.
Типовий сценарій:
Застосунок отримує запит.
Перевіряє, чи є потрібні дані в Redis.
Якщо дані є — повертає їх із кешу.
Якщо даних немає — читає їх із основної бази даних.
Зберігає результат у Redis на певний час.
Повертає результат клієнту.
Так Redis допомагає:
зменшити кількість запитів до основної бази даних;
пришвидшити відповіді;
зменшити навантаження на сервер застосунку та базу даних.
Redis зберігає дані в оперативній пам’яті, тому операції виконуються дуже швидко. Водночас кеш не повинен бути єдиним місцем зберігання важливих даних: кешовані значення можна видалити або вони можуть застаріти.
Для локальної роботи потрібен запущений Redis-сервер. Наприклад, його можна запустити в Docker:
docker run --name redis-cache -p 6379:6379 -d redisЗа замовчуванням Redis буде доступний за адресою:
хост: localhost;
порт: 6379.
Після запуску можна перевірити з’єднання за допомогою Redis CLI:
redis-cli pingОчікувана відповідь:
PONGДля роботи з Redis у Node.js використаємо пакет redis:
npm install redisБазове підключення:
import { createClient } from "redis";
const client = createClient({
url: "redis://localhost:6379",
});
client.on("error", (error) => {
console.error("Помилка Redis:", error);
});
await client.connect();
console.log("Redis підключено");
await client.quit();Метод createClient створює клієнт Redis.
Метод connect встановлює з’єднання із сервером.
Обробник події error потрібен, щоб бачити помилки підключення або виконання команд.
Після завершення роботи з клієнтом викликають quit.
У довготривалому серверному застосунку клієнт зазвичай підключають один раз під час запуску програми, а не для кожного HTTP-запиту.
Найпростіший тип даних Redis — рядок. Він підходить для:
тексту;
чисел;
JSON;
токенів;
результатів обчислень.
import { createClient } from "redis";
const client = createClient({
url: "redis://localhost:6379",
});
client.on("error", (error) => {
console.error("Помилка Redis:", error);
});
await client.connect();
await client.set("site:title", "Мій сайт");
const title = await client.get("site:title");
console.log(title); // Мій сайт
await client.quit();Redis зберігає значення як рядки. Якщо потрібно зберегти об’єкт JavaScript, його потрібно перетворити на JSON:
const user = {
id: 42,
name: "Олена",
role: "admin",
};
await client.set("user:42", JSON.stringify(user));
const cachedUser = await client.get("user:42");
const parsedUser = JSON.parse(cachedUser);
console.log(parsedUser.name); // ОленаЯкщо ключ не існує, get повертає null:
const value = await client.get("missing:key");
console.log(value); // nullЦе називають cache miss — промахом кешу.
Кешовані дані часто мають бути доступними лише певний час. Для цього використовують TTL — time to live, тобто час життя ключа.
Зберегти значення на 60 секунд можна так:
await client.set(
"weather:kyiv",
JSON.stringify({
temperature: 18,
condition: "sunny",
}),
{
EX: 60,
},
);Після завершення 60 секунд Redis автоматично видалить ключ.
Перевірити залишок часу можна командою ttl:
const secondsLeft = await client.ttl("weather:kyiv");
console.log(secondsLeft);Можливі результати:
додатне число — кількість секунд до видалення;
-1 — ключ існує, але не має TTL;
-2 — ключ не існує.
TTL важливий для кешу, бо без нього дані можуть залишатися застарілими необмежений час.
Поширений шаблон кешування називають cache-aside:
спочатку перевірити Redis;
якщо значення знайдено — повернути його;
якщо значення не знайдено — отримати дані з основного джерела;
записати результат у Redis;
повернути результат.
import { createClient } from "redis";
const client = createClient({
url: "redis://localhost:6379",
});
client.on("error", (error) => {
console.error("Помилка Redis:", error);
});
await client.connect();
async function getProduct(productId) {
const key = `product:${productId}`;
// Спочатку перевіряємо кеш
const cachedProduct = await client.get(key);
if (cachedProduct !== null) {
console.log("Дані отримано з кешу");
return JSON.parse(cachedProduct);
}
console.log("Кеш порожній, читаємо основне джерело");
// У реальному застосунку тут буде запит до бази даних
const product = {
id: productId,
name: "Механічна клавіатура",
price: 2500,
};
// Зберігаємо результат у кеш на 5 хвилин
await client.set(key, JSON.stringify(product), {
EX: 300,
});
return product;
}
const firstResult = await getProduct(101);
console.log(firstResult);
const secondResult = await getProduct(101);
console.log(secondResult);
await client.quit();Перший виклик не знаходить значення в Redis і створює його. Другий виклик отримує те саме значення з кешу.
Видалити ключ можна за допомогою del:
await client.del("product:101");Іноді кеш потрібно видалити після зміни даних у основній базі:
async function updateProduct(productId, changes) {
// Тут відбувається оновлення товару в основній базі даних
console.log("Товар оновлено:", productId, changes);
// Видаляємо старе значення з кешу
await client.del(`product:${productId}`);
}Після видалення наступний запит знову прочитає актуальні дані з основного джерела та збереже їх у кеш.
Також ключ можна перезаписати новим значенням:
await client.set(
"config:theme",
"dark",
{
EX: 3600,
},
);Redis не має окремих просторів імен для різних сутностей. Усі ключі перебувають в одному сховищі, тому важливо використовувати зрозумілий формат імен.
Поширений стиль — розділяти частини ключа двокрапкою:
user:42
user:42:profile
product:101
category:books:products
session:abc123Переваги такого формату:
видно, до якої сутності належить ключ;
простіше шукати ключі під час налагодження;
зменшується ризик випадкового використання однакових імен.
Ключі мають бути достатньо конкретними. Наприклад, для списку товарів із різними параметрами потрібні різні ключі:
products:category:books:page:1
products:category:books:page:2Окрім рядків, Redis має кілька структур даних. Вибір структури залежить від того, як застосунок працює з даними.
Hash зберігає набір полів і значень для одного об’єкта. Він підходить, коли потрібно зберігати окремі властивості сутності.
Наприклад, користувача:
await client.hSet("user:42", {
name: "Олена",
email: "olena@example.com",
role: "admin",
});
const user = await client.hGetAll("user:42");
console.log(user);
// {
// name: "Олена",
// email: "olena@example.com",
// role: "admin"
// }Окреме поле можна прочитати або змінити:
const role = await client.hGet("user:42", "role");
await client.hSet("user:42", "role", "editor");Hash зручний, коли не потрібно серіалізувати весь об’єкт у JSON для зміни одного поля.
Однак для простого кешування повного об’єкта рядок із JSON часто є зрозумілішим і достатнім рішенням.
List — це впорядкована послідовність значень. Його можна використовувати для простих списків або черг.
await client.lPush("notifications:42", "Нове повідомлення");
await client.lPush("notifications:42", "Ваше замовлення прийнято");
const notifications = await client.lRange("notifications:42", 0, -1);
console.log(notifications);Команда lPush додає елемент на початок списку.
У Redis індексація списку починається з нуля. Значення -1 у lRange означає останній елемент, тому діапазон 0, -1 повертає весь список.
Для кешу List може бути корисним, наприклад, для тимчасового списку останніх подій:
await client.lPush("events:latest", JSON.stringify({
type: "login",
userId: 42,
}));
await client.lTrim("events:latest", 0, 99);Команда lTrim залишає лише вказаний діапазон. У цьому прикладі зберігаються 100 останніх елементів.
Set — це невпорядкована множина унікальних значень. Один і той самий елемент не зберігається двічі.
await client.sAdd("user:42:roles", "admin");
await client.sAdd("user:42:roles", "editor");
await client.sAdd("user:42:roles", "admin");
const roles = await client.sMembers("user:42:roles");
console.log(roles);
// ["admin", "editor"]Set підходить для:
унікальних тегів;
набору дозволів;
списку ідентифікаторів;
перевірки належності до групи.
Перевірити, чи є значення в Set:
const isAdmin = await client.sIsMember("user:42:roles", "admin");
console.log(isAdmin); // trueSorted Set зберігає унікальні значення разом із числовим балом. Елементи можна отримувати в порядку цього бала.
await client.zAdd("leaderboard", [
{ score: 120, value: "user:42" },
{ score: 95, value: "user:17" },
{ score: 150, value: "user:8" },
]);
const leaders = await client.zRange("leaderboard", 0, -1, {
REV: true,
});
console.log(leaders);
// ["user:8", "user:42", "user:17"]Sorted Set підходить для:
рейтингів;
списків, упорядкованих за оцінкою;
тимчасових подій із часовим балом;
вибірки елементів за числовим значенням.
Орієнтуйтеся на форму даних і потрібні операції:
String — повний JSON-об’єкт, число, текст або просте значення.
Hash — об’єкт із незалежними полями.
List — впорядкована послідовність або проста черга.
Set — унікальна множина без потреби в порядку.
Sorted Set — унікальні значення, які потрібно сортувати за балом.
Для більшості перших кешів достатньо String із JSON і TTL:
product:101 -> JSON товаруНе потрібно використовувати складнішу структуру лише тому, що Redis її підтримує. Обирайте структуру під конкретні операції читання та оновлення.
Нижче наведено приклад, який демонструє підключення, кешування об’єкта, TTL і видалення ключа:
import { createClient } from "redis";
const client = createClient({
url: process.env.REDIS_URL ?? "redis://localhost:6379",
});
client.on("error", (error) => {
console.error("Помилка Redis:", error);
});
async function loadProductFromDatabase(productId) {
// Імітуємо повільний запит до основної бази даних
await new Promise((resolve) => setTimeout(resolve, 500));
return {
id: productId,
name: "Механічна клавіатура",
price: 2500,
};
}
async function getProduct(productId) {
const cacheKey = `product:${productId}`;
const cachedValue = await client.get(cacheKey);
if (cachedValue) {
console.log("Відповідь із кешу");
return JSON.parse(cachedValue);
}
const product = await loadProductFromDatabase(productId);
await client.set(cacheKey, JSON.stringify(product), {
EX: 60,
});
console.log("Відповідь із основної бази");
return product;
}
async function main() {
await client.connect();
const firstProduct = await getProduct(101);
console.log(firstProduct);
const secondProduct = await getProduct(101);
console.log(secondProduct);
await client.del("product:101");
await client.quit();
}
main().catch(async (error) => {
console.error(error);
if (client.isOpen) {
await client.quit();
}
process.exitCode = 1;
});Запустити приклад можна командою:
node index.jsЯкщо у package.json не вказано "type": "module", додайте його або використовуйте відповідний формат імпортів для налаштування свого Node.js-проєкту.
Якщо кешувати значення без TTL, воно може залишатися там після того, як стало неактуальним.
Краще одразу визначити прийнятний час життя:
await client.set("exchange-rate:usd", "41.2", {
EX: 300,
});Redis не зберігає JavaScript-об’єкти напряму. Для об’єкта використовуйте JSON.stringify і JSON.parse:
await client.set("user:42", JSON.stringify(user));
const value = await client.get("user:42");
const restoredUser = JSON.parse(value);get може повернути null. Не можна без перевірки одразу покладатися на результат:
const value = await client.get("product:101");
if (value === null) {
// Завантажуємо дані з основного джерела
}Після зміни даних у основній базі старе значення може залишитися в Redis. У такому разі видаліть відповідний ключ або оновіть його.
Створення нового з’єднання Redis для кожного запиту додає зайві витрати. У серверному застосунку клієнт краще створити та підключити один раз.
Ключі на кшталт data1, cache2 і temp складно підтримувати. Використовуйте єдину схему:
entity:id[:additional-part]Наприклад:
user:42
user:42:permissions
product:101Redis — швидке сховище в пам’яті, яке часто використовують як кеш.
Для підключення в Node.js можна використати пакет redis.
Значення кешу зазвичай зберігають із TTL.
Для об’єктів використовують JSON або Hash.
String підходить для простих значень і повних JSON-об’єктів.
Hash призначений для полів одного об’єкта.
List зберігає впорядковані елементи.
Set зберігає унікальні значення.
Sorted Set зберігає унікальні значення з числовим порядком.
Типовий алгоритм кешування: перевірити Redis, за промаху отримати дані з основного джерела, зберегти їх у Redis і повернути результат.
Після зміни основних даних потрібно подбати про видалення або оновлення кешу.