Node.js Caching: Redis ile Performans Artırma

Node.js Caching: Redis ile Performans Artırma
Caching, sıkça erişilen verileri hızlı erişilebilen bir katmanda saklayarak uygulama performansını dramatik şekilde artıran bir tekniktir. Redis, in-memory veri yapısı deposu olarak Node.js uygulamalarında en yaygın kullanılan caching çözümüdür. Bu rehberde Redis ile caching stratejilerini, implementasyonlarını ve best practice'leri detaylı olarak inceleyeceğiz.
1. Neden Caching?
- Hız: Veritabanı sorguları ms-saniye seviyesindeyken Redis mikrosaniye seviyesinde yanıt verir
- Veritabanı yükü azaltma: Tekrarlayan sorguların DB'ye ulaşmasını engeller
- Maliyet: Daha az DB kaynağı tüketimi
- Ölçeklenebilirlik: Yüksek trafiği daha az sunucu ile karşılama
2. Redis Kurulumu ve Bağlantı
# Redis kurulumu (Docker ile)
docker run -d --name redis -p 6379:6379 redis:7-alpine
# Node.js client kurulumu
npm install ioredis
// redis.js — Bağlantı modülü
const Redis = require('ioredis');
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD || undefined,
maxRetriesPerRequest: 3,
retryStrategy(times) {
const delay = Math.min(times * 50, 2000);
return delay;
},
lazyConnect: true,
});
redis.on('connect', () => console.log('Redis bağlantısı kuruldu'));
redis.on('error', (err) => console.error('Redis hatası:', err.message));
module.exports = redis;
3. Temel Cache Stratejileri
Cache-Aside (Lazy Loading)
En yaygın strateji. Veri önce cache'den aranır, yoksa veritabanından alınıp cache'e yazılır.
const redis = require('./redis');
const db = require('./database');
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// 1. Cache'i kontrol et
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 2. Veritabanından al
const user = await db.findUser(userId);
if (!user) return null;
// 3. Cache'e yaz (TTL: 1 saat)
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
return user;
}
Write-Through
Veri yazıldığında hem veritabanına hem cache'e aynı anda yazılır.
async function updateUser(userId, data) {
const cacheKey = `user:${userId}`;
// 1. Veritabanını güncelle
const user = await db.updateUser(userId, data);
// 2. Cache'i güncelle
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
return user;
}
Write-Behind (Write-Back)
Veri önce cache'e yazılır, daha sonra asenkron olarak veritabanına aktarılır. Yüksek yazma performansı sağlar.
// Write-behind queue
async function updateUserAsync(userId, data) {
const cacheKey = `user:${userId}`;
// Cache'i hemen güncelle
const merged = { ...JSON.parse(await redis.get(cacheKey) || '{}'), ...data };
await redis.set(cacheKey, JSON.stringify(merged), 'EX', 3600);
// Kuyruğa ekle — arka planda DB'ye yazılacak
await redis.lpush('db:write_queue', JSON.stringify({
collection: 'users',
id: userId,
data: merged,
timestamp: Date.now(),
}));
}
// Worker — kuyruğu işle
async function processWriteQueue() {
while (true) {
const item = await redis.brpop('db:write_queue', 5);
if (item) {
const task = JSON.parse(item[1]);
await db.update(task.collection, task.id, task.data);
}
}
}
4. Cache Middleware
// middleware/cache.js
const redis = require('../redis');
function cacheMiddleware(options = {}) {
const { ttl = 300, keyPrefix = 'api' } = options;
return async (req, res, next) => {
// Sadece GET isteklerini cache'le
if (req.method !== 'GET') return next();
const cacheKey = `${keyPrefix}:${req.originalUrl}`;
try {
const cached = await redis.get(cacheKey);
if (cached) {
const data = JSON.parse(cached);
res.set('X-Cache', 'HIT');
return res.json(data);
}
} catch (err) {
console.error('Cache okuma hatası:', err.message);
}
// Orijinal res.json'ı yakala
const originalJson = res.json.bind(res);
res.json = async (data) => {
res.set('X-Cache', 'MISS');
// Cache'e yaz (hata olsa bile response'u engelleme)
try {
await redis.set(cacheKey, JSON.stringify(data), 'EX', ttl);
} catch (err) {
console.error('Cache yazma hatası:', err.message);
}
return originalJson(data);
};
next();
};
}
// Kullanım
const app = require('express')();
app.get('/api/products', cacheMiddleware({ ttl: 600 }), async (req, res) => {
const products = await db.getProducts();
res.json(products);
});
app.get('/api/users/:id', cacheMiddleware({ ttl: 300 }), async (req, res) => {
const user = await db.getUser(req.params.id);
res.json(user);
});
5. Cache Invalidation Stratejileri
"Cache invalidation" bilgisayar biliminin en zor iki probleminden biridir. Doğru strateji kritiktir.
// 1. TTL (Time-To-Live) — En basit yöntem
await redis.set('key', 'value', 'EX', 3600); // 1 saat sonra otomatik silinir
// 2. Manuel invalidation
async function deleteUserCache(userId) {
await redis.del(`user:${userId}`);
}
// 3. Pattern bazlı invalidation
async function clearUserCaches(userId) {
const keys = await redis.keys(`user:${userId}:*`);
if (keys.length > 0) {
await redis.del(...keys);
}
}
// 4. Tag bazlı invalidation
async function setWithTags(key, value, ttl, tags) {
const pipeline = redis.pipeline();
pipeline.set(key, JSON.stringify(value), 'EX', ttl);
for (const tag of tags) {
pipeline.sadd(`tag:${tag}`, key);
pipeline.expire(`tag:${tag}`, ttl);
}
await pipeline.exec();
}
async function invalidateByTag(tag) {
const keys = await redis.smembers(`tag:${tag}`);
if (keys.length > 0) {
await redis.del(...keys, `tag:${tag}`);
}
}
// Kullanım
await setWithTags('product:1', product, 3600, ['products', 'category:electronics']);
await invalidateByTag('products'); // Tüm ürün cache'ini temizle
redis.keys() kullanmaktan kaçının. Bu komut tüm key'leri tarar ve Redis'i bloke edebilir. Bunun yerine SCAN komutunu kullanın.
6. Redis Veri Yapıları ile Cache
// Hash — nesne alanlarını ayrı cache'leme
await redis.hset('user:1', {
name: 'Tarık',
email: 'tarik@example.com',
role: 'admin',
});
const name = await redis.hget('user:1', 'name');
const user = await redis.hgetall('user:1');
// Sorted Set — sıralı listeler (leaderboard, trending)
await redis.zadd('trending:posts', Date.now(), 'post:123');
const topPosts = await redis.zrevrange('trending:posts', 0, 9); // İlk 10
// List — kuyruk ve son aktiviteler
await redis.lpush('recent:activity', JSON.stringify({ action: 'login', userId: 1 }));
await redis.ltrim('recent:activity', 0, 99); // Son 100 kayıt
const recent = await redis.lrange('recent:activity', 0, 9);
7. Cache Stampede Koruması
Popüler bir cache key expire olduğunda, yüzlerce istek aynı anda veritabanına gider. Bu "cache stampede" problemidir.
// Mutex (lock) ile stampede koruması
async function getWithLock(key, fetchFn, ttl = 3600) {
// Cache'i kontrol et
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
// Lock al
const lockKey = `lock:${key}`;
const lockAcquired = await redis.set(lockKey, '1', 'EX', 10, 'NX');
if (lockAcquired) {
try {
// Veriyi getir ve cache'le
const data = await fetchFn();
await redis.set(key, JSON.stringify(data), 'EX', ttl);
return data;
} finally {
await redis.del(lockKey);
}
} else {
// Lock alınamadı — kısa bekle ve tekrar dene
await new Promise(resolve => setTimeout(resolve, 100));
return getWithLock(key, fetchFn, ttl);
}
}
// Kullanım
const products = await getWithLock('products:all', () => db.getProducts(), 600);
8. Monitoring ve Metrikler
// Cache hit/miss oranı takibi
class CacheMetrics {
constructor() {
this.hits = 0;
this.misses = 0;
}
recordHit() { this.hits++; }
recordMiss() { this.misses++; }
getStats() {
const total = this.hits + this.misses;
return {
hits: this.hits,
misses: this.misses,
hitRate: total > 0 ? ((this.hits / total) * 100).toFixed(2) + '%' : '0%',
};
}
}
// Redis bilgileri
async function getRedisInfo() {
const info = await redis.info('memory');
const keyspace = await redis.info('keyspace');
return { memory: info, keyspace };
}
Sonuç
Redis ile caching, Node.js uygulamalarının performansını katları ile artırabilir. Cache-aside pattern'i çoğu senaryo için idealdir. Cache invalidation stratejinizi iyi planlayın — TTL, manuel invalidation ve tag bazlı yaklaşımları duruma göre kullanın. Cache stampede koruması yüksek trafikli uygulamalarda kritiktir. Redis'in zengin veri yapılarını (hash, sorted set, list) kullanarak daha sofistike caching çözümleri oluşturabilirsiniz. Cache hit oranınızı izleyin ve %85+ hedefleyin.