Redis Caching Patterns for Web Applications

Master Redis caching patterns for Node.js and NestJS: cache-aside, write-through, cache stampede prevention, TTL strategies, invalidation, and production monitoring.

E
ECOSIRE Research and Development Team
|19 Mart 202610 dk okuma2.1k Kelime|

Performance & Scalability serimizin bir parçası

Tam kılavuzu okuyun

Web 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 TTLGeçersiz Kılma Stratejisi
Kullanıcı profili15 dakikaProfil güncellemesinde
Organizasyon ayarları1 saatAyarlar değişikliğinde
Ürün kataloğu24 saatÜrün oluşturma/güncelleme hakkında
Blog yazısı listesi10 dakikaYayınlandıktan sonra
API oran sınırı sayacıPencere başına (60s)Otomatik sona erme
Yetkilendirme oturumu15 dakikaOturum kapatıldığında veya jeton iptal edildiğinde
Arama sonuçları5 dakikaYalnızca TTL
Toplu ölçümler1 dakikaYalnızca TTL
Tek kullanımlık kodlar (yetkilendirme)60 saniyeKullandıktan veya son kullanma tarihi geçtikten sonra
Sayfalandırılmış liste sorgusu5 dakikaorg'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 izleyin
  • evicted_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.

E

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.

WhatsApp'ta Sohbet Et