Пошук уроків, статей та іншого контенту
Підключайте Redis до Node.js та використовуйте його для зберігання даних, черг і координації процесів.
Redis — це швидке сховище даних у пам’яті, до якого Node.js підключається через TCP. Дані зберігаються у форматі ключ-значення, а Redis підтримує кілька структур даних:
рядки (String) — текст, числа або JSON;
хеші (Hash) — набір полів одного об’єкта;
списки (List) — послідовність значень, зручна для простих черг;
множини та інші структури, які не розглядаються в цій лекції.
Redis часто використовують для:
кешування результатів;
зберігання короткоживучих даних із терміном дії;
черг фонових задач;
блокування спільного ресурсу між кількома процесами.
На відміну від основної бази даних застосунку, Redis зазвичай використовують для швидкого доступу та координації. Перед використанням потрібно визначити, чи можна відновити дані після їх видалення або втрати.
Спочатку потрібен запущений сервер Redis. Пакет для Node.js встановлюється так:
npm install redisУ прикладах використовується офіційний клієнт redis версії 4 або новішої.
Підключення виконується через змінну середовища REDIS_URL. Типове значення:
redis://localhost:6379Клієнт створюють за допомогою createClient, після чого викликають connect().
const { createClient } = require('redis');
const client = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});
client.on('error', (error) => {
console.error('Помилка Redis:', error);
});
async function main() {
await client.connect();
console.log('Підключення до Redis встановлено');
await client.set('app:status', 'online');
const status = await client.get('app:status');
console.log(status);
await client.quit();
}
main().catch(async (error) => {
console.error(error);
if (client.isOpen) {
await client.quit();
}
process.exitCode = 1;
});Виклик client.on('error', ...) важливий: мережеві помилки можуть виникнути вже після успішного підключення.
Redis зберігає значення як рядки. Об’єкти JavaScript потрібно явно перетворювати на JSON:
const user = {
id: 42,
name: 'Олена',
role: 'admin'
};
await client.set('user:42', JSON.stringify(user));
const savedUser = await client.get('user:42');
const parsedUser = JSON.parse(savedUser);
console.log(parsedUser.name); // ОленаНазви ключів зазвичай будують із частин, розділених двокрапкою:
user:42
product:15
session:abc123
cache:weather:kyivТакий формат полегшує пошук пов’язаних ключів і зменшує ризик випадкових конфліктів.
Для кешу та сесій часто потрібно автоматично видаляти дані. Для цього задають час життя ключа через параметр EX у секундах:
await client.set(
'cache:profile:42',
JSON.stringify({ id: 42, name: 'Олена' }),
{ EX: 300 }
);Ключ буде доступний протягом 300 секунд. Після завершення цього часу GET поверне null.
Також термін дії можна встановити окремою командою:
await client.set('temporary:value', 'data');
await client.expire('temporary:value', 60);
const secondsLeft = await client.ttl('temporary:value');
console.log(secondsLeft);Для кешу важливо завжди встановлювати TTL. Інакше застарілі ключі поступово накопичуватимуться.
Хеш Redis зручно використовувати для об’єктів, поля яких потрібно читати або змінювати окремо:
await client.hSet('user:42', {
name: 'Олена',
role: 'admin',
loginCount: '7'
});
const user = await client.hGetAll('user:42');
console.log(user);
// { name: 'Олена', role: 'admin', loginCount: '7' }
await client.hIncrBy('user:42', 'loginCount', 1);
const loginCount = await client.hGet('user:42', 'loginCount');
console.log(loginCount); // 8Значення полів хешу також повертаються як рядки. Якщо поле містить число, його потрібно перетворити за потреби:
const count = Number(loginCount);Хеші підходять для простих плоских об’єктів. Для вкладених структур зазвичай зберігають JSON у рядковому значенні.
Список Redis можна використати як чергу:
RPUSH додає елемент у кінець;
LPOP забирає елемент із початку;
BRPOP чекає, поки в списку з’явиться елемент.
Виробник задач додає JSON у список:
const task = {
type: 'send-email',
userId: 42,
template: 'welcome'
};
await client.rPush('queue:emails', JSON.stringify(task));Воркер забирає задачу:
const result = await client.sendCommand([
'BRPOP',
'queue:emails',
'0'
]);
const [, rawTask] = result;
const task = JSON.parse(rawTask);
console.log('Отримано задачу:', task);Останній аргумент 0 означає чекати без обмеження часу. Якщо вказати, наприклад, 5, команда чекатиме не більше п’яти секунд.
BRPOP видаляє елемент зі списку одразу після отримання. Якщо процес завершиться під час обробки, задача може бути втрачена.
Така схема підходить для задач, які можна безпечно повторити або відновити іншим способом. Для критичних задач потрібна окрема схема підтвердження обробки, а не звичайне видалення елемента з черги.
Кілька екземплярів Node.js можуть одночасно намагатися виконати одну й ту саму операцію. Наприклад, усі процеси можуть захотіти оновити один ресурс.
Redis дозволяє атомарно створити ключ лише тоді, коли його ще немає:
const lockValue = 'unique-process-token';
const acquired = await client.set(
'lock:report-generation',
lockValue,
{
NX: true,
EX: 30
}
);
if (acquired === 'OK') {
console.log('Блокування отримано');
} else {
console.log('Ресурс уже зайнятий');
}NX означає «встановити лише якщо ключ не існує»;
EX: 30 автоматично видалить блокування через 30 секунд.
TTL обов’язковий: якщо процес аварійно завершиться, блокування не повинно залишитися назавжди.
Не можна безумовно викликати DEL для ключа блокування. Інший процес міг уже отримати нове блокування після завершення попереднього TTL. Потрібно спочатку перевірити, що значення ключа належить саме поточному процесу.
Redis виконує Lua-скрипт атомарно:
const releaseLockScript = `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
end
return 0
`;
await client.sendCommand([
'EVAL',
releaseLockScript,
'1',
'lock:report-generation',
lockValue
]);Тут:
KEYS[1] — ключ блокування;
ARGV[1] — унікальне значення поточного процесу;
ключ видаляється лише за збігу значень.
Наведений приклад демонструє:
збереження JSON із TTL;
роботу з хешем;
додавання та отримання задачі з черги;
отримання й безпечне зняття блокування.
const { createClient } = require('redis');
const { randomUUID } = require('node:crypto');
const client = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});
client.on('error', (error) => {
console.error('Помилка Redis:', error);
});
const sleep = (milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
async function main() {
await client.connect();
// Для повторного запуску прикладу видаляємо його демонстраційні ключі.
await client.del(
'cache:user:42',
'user:42',
'queue:emails',
'lock:report-generation'
);
// Кешування JSON із терміном дії 60 секунд.
const profile = {
id: 42,
name: 'Олена',
role: 'admin'
};
await client.set(
'cache:user:42',
JSON.stringify(profile),
{ EX: 60 }
);
const cachedProfile = await client.get('cache:user:42');
console.log('Кеш:', JSON.parse(cachedProfile));
// Зберігання полів користувача в хеші.
await client.hSet('user:42', {
name: 'Олена',
loginCount: '0'
});
await client.hIncrBy('user:42', 'loginCount', 1);
console.log('Користувач:', await client.hGetAll('user:42'));
// Додавання задачі до черги.
const task = {
type: 'send-email',
userId: 42,
template: 'welcome'
};
await client.rPush('queue:emails', JSON.stringify(task));
// Очікування та отримання першої задачі.
const queueResult = await client.sendCommand([
'BRPOP',
'queue:emails',
'5'
]);
if (queueResult) {
const [, rawTask] = queueResult;
console.log('Задача з черги:', JSON.parse(rawTask));
} else {
console.log('Черга порожня протягом 5 секунд');
}
// Спроба отримати розподілене блокування.
const lockKey = 'lock:report-generation';
const lockValue = randomUUID();
const lockResult = await client.set(
lockKey,
lockValue,
{
NX: true,
EX: 30
}
);
if (lockResult === 'OK') {
try {
console.log('Блокування отримано');
// Імітація роботи, яку має виконувати лише один процес.
await sleep(500);
console.log('Генерацію звіту завершено');
} finally {
// Видаляємо блокування лише якщо воно належить цьому процесу.
const releaseScript = `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
end
return 0
`;
await client.sendCommand([
'EVAL',
releaseScript,
'1',
lockKey,
lockValue
]);
console.log('Блокування звільнено');
}
} else {
console.log('Інший процес уже виконує цю операцію');
}
await client.quit();
}
main().catch(async (error) => {
console.error(error);
if (client.isOpen) {
await client.quit();
}
process.exitCode = 1;
});Запустити приклад можна командою:
node redis-example.jsВикористовуйте зрозумілі префікси ключів: user:, cache:, queue:, lock:.
Для кешу та блокувань завжди задавайте TTL.
Пам’ятайте, що значення GET і поля хешів повертаються як рядки.
Серіалізуйте об’єкти через JSON.stringify, а після читання використовуйте JSON.parse.
Не видаляйте блокування без перевірки його унікального значення.
Не вважайте просту чергу на списках надійною системою доставки: після BRPOP задача вже вилучена.
Реєструйте обробник події error для клієнта Redis.
Закривайте з’єднання через quit() під час коректного завершення програми.
await client.connect()Команди не можна виконувати до встановлення з’єднання:
await client.connect();
await client.set('key', 'value');Redis не зберігає JavaScript-об’єкти безпосередньо:
await client.set('user:42', JSON.stringify(user));Під час читання потрібно виконати зворотне перетворення:
const user = JSON.parse(await client.get('user:42'));Якщо кеш не має TTL, він не очищатиметься автоматично:
await client.set('cache:data', value, { EX: 300 });Команда DEL без перевірки значення може видалити блокування, яке вже належить іншому процесу. Для зняття блокування використовуйте атомарну перевірку та видалення.
Якщо процес завершиться після встановлення блокування, ключ може залишитися назавжди. Параметр EX захищає від такої ситуації.
Пакет redis підключає Node.js до сервера Redis.
Рядки підходять для простих значень і JSON.
Хеші зручні для плоских об’єктів і часткових оновлень.
TTL автоматично видаляє тимчасові дані.
Списки Redis можна використовувати для простих черг через RPUSH і BRPOP.
Комбінація SET NX EX створює розподілене блокування з автоматичним завершенням.
Блокування потрібно знімати лише після перевірки унікального значення.
Redis-команди треба виконувати після підключення, а помилки з’єднання — обробляти.