Performance & Scalability serimizin bir parçası
Tam kılavuzu okuyunWeb Uygulamaları için Redis Önbelleğe Alma Desenleri
Veritabanı sorguları web uygulamalarında en yaygın performans darboğazıdır. 1.000 eş zamanlı kullanıcı için her sayfa yüklemesinde çağrılan, 200 ms süren, kötü indekslenmiş bir sorgu, saniyede 200 saniyelik veritabanı CPU'su üretir; bu bir ölüm sarmalıdır. Redis bunun panzehiridir: normalde aynı veriler için veritabanınıza tekrar tekrar gelebilecek okuma yükünü emen, milisaniyenin altındaki bir bellek içi depo.
Ancak önbelleğe alma yalnızca "bir şeyleri Redis'e koymak" değildir. Yanlış kalıp, eski veri hatalarına, büyük sürü felaketlerine veya sınırsız hafıza büyümesine neden olur. Bu kılavuz, dört standart önbellekleme modelini, önbellek geçersiz kılma stratejilerini, izdiham önlemeyi ve üretim NestJS uygulamasını kapsar; böylece başlangıçtan itibaren doğru şekilde önbellekleyebilirsiniz.
Önemli Çıkarımlar
- Önbellek ayırma (tembel yükleme) en güvenli varsayılandır; yalnızca gerçekte istenenleri önbelleğe alır
- İçe yazma, önbellek ve veritabanını senkronize halde tutar ancak yazma gecikmesini artırır; sık okunan, seyrek yazılan veriler için kullanın
- Önbellek damgası (gürültü sürüsü), popüler bir anahtarın süresi dolduğunda veritabanlarını yok eder; olasılığa dayalı erken son kullanma tarihi veya muteks kilitleri kullanın
- Her anahtarda TTL ayarlanmalıdır — Redis'in sınırsız büyümesi, gerçekleşmesini bekleyen bir olaydır
- Önbelleği geçersiz kılmak zordur: mükemmel şekilde geçersiz kılmaya çalışmak yerine kısa TTL'leri ve olaya dayalı geçersiz kılmayı tercih edin
- Önbellek anahtarları tüm sorgu boyutlarını içermelidir:
contacts:{orgId}:{page}:{limit}:{search}:{sortField}:{sortDir}- Toplu geçersiz kılmayı etkinleştirmek için Redis ad alanlarını ve anahtar kalıplarını kullanın
- Önbellek isabet oranını izleyin; %80'in altı, TTL'lerinizin çok kısa olduğu veya anahtar yapınızın yanlış olduğu anlamına gelir
NestJS'de Redis Kurulumu
pnpm add ioredis @nestjs-modules/ioredis
// src/modules/cache/cache.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
@Injectable()
export class CacheService {
private readonly logger = new Logger(CacheService.name);
constructor(@InjectRedis() private readonly redis: Redis) {}
async get<T>(key: string): Promise<T | null> {
const value = await this.redis.get(key);
if (!value) return null;
return JSON.parse(value) as T;
}
async set<T>(key: string, value: T, ttlSeconds = 300): Promise<void> {
await this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
}
async del(key: string): Promise<void> {
await this.redis.del(key);
}
// Use SCAN instead of KEYS to avoid blocking Redis on large key spaces
async invalidatePattern(pattern: string): Promise<void> {
let cursor = '0';
do {
const [nextCursor, keys] = await this.redis.scan(
cursor,
'MATCH', pattern,
'COUNT', 100
);
cursor = nextCursor;
if (keys.length > 0) {
await this.redis.del(...keys);
}
} while (cursor !== '0');
}
async remember<T>(
key: string,
ttlSeconds: number,
factory: () => Promise<T>
): Promise<T> {
const cached = await this.get<T>(key);
if (cached !== null) return cached;
const value = await factory();
await this.set(key, value, ttlSeconds);
return value;
}
}
// src/modules/cache/cache.module.ts
import { Global, Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-modules/ioredis';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CacheService } from './cache.service';
@Global()
@Module({
imports: [
RedisModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'single',
options: {
host: config.get('REDIS_HOST', 'localhost'),
port: config.get<number>('REDIS_PORT', 6379),
password: config.get('REDIS_PASSWORD'),
db: config.get<number>('REDIS_DB', 0),
keyPrefix: 'ecosire:',
},
}),
}),
],
providers: [CacheService],
exports: [CacheService],
})
export class AppCacheModule {}
Desen 1: Önbellek Kenarı (Tembel Yükleme)
En yaygın desen. Önbellekten oku; kaçırıldığında, veritabanından okuyun ve önbelleği doldurun.
// contacts.service.ts
async findAll(orgId: string, page: number, limit: number, search?: string) {
const cacheKey = `contacts:${orgId}:${page}:${limit}:${search ?? 'all'}`;
return this.cache.remember(cacheKey, 300, async () => {
const results = await this.db.query.contacts.findMany({
where: and(
eq(contacts.organizationId, orgId),
search
? or(
ilike(contacts.name, `%${search}%`),
ilike(contacts.email, `%${search}%`)
)
: undefined
),
limit,
offset: (page - 1) * limit,
orderBy: desc(contacts.createdAt),
});
return results;
});
}
Artıları: Basit; önbellek yalnızca gerçekte istenenleri içerir. Eksileri: İlk istek her zaman yavaştır (önbellek kaybı); TTL'nin süresi dolana kadar veriler eski olabilir.
Ne zaman geçersiz kılınmalı: Oluşturma/güncelleme/silme işleminden sonra contacts:{orgId}:* ile eşleşen tüm anahtarları silin.
Desen 2: İçe Yazma
Her yazma işleminde önbelleği veritabanıyla aynı anda güncelleyin. İşlemler okuma tutarlılığı için gecikme yazar.
async update(orgId: string, contactId: string, dto: UpdateContactDto) {
// 1. Update the database
const [updated] = await this.db
.update(contacts)
.set({ ...dto, updatedAt: new Date() })
.where(
and(
eq(contacts.id, contactId),
eq(contacts.organizationId, orgId)
)
)
.returning();
// 2. Update the individual contact cache immediately (write-through)
const singleKey = `contact:${orgId}:${contactId}`;
await this.cache.set(singleKey, updated, 3600);
// 3. Invalidate list caches (they now contain stale order/counts)
await this.cache.invalidatePattern(`contacts:${orgId}:*`);
return updated;
}
Ne zaman kullanılmalı: Sık okunan, seyrek yazılan kayıtlar (kullanıcı profilleri, ayarlar, ürün kataloğu).
Desen 3: Önbellek Sıkışmasını Önleme
Popüler bir önbellek anahtarının süresi dolduğunda, yüzlerce eşzamanlı isteğin tümü aynı anda önbelleği kaçırır ve tümü veritabanına - "gürleyen sürüye" - ulaşır. Ani yük altında veritabanı çöküyor.
Mutex Kilit Deseni
Yalnızca bir istek önbelleği yeniden oluşturur; diğerleri bekleyip yeniden dener:
// cache.service.ts — mutex-protected cache fetch
async getWithLock<T>(
key: string,
ttlSeconds: number,
factory: () => Promise<T>
): Promise<T> {
// Try cache first
const cached = await this.get<T>(key);
if (cached !== null) return cached;
const lockKey = `lock:${key}`;
const lockOwner = `${process.pid}-${Date.now()}`;
// Try to acquire lock: NX = only set if not exists, EX = expire in 30s
const acquired = await this.redis.set(lockKey, lockOwner, 'NX', 'EX', 30);
if (acquired === 'OK') {
try {
const value = await factory();
await this.set(key, value, ttlSeconds);
return value;
} finally {
// Atomic check-and-delete: only release if we still own the lock
// Uses Lua defined with defineCommand for atomicity
const currentOwner = await this.redis.get(lockKey);
if (currentOwner === lockOwner) {
await this.redis.del(lockKey);
}
}
} else {
// Another process is rebuilding — wait 100ms and retry
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 100);
// Store timer reference so Node.js does not block on it
timer.unref?.();
});
return this.getWithLock(key, ttlSeconds, factory);
}
}
Olasılığa Dayalı Erken Sona Erme (XFetch)
Yeniden hesaplama yükünü yayarak, süresi dolmadan önce önbelleği olasılıksal olarak yeniden oluşturmaya başlayın:
async getWithEarlyExpiration<T>(
key: string,
ttlSeconds: number,
factory: () => Promise<T>,
beta = 1 // Higher = more aggressive early recomputation
): Promise<T> {
const metaKey = `${key}:meta`;
const meta = await this.get<{ createdAt: number; ttl: number }>(metaKey);
const value = await this.get<T>(key);
if (value !== null && meta) {
const elapsed = Date.now() / 1000 - meta.createdAt;
const remaining = meta.ttl - elapsed;
// XFetch: trigger early recomputation probabilistically
const shouldRecompute = -Math.log(Math.random()) * beta > remaining;
if (!shouldRecompute) return value;
}
const newValue = await factory();
const now = Date.now() / 1000;
await this.set(key, newValue, ttlSeconds);
await this.set(metaKey, { createdAt: now, ttl: ttlSeconds }, ttlSeconds + 60);
return newValue;
}
Desen 4: Etiket Tabanlı Önbelleği Geçersiz Kılma
Önbellek anahtarlarını mantıksal etiketlere göre gruplayın ve etiketin tamamını tek seferde geçersiz kılın:
// Tag-based invalidation using Redis sets
async setWithTags(
key: string,
value: unknown,
ttlSeconds: number,
tags: string[]
): Promise<void> {
await this.set(key, value, ttlSeconds);
// Add key to each tag's member set using a pipeline
const pipeline = this.redis.pipeline();
for (const tag of tags) {
pipeline.sadd(`tag:${tag}`, key);
pipeline.expire(`tag:${tag}`, ttlSeconds + 60);
}
await pipeline.exec();
}
async invalidateByTag(tag: string): Promise<void> {
const keys = await this.redis.smembers(`tag:${tag}`);
if (keys.length > 0) {
const pipeline = this.redis.pipeline();
for (const cacheKey of keys) {
pipeline.del(cacheKey);
}
pipeline.del(`tag:${tag}`);
await pipeline.exec();
}
}
// Usage
await this.setWithTags(
`contacts:${orgId}:page:1`,
contacts,
300,
[`org:${orgId}`, 'contacts']
);
// After any contact mutation in org_123:
await this.invalidateByTag(`org:${orgId}`);
Oturum ve Kimlik Doğrulama Önbelleğe Alma
Redis, kimlik doğrulama oturumu depolaması için idealdir; her istekte veritabanı aramalarını ortadan kaldırır:
// Cache user session data (role, permissions, orgId) for JWT validation
async cacheUserSession(userId: string, session: UserSession): Promise<void> {
const key = `session:${userId}`;
await this.redis.hset(key,
'id', session.id,
'email', session.email,
'role', session.role,
'organizationId', session.organizationId,
'tokenVersion', String(session.tokenVersion)
);
await this.redis.expire(key, 900); // 15 minutes — matches access token TTL
}
async getUserSession(userId: string): Promise<UserSession | null> {
const data = await this.redis.hgetall(`session:${userId}`);
if (!data || Object.keys(data).length === 0) return null;
return {
...data,
tokenVersion: parseInt(data.tokenVersion, 10),
} as UserSession;
}
async invalidateUserSession(userId: string): Promise<void> {
await this.redis.del(`session:${userId}`);
}
Veri Türüne Göre TTL Stratejisi
| Veri Türü | Önerilen TTL | Geçersiz Kılma Stratejisi |
|---|---|---|
| Kullanıcı profili | 15 dakika | Profil güncellemesinde |
| Organizasyon ayarları | 1 saat | Ayarlar değişikliğinde |
| Ürün kataloğu | 24 saat | Ürün oluşturma/güncelleme hakkında |
| Blog yazısı listesi | 10 dakika | Yayınlandıktan sonra |
| API oran sınırı sayacı | Pencere başına (60s) | Otomatik sona erme |
| Yetkilendirme oturumu | 15 dakika | Oturum kapatıldığında veya jeton iptal edildiğinde |
| Arama sonuçları | 5 dakika | Yalnızca TTL |
| Toplu ölçümler | 1 dakika | Yalnızca TTL |
| Tek kullanımlık kodlar (yetkilendirme) | 60 saniye | Kullandıktan veya son kullanma tarihi geçtikten sonra |
| Sayfalandırılmış liste sorgusu | 5 dakika | org'daki herhangi bir mutasyonda |
Önbellek Performansını İzleme
// Add hit/miss tracking to CacheService
async get<T>(key: string): Promise<T | null> {
const value = await this.redis.get(key);
if (value) {
await this.redis.incr('metrics:cache_hits');
return JSON.parse(value) as T;
}
await this.redis.incr('metrics:cache_misses');
return null;
}
// Report hit rate every 5 minutes
@Cron('*/5 * * * *')
async reportCacheMetrics(): Promise<void> {
const [hits, misses] = await this.redis.mget(
'metrics:cache_hits',
'metrics:cache_misses'
);
const h = parseInt(hits ?? '0', 10);
const m = parseInt(misses ?? '0', 10);
const total = h + m;
const hitRate = total === 0 ? 1 : h / total;
if (hitRate < 0.8) {
this.logger.warn(`Low cache hit rate: ${(hitRate * 100).toFixed(1)}%`);
}
// Reset counters
await this.redis.set('metrics:cache_hits', '0');
await this.redis.set('metrics:cache_misses', '0');
}
redis-cli INFO stats aracılığıyla izlenecek temel Redis metrikleri:
keyspace_hits/keyspace_misses— küresel isabet oranı (hedef %80'in üzerinde)used_memory— sınırsız büyümeyi izleyinevicted_keys— maxmemory politikası tarafından çıkarılan anahtarlar (önbellek için 0'a yakın olmalıdır)connected_clients— bağlantı havuzu kullanımıinstantaneous_ops_per_sec— mevcut verim
Sıkça Sorulan Sorular
Önbellek için hangi maksimum bellek politikasını kullanmalıyım?
allkeys-lru (tüm anahtarlardan en son kullanılan anahtarları çıkar) veya volatile-lru (TTL ayarına sahip LRU anahtarlarını çıkar) kullanın. Saf önbellek iş yükü için allkeys-lru standarttır — Redis, bellek dolduğunda otomatik olarak soğuk anahtarları çıkarır. Hiçbir zaman önbellek için noeviction kullanmayın; bellek dolduğunda eski verileri çıkarmak yerine hatalar döndürür.
Hassas verilerin Redis'te saklanmasını nasıl önleyebilirim?
Ham şifreleri, özel anahtarları, ödeme kartı verilerini veya SSN'leri asla önbelleğe almayın. Kimlik doğrulama oturumları için minimum veriyi önbelleğe alın: userId, role, organizasyonId, tokenVersion; tam JWT verileri veya API kimlik bilgileri değil. Üretimde Redis AUTH ve TLS'yi etkinleştirin. Redis örneğinizin güvenliği ihlal edilirse bu disiplini takip ettiğinizde yalnızca meta veriler açığa çıkar.
Desen silmek için SCAN ve ANAHTARLAR arasındaki fark nedir?
KEYS pattern, tarama sırasında tüm Redis komutlarını duraklatan, engelleyici bir O(n) işlemidir; büyük anahtar alanlarında saniyeler süren kesintilere neden olabilir. SCAN engellemesizdir ve imleçle küçük parçalar halinde yinelenir. Üretim modelini silmek için her zaman SCAN kullanın. Buradaki ödünleşim, SCAN'ün tarama sırasında eklenmesi veya silinmesi durumunda tüm anahtarları döndürmemesidir; bu, önbellek geçersiz kılma için kabul edilebilir.
Önbelleğe alma, hız sınırlama ve oturumlar için ayrı bir Redis örneği kullanmalı mıyım?
Çoğu uygulama için farklı anahtar öneklerine sahip bir Redis örneği uygundur. Ayrı örnekler şu durumlarda anlamlıdır: önbellek, hız sınırı deposundan farklı bir maxmemory-policy gerektirdiğinde (önbellek: allkeys-lru, hız sınırları: noeviction) veya bağımsız ölçeklendirme ve yük devretmeye ihtiyaç duyduğunuzda. Uygun ölçekte, iş yükü türü başına ayrı kümelerle Redis Cluster'ı kullanın.
Sayfalara ayrılmış liste sorguları için önbelleğin geçersiz kılınmasını nasıl hallederim?
Sayfalandırılmış liste önbellekleri yanıltıcıdır; 1. sayfaya bir kişi eklemek, 2. ve sonraki sayfalardaki tüm kişileri kaydırır. Pragmatik çözüm: kısa TTL'ler kullanın (2-5 dakika) ve kalıp geçersiz kılmayı (contacts:{orgId}:*) kullanarak herhangi bir yazma işleminde kuruluşa ait tüm sayfaları geçersiz kılın. Ağır yazma hacimlerine sahip büyük kuruluşlar için, önbelleğe alma sayfalandırmasını tamamen atlayın ve bunun yerine veritabanı düzeyinde optimizasyona (uygun dizinler, dizinleri kapsayan) güvenin.
Vitest'te önbellek davranışını nasıl test ederim?
İsabet/ıskalama senaryoları için uygun değerleri döndüren vi.fn() ile birim testlerinde CacheService'i taklit edin. Entegrasyon testleri için gerçek bir Redis örneği (Docker) kullanın ve temizlemeye başlamak için beforeEach içinde redis.flushdb() kullanın. Hem önbellek isabet yolunu (veritabanı çağrısı yok) hem de önbellek kaçırma yolunu (veritabanı çağrıldı + önbellek dolduruldu) açıkça test edin.
Sonraki Adımlar
Redis'in önbelleğe alınması, bir web uygulamasındaki en yüksek yatırım getirisi performansına sahip yatırımlardan biridir. 200 ms'lik bir veritabanı sorgusu ile 2 ms'lik bir Redis isabeti arasındaki fark, ölçekte 100 kattır ve bu, doğrudan altyapı maliyet tasarrufu ve kullanıcı deneyiminin iyileştirilmesi anlamına gelir.
ECOSIRE, her NestJS projesinde önbellek ayırma, yazma yoluyla geçersiz kılma, izdiham önleme ve önbellek isabet oranı izleme özellikleriyle Redis önbelleğe almayı uygular. Performanslı, ölçeklenebilir API'leri nasıl tasarladığımızı öğrenmek için arka uç mühendislik hizmetlerimizi keşfedin.
Yazan
ECOSIRE Research and Development Team
ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.
İlgili Makaleler
API Rate Limiting: Patterns and Best Practices
Master API rate limiting with token bucket, sliding window, and fixed counter patterns. Protect your backend with NestJS throttler, Redis, and real-world configuration examples.
k6 Load Testing: Stress-Test Your APIs Before Launch
Master k6 load testing for Node.js APIs. Covers virtual user ramp-ups, thresholds, scenarios, HTTP/2, WebSocket testing, Grafana dashboards, and CI integration patterns.
NestJS 11 Enterprise API Patterns
Master NestJS 11 enterprise patterns: guards, interceptors, pipes, multi-tenancy, and production-ready API design for scalable backend systems.
Performance & Scalability serisinden daha fazlası
k6 Load Testing: Stress-Test Your APIs Before Launch
Master k6 load testing for Node.js APIs. Covers virtual user ramp-ups, thresholds, scenarios, HTTP/2, WebSocket testing, Grafana dashboards, and CI integration patterns.
Nginx Production Configuration: SSL, Caching, and Security
Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.
Odoo Performance Tuning: PostgreSQL and Server Optimization
Expert guide to Odoo 19 performance tuning. Covers PostgreSQL configuration, indexing, query optimization, Nginx caching, and server sizing for enterprise deployments.
Odoo vs Acumatica: Cloud ERP for Growing Businesses
Odoo vs Acumatica compared for 2026: unique pricing models, scalability, manufacturing depth, and which cloud ERP fits your growth trajectory.
Testing and Monitoring AI Agents in Production
A complete guide to testing and monitoring AI agents in production environments. Covers evaluation frameworks, observability, drift detection, and incident response for OpenClaw deployments.
Compliance Monitoring Agents with OpenClaw
Deploy OpenClaw AI agents for continuous compliance monitoring. Automate regulatory checks, policy enforcement, audit trail generation, and compliance reporting.