Ответ
Использовал несколько стратегий кеширования в Node.js в зависимости от требований:
1. In-memory кеш для быстрых операций:
class MemoryCache {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}
set(key, value, customTtl) {
const expiry = Date.now() + (customTtl || this.ttl);
this.cache.set(key, { value, expiry });
// Автоочистка просроченных записей
setTimeout(() => {
if (this.cache.get(key)?.expiry < Date.now()) {
this.cache.delete(key);
}
}, customTtl || this.ttl);
}
get(key) {
const item = this.cache.get(key);
if (!item || item.expiry < Date.now()) {
this.cache.delete(key);
return null;
}
return item.value;
}
}
// Использование
const cache = new MemoryCache();
const user = cache.get(`user:${userId}`) || await fetchUser(userId);
2. Redis для распределенного кеширования:
const redis = require('redis');
const { promisify } = require('util');
class RedisCache {
constructor() {
this.client = redis.createClient({
url: process.env.REDIS_URL
});
this.getAsync = promisify(this.client.get).bind(this.client);
this.setExAsync = promisify(this.client.setex).bind(this.client);
}
async getOrSet(key, fetchFn, ttl = 3600) {
const cached = await this.getAsync(key);
if (cached) return JSON.parse(cached);
const data = await fetchFn();
await this.setExAsync(key, ttl, JSON.stringify(data));
return data;
}
}
// Пример с кешированием запросов к БД
app.get('/api/products', async (req, res) => {
const products = await redisCache.getOrSet(
`products:page:${req.query.page}`,
() => Product.find().skip(offset).limit(limit),
300 // 5 минут
);
res.json(products);
});
3. HTTP-кеширование с заголовками:
app.get('/api/static-data', (req, res) => {
const data = getStaticData();
// Устанавливаем заголовки кеширования
res.set({
'Cache-Control': 'public, max-age=3600', // 1 час
'ETag': generateETag(data),
'Last-Modified': new Date().toUTCString()
});
// Проверяем условные запросы
if (req.fresh) { // Используется middleware fresh
return res.status(304).end(); // Not Modified
}
res.json(data);
});
4. Кеширование на уровне БД:
- Query caching в Mongoose
- Connection pooling для повторного использования соединений
- DataLoader для batch-запросов в GraphQL
5. Стратегии инвалидации:
- TTL-based (время жизни)
- Write-through (обновление кеша при записи)
- Cache-aside (ленивая загрузка)
- Tag-based инвалидация для сложных зависимостей
Производительность:
- In-memory: ~0.1ms на операцию
- Redis: ~1-5ms (с учетом сетевой задержки)
- Без кеша: 50-500ms (зависит от источника данных)
Для production-приложений обычно комбинирую несколько подходов: in-memory для горячих данных, Redis для распределенного доступа, HTTP-кеширование для статики.
Ответ 18+ 🔞
А, ну вот, слушай, про кеширование в Node.js. Тема, блядь, важная, как соль в борще — без неё можно, но потом охуеешь от того, как всё медленно работает. Расскажу, как я обычно эту магию делаю, чтобы сервер не ложился как последний бздун при первой же нагрузке.
1. Кеш прямо в оперативке — для самых отчаянных. Это когда тебе надо, чтобы всё было быстрее, чем мысль «а не выпить ли?». Просто, как три копейки, но есть нюанс — память жрёт, и если сервер перезапустишь, всё накрывается медным тазом.
class MemoryCache {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}
set(key, value, customTtl) {
const expiry = Date.now() + (customTtl || this.ttl);
this.cache.set(key, { value, expiry });
// Автоочистка просроченных записей
setTimeout(() => {
if (this.cache.get(key)?.expiry < Date.now()) {
this.cache.delete(key);
}
}, customTtl || this.ttl);
}
get(key) {
const item = this.cache.get(key);
if (!item || item.expiry < Date.now()) {
this.cache.delete(key);
return null;
}
return item.value;
}
}
// Использование
const cache = new MemoryCache();
const user = cache.get(`user:${userId}`) || await fetchUser(userId);
Штука в том, что это — огонь для данных, которые тебе нужны чаще, чем ты моргаешь. Но если у тебя несколько инстансов сервера, то тут начинается пиздец, потому что у каждого своя память, и кеши синхронизировать — это тот ещё геморрой.
2. Redis — когда тебе уже серьёзно. Вот это уже взрослый подход. Все инстансы лезут в одну общую память, и никаких проблем. Правда, появляется сетевая задержка, но это, как говорится, доверия ебать ноль по сравнению с походом в базу данных.
const redis = require('redis');
const { promisify } = require('util');
class RedisCache {
constructor() {
this.client = redis.createClient({
url: process.env.REDIS_URL
});
this.getAsync = promisify(this.client.get).bind(this.client);
this.setExAsync = promisify(this.client.setex).bind(this.client);
}
async getOrSet(key, fetchFn, ttl = 3600) {
const cached = await this.getAsync(key);
if (cached) return JSON.parse(cached);
const data = await fetchFn();
await this.setExAsync(key, ttl, JSON.stringify(data));
return data;
}
}
// Пример с кешированием запросов к БД
app.get('/api/products', async (req, res) => {
const products = await redisCache.getOrSet(
`products:page:${req.query.page}`,
() => Product.find().skip(offset).limit(limit),
300 // 5 минут
);
res.json(products);
});
Красота в том, что ты пишешь один раз, а потом просто получаешь данные. Если в кеше есть — отдаём, нет — идём в базу, кладём в кеш и больше туда не ходим. Удивление пиздец, насколько это ускоряет жизнь.
3. HTTP-кеширование — чтобы браузеры не дергали тебя по пустякам. Это когда ты говоришь клиенту: «Чувак, у тебя уже есть эти данные, не позорься, не стучись ко мне каждую секунду». Ставишь правильные заголовки — и живёшь спокойно.
app.get('/api/static-data', (req, res) => {
const data = getStaticData();
// Устанавливаем заголовки кеширования
res.set({
'Cache-Control': 'public, max-age=3600', // 1 час
'ETag': generateETag(data),
'Last-Modified': new Date().toUTCString()
});
// Проверяем условные запросы
if (req.fresh) { // Используется middleware fresh
return res.status(304).end(); // Not Modified
}
res.json(data);
});
Если данные не поменялись, сервер просто шлёт статус «304 — не трогай старое», и всё. Трафик экономится, нагрузка падает. Просто, но гениально, ёпта.
4. Кеширование на уровне самой базы данных. Тут уже без твоего прямого участия, но знать надо. В Mongoose есть встроенные механизмы, Connection pooling не даёт открывать новое соединение на каждый чих, а для GraphQL есть DataLoader, который кучу мелких запросов собирает в один здоровенный. Волнение ебать, когда видишь, как это работает.
5. А самое главное — как это всё обновлять и когда сбрасывать. Вот тут уже надо думать головой, а не жопой. Стратегий куча:
- По времени (TTL) — проще пареной репы, поставил и забыл. Но данные могут устареть раньше.
- Write-through — обновляешь базу, заодно и кеш. Надёжно, но медленнее на запись.
- Cache-aside (Lazy Loading) — классика, которую все используют. Нет в кеше — пошёл взял из базы, положил в кеш. Есть риск «кеш-промаха» при первом запросе.
- Инвалидация по тегам — вот это для сложных зависимостей, когда данные связаны. Удалил товар — инвалидируешь все списки, где он фигурировал. Терпения ноль ебать настраивать, но потом живёшь как король.
Итог по скорости, чтобы ты понимал масштаб:
- Прямо в памяти: ~0.1 мс — быстрее, чем ты успел подумать.
- Redis: ~1-5 мс — с учётом того, что надо по сети сходить. Но всё равно в десятки раз быстрее базы.
- Без кеша вообще: 50-500 мс — и это в лучшем случае. А если база тупит, то и секунды.
Так что, чувак, в продакшене я обычно всё комбинирую. Что-то горячее — в память, что нужно между серверами — в Redis, а статику вообще браузерам кешировать велю. Главное — не переусердствовать, а то можно так закешировать, что потом сам не разберёшь, где актуальные данные, а где вчерашние.