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 TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
ECOSIRE ile İşinizi Büyütün
ERP, e-Ticaret, yapay zeka, analitik ve otomasyon genelinde kurumsal çözümler.
İlgili Makaleler
Odoo 19 HR: Beceri Matrisi, Kariyer Planları, Performans Döngüleri
Odoo 19 İK yükseltmesi: yerel beceriler matrisi, kariyer yolu planlaması, performans inceleme döngüleri, 9 kutulu tablo, yedekleme planlaması, HRIS entegrasyonu.
Odoo 19 Performans Karşılaştırmaları: PostgreSQL 17 Ayar Numaraları
Gerçek dünya Odoo 19 performans kıyaslamaları: web istemci hızı, ORM verimi, PG17 ayarlama ayarları, bağlantı havuzu oluşturma, çalışan sayıları, ölçeklendirme eşikleri.
OpenClaw Maliyet Optimizasyonu ve Büyük Ölçekte Token Verimliliği
OpenClaw belirteci maliyet optimizasyonu: hızlı önbelleğe alma, model yönlendirme, yanıt önbelleğe alma, toplu API'ler ve üretim aracıları için kiracı başına maliyet korkulukları.
Performance & Scalability serisinden daha fazlası
Odoo 19 HR: Beceri Matrisi, Kariyer Planları, Performans Döngüleri
Odoo 19 İK yükseltmesi: yerel beceriler matrisi, kariyer yolu planlaması, performans inceleme döngüleri, 9 kutulu tablo, yedekleme planlaması, HRIS entegrasyonu.
Odoo 19 Performans Karşılaştırmaları: PostgreSQL 17 Ayar Numaraları
Gerçek dünya Odoo 19 performans kıyaslamaları: web istemci hızı, ORM verimi, PG17 ayarlama ayarları, bağlantı havuzu oluşturma, çalışan sayıları, ölçeklendirme eşikleri.
OpenClaw Maliyet Optimizasyonu ve Büyük Ölçekte Token Verimliliği
OpenClaw belirteci maliyet optimizasyonu: hızlı önbelleğe alma, model yönlendirme, yanıt önbelleğe alma, toplu API'ler ve üretim aracıları için kiracı başına maliyet korkulukları.
10 Milyon Satırdan Fazla Tablolar için Power BI Artımlı Yenileme
10 milyondan fazla satır tablosu için Power BI Artımlı Yenileme oyun kitabı: bölüm tasarımı, RangeStart/RangeEnd, yenileme ilkeleri, sorgu katlama ve DirectQuery hibritleri.
Web Kancası Hata Ayıklama ve İzleme: Eksiksiz Sorun Giderme Kılavuzu
Arıza modellerini, hata ayıklama araçlarını, yeniden deneme stratejilerini, izleme kontrol panellerini ve en iyi güvenlik uygulamalarını kapsayan bu eksiksiz kılavuzla webhook hata ayıklama konusunda uzmanlaşın.
k6 Yük Testi: Lansmandan Önce API'lerinize Stres Testi Yapın
Node.js API'leri için k6 yük testinde uzmanlaşın. Sanal kullanıcı artışlarını, eşikleri, senaryoları, HTTP/2, WebSocket testini, Grafana kontrol panellerini ve CI entegrasyon modellerini kapsar.