Кэширование в JavaScript: Redis, Memcached и встроенные решения (продвинутый уровень)
В современной веб-разработке производительность приложения часто упирается в скорость доступа к данным. Каждый лишний запрос к базе данных или внешнему API увеличивает время ответа и нагрузку на сервер. Кэширование — это не просто "сохранить результат", а целая наука с выбором правильной стратегии, инструмента и механизма инвалидации. В этой статье мы разберём продвинутые техники кэширования в Node.js: от встроенных решений до промышленных систем Redis и Memcached.
1. Встроенное кэширование в Node.js: простота и ограничения
Прежде чем подключать внешние сервисы, стоит оценить возможности самого Node.js. Встроенное кэширование реализуется с помощью объектов Map или WeakMap, а также простых глобальных переменных. Это идеально для данных, которые редко меняются и живут в рамках одного процесса.
// Простейший кэш в памяти с TTL (Time To Live)
class MemoryCache {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}
set(key, value) {
this.cache.set(key, {
value,
expiry: Date.now() + this.ttl
});
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
return item.value;
}
delete(key) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
}
// Использование
const cache = new MemoryCache(30000); // 30 секунд TTL
cache.set('user:42', { name: 'Alice', role: 'admin' });
console.log(cache.get('user:42')); // { name: 'Alice', role: 'admin' }
Плюсы: нулевая задержка сети, простота реализации, не требует внешних зависимостей.
Минусы: данные теряются при перезапуске процесса, не масштабируется на несколько серверов, ограничено оперативной памятью одного узла.
Для микросервисов, работающих в одном процессе, или для кэширования результатов тяжелых вычислений (например, рендеринга шаблонов) встроенное решение — отличный выбор. Однако для распределённых систем его недостаточно.
2. Redis: король кэширования в Node.js
Redis — это in-memory хранилище данных, которое часто используется как высокопроизводительный кэш. Его главные преимущества: поддержка сложных структур данных (строки, списки, множества, хэши), встроенный TTL, pub/sub и персистентность на диск.
Подключение Redis в Node.js
Для работы с Redis в Node.js используется пакет ioredis (рекомендуется) или redis. Рассмотрим пример с ioredis:
const Redis = require('ioredis');
const redis = new Redis({
host: 'localhost',
port: 6379,
retryStrategy: (times) => Math.min(times * 50, 2000) // стратегия повторного подключения
});
// Базовые операции
await redis.set('mykey', 'Hello, Redis!', 'EX', 60); // TTL 60 секунд
const value = await redis.get('mykey');
console.log(value); // "Hello, Redis!"
// Использование хэша для хранения сложных объектов
await redis.hmset('user:100', {
name: 'Bob',
email: 'bob@example.com',
age: 30
});
const user = await redis.hgetall('user:100');
console.log(user); // { name: 'Bob', email: 'bob@example.com', age: '30' }
Продвинутые стратегии с Redis
Cache-Aside (Lazy Loading): приложение сначала проверяет кэш, и только при промахе обращается к базе данных и заполняет кэш.
async function getUser(userId) {
const cacheKey = `user:${userId}`;
let user = await redis.get(cacheKey);
if (user) {
return JSON.parse(user);
}
// Промах кэша — загружаем из БД
user = await db.findUserById(userId);
if (user) {
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); // кэш на 1 час
}
return user;
}
Write-Through и Write-Behind: запись данных всегда проходит через кэш. При Write-Through данные синхронно записываются в БД, при Write-Behind — асинхронно с задержкой. Это повышает консистентность, но требует аккуратной реализации.
Инвалидация кэша: одна из сложнейших задач. Используйте версионирование ключей (например, user:42:v2), установку короткого TTL для "горячих" данных или паттерн "публикация-подписка" для уведомления других сервисов об изменениях.
// Инвалидация через pub/sub
const subscriber = new Redis();
subscriber.subscribe('cache:invalidate');
subscriber.on('message', (channel, message) => {
if (channel === 'cache:invalidate') {
const key = message;
redis.del(key);
console.log(`Кэш инвалидирован: ${key}`);
}
});
// Публикация при изменении данных
await redis.publish('cache:invalidate', 'user:42');
3. Memcached: легковесная альтернатива для простых сценариев
Memcached — это распределённая система кэширования, которая проще Redis, но уступает ему в функциональности. Memcached поддерживает только строковые значения, не имеет персистентности и сложных структур данных. Однако он быстрее в простых операциях get/set и потребляет меньше памяти на служебные нужды.
Работа с Memcached в Node.js
const memjs = require('memjs');
const mc = memjs.Client.create();
// Запись с TTL (в секундах)
await mc.set('mykey', 'Hello, Memcached!', { expires: 60 });
// Чтение
const { value } = await mc.get('mykey');
console.log(value ? value.toString() : null); // "Hello, Memcached!"
// Удаление
await mc.delete('mykey');
Когда выбирать Memcached? Если ваши данные — это простые строки (например, HTML-фрагменты, сериализованные JSON-объекты), и вам не нужны транзакции, pub/sub или персистентность. Memcached отлично подходит для кэширования результатов SQL-запросов или сессий в простых архитектурах.