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 مارچ، 202612 منٹ پڑھیں2.6k الفاظ|

ہماری Performance & Scalability سیریز کا حصہ

مکمل گائیڈ پڑھیں

ویب ایپلیکیشنز کے لیے کیشنگ پیٹرنز کو ریڈیس

ویب ایپلیکیشنز میں ڈیٹا بیس کے سوالات سب سے عام کارکردگی کی رکاوٹ ہیں۔ ایک ناقص انڈیکس شدہ استفسار جس میں 200ms کا وقت لگتا ہے، جسے 1,000 ہم آہنگ صارفین کے لیے ہر صفحہ لوڈ پر کال کیا جاتا ہے، 200 سیکنڈ ڈیٹا بیس CPU فی سیکنڈ پیدا کرتا ہے - ایک موت کا سرپل۔ Redis ایک تریاق ہے: ایک ذیلی ملی سیکنڈ ان میموری اسٹور جو پڑھنے کے بوجھ کو جذب کرتا ہے جو بصورت دیگر ایک جیسے ڈیٹا کے لیے آپ کے ڈیٹا بیس کو بار بار مارے گا۔

لیکن کیشنگ صرف "چیزوں کو ریڈیس میں ڈالنا" نہیں ہے۔ غلط پیٹرن باسی ڈیٹا کیڑے، گرجنے والی ریوڑ کی آفات، یا یادداشت کی بے حد ترقی کا سبب بنتا ہے۔ اس گائیڈ میں چار کینونیکل کیشنگ پیٹرن، کیش کو غلط کرنے کی حکمت عملی، بھگدڑ کی روک تھام، اور پروڈکشن NestJS کے نفاذ کا احاطہ کیا گیا ہے تاکہ آپ شروع سے ہی صحیح طریقے سے کیش کر سکیں۔

اہم ٹیک ویز

  • Cache-side (سست لوڈنگ) سب سے محفوظ ڈیفالٹ ہے — صرف وہی کیش کریں جس کی اصل میں درخواست کی گئی ہے
  • رائٹ تھرو کیش اور ڈیٹا بیس کو مطابقت پذیر رکھتا ہے لیکن لکھنے میں تاخیر کا اضافہ کرتا ہے - اکثر پڑھے جانے والے، کبھی کبھار لکھے جانے والے ڈیٹا کے لیے استعمال کریں
  • کیش سٹیمپیڈ (گرجتا ہوا ریوڑ) ڈیٹا بیس کو تباہ کر دیتا ہے جب ایک مقبول کلید کی میعاد ختم ہو جاتی ہے — امکانی ابتدائی میعاد ختم ہونے یا میوٹیکس تالے کا استعمال کریں
  • TTL کو ہر کلید پر سیٹ کیا جانا چاہیے — بے حد Redis گروتھ ایک ایسا واقعہ ہے جو ہونے کا انتظار کر رہا ہے۔
  • کیشے کی غلط کاری مشکل ہے: مکمل طور پر باطل کرنے کی کوشش کرنے پر مختصر TTLs اور ایونٹ پر مبنی باطل کو ترجیح دیں
  • کیشے کیز میں استفسار کے تمام جہتیں شامل ہونی چاہئیں: contacts:{orgId}:{page}:{limit}:{search}:{sortField}:{sortDir}
  • بڑی تعداد میں باطل کرنے کے لیے Redis نام کی جگہوں اور کلیدی نمونوں کا استعمال کریں۔
  • کیش ہٹ ریٹ مانیٹر کریں؛ 80% سے کم کا مطلب ہے کہ آپ کے TTLs بہت چھوٹے ہیں یا آپ کی کلیدی ساخت غلط ہے۔

NestJS میں Redis سیٹ اپ

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 {}

پیٹرن 1: Cache-Side (Lazy Loading)

سب سے عام پیٹرن. کیشے سے پڑھیں؛ مس ہونے پر، ڈیٹا بیس سے پڑھیں اور کیشے کو پاپولٹ کریں۔

// 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;
  });
}

** پیشہ **: سادہ؛ cache میں صرف وہی ہوتا ہے جس کی اصل میں درخواست کی جاتی ہے۔ کونس: پہلی درخواست ہمیشہ سست ہوتی ہے (کیشے کی کمی)؛ TTL کی میعاد ختم ہونے تک ڈیٹا باسی ہو سکتا ہے۔

کب باطل کرنا ہے: تخلیق/اپ ڈیٹ/حذف کرنے کے بعد، contacts:{orgId}:* سے مماثل تمام کلیدوں کو حذف کر دیں۔


پیٹرن 2: لکھنا

ہر تحریر پر ڈیٹا بیس کے ساتھ بیک وقت کیشے کو اپ ڈیٹ کریں۔ ٹریڈز پڑھنے میں مستقل مزاجی کے لیے لیٹنسی لکھتے ہیں۔

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;
}

کب استعمال کرنا ہے: کثرت سے پڑھے جانے والے، کبھی کبھار لکھے جانے والے ریکارڈ (صارف کے پروفائلز، سیٹنگز، پروڈکٹ کیٹلاگ)۔


پیٹرن 3: کیشے سٹیمپیڈ کی روک تھام

جب ایک مقبول کیش کلید کی میعاد ختم ہو جاتی ہے، تو سینکڑوں ہم آہنگ درخواستیں بیک وقت کیشے سے محروم ہو جاتی ہیں اور سبھی ڈیٹابیس سے ٹکرا جاتی ہیں - "تھنڈرنگ ہرڈ۔" ڈیٹا بیس اچانک بوجھ کے نیچے گر جاتا ہے۔

Mutex لاک پیٹرن

صرف ایک درخواست کیشے کو دوبارہ بناتی ہے۔ دوسرے انتظار کریں اور دوبارہ کوشش کریں:

// 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);
  }
}

امکانی ابتدائی معیاد (XFetch)

کیشے کی میعاد ختم ہونے سے پہلے ممکنہ طور پر دوبارہ بنانا شروع کریں، دوبارہ گنتی کے بوجھ کو پھیلاتے ہوئے:

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;
}

پیٹرن 4: ٹیگ پر مبنی کیشے کی غلط کاری

کیش کیز کو منطقی ٹیگز کے ذریعے گروپ کریں اور ایک ہی وقت میں پورے ٹیگ کو باطل کریں:

// 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}`);

سیشن اور اوتھ کیشنگ

Redis auth سیشن اسٹوریج کے لیے مثالی ہے — ہر درخواست پر ڈیٹا بیس کی تلاش کو ختم کریں:

// 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}`);
}

ڈیٹا کی قسم کے لحاظ سے TTL حکمت عملی

ڈیٹا کی قسمتجویز کردہ TTLباطل کرنے کی حکمت عملی
صارف پروفائل15 منٹپروفائل اپ ڈیٹ پر
تنظیم کی ترتیبات1 گھنٹہترتیبات کی تبدیلی پر
مصنوعات کی فہرست24 گھنٹےپروڈکٹ تخلیق / اپ ڈیٹ پر
بلاگ پوسٹ کی فہرست10 منٹپوسٹ شائع کرنے پر
API شرح کی حد کاؤنٹرفی ونڈو (60s)خودکار ختم
توثیق سیشن15 منٹلاگ آؤٹ یا ٹوکن منسوخ ہونے پر
تلاش کے نتائج5 منٹصرف TTL
مجموعی میٹرکس1 منٹصرف TTL
ایک بار کے کوڈز (تصویر)60 سیکنڈاستعمال یا ختم ہونے کے بعد
صفحہ بندی کی فہرست کا سوال5 منٹorg میں کسی بھی تبدیلی پر

کیشے کی کارکردگی کی نگرانی کرنا

// 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 کے ذریعے مانیٹر کرنے کے لیے کلیدی ریڈیس میٹرکس:

  • keyspace_hits / keyspace_misses — عالمی ہٹ ریٹ (ہدف 80% سے زیادہ)
  • used_memory — بے حد ترقی پر نگاہ رکھیں
  • evicted_keys — maxmemory پالیسی کے ذریعے نکالی گئی چابیاں (کیشے کو ایک طرف رکھنے کے لیے 0 کے قریب ہونا چاہیے)
  • connected_clients - کنکشن پول کا استعمال
  • instantaneous_ops_per_sec - موجودہ تھرو پٹ

اکثر پوچھے گئے سوالات

کیشے کے لیے مجھے کونسی maxmemory پالیسی استعمال کرنی چاہیے؟

allkeys-lru استعمال کریں (تمام کلیدوں سے کم از کم حال ہی میں استعمال شدہ چابیاں نکالیں) یا volatile-lru (LRU کیز کو بے دخل کریں جن میں TTL سیٹ ہے)۔ خالص کیشے کے کام کے بوجھ کے لیے، allkeys-lru معیاری ہے — میموری بھر جانے پر Redis خود بخود کولڈ کیز کو نکال دیتا ہے۔ کبھی بھی کیشے کے لیے noeviction استعمال نہ کریں — یہ پرانے ڈیٹا کو نکالنے کے بجائے میموری بھر جانے پر غلطیاں لوٹاتا ہے۔

میں Redis میں حساس ڈیٹا کو ذخیرہ کرنے سے کیسے بچ سکتا ہوں؟

کبھی بھی خام پاس ورڈز، پرائیویٹ کیز، پیمنٹ کارڈ ڈیٹا، یا SSNs کو کیش نہ کریں۔ توثیق کے سیشنز کے لیے، کم سے کم ڈیٹا کیش کریں: userId، رول، OrganizationId، token Version — مکمل JWT پے لوڈز یا API اسناد نہیں۔ پروڈکشن میں Redis AUTH اور TLS کو فعال کریں۔ اگر آپ کی Redis مثال سے سمجھوتہ کیا گیا ہے، تو صرف میٹا ڈیٹا سامنے آتا ہے جب آپ اس نظم و ضبط کی پیروی کرتے ہیں۔

SCAN اور KEYS میں پیٹرن کو حذف کرنے کے لیے کیا فرق ہے؟

KEYS pattern ایک بلاک کرنے والا O(n) آپریشن ہے جو اسکیننگ کے دوران تمام Redis کمانڈز کو روکتا ہے — یہ بڑی کلیدی جگہوں پر سیکنڈوں کے ڈاؤن ٹائم کا سبب بن سکتا ہے۔ SCAN غیر مسدود ہے، کرسر کے ساتھ چھوٹے ٹکڑوں میں اعادہ کرتا ہے۔ پروڈکشن پیٹرن کو حذف کرنے کے لیے ہمیشہ SCAN استعمال کریں۔ ٹریڈ آف یہ ہے کہ SCAN تمام چابیاں واپس نہیں کر سکتا ہے اگر وہ سکین کے دوران شامل یا حذف کر دی جائیں — کیشے کی غلط کاری کے لیے قابل قبول ہے۔

کیا مجھے کیشنگ بمقابلہ ریٹ محدود بمقابلہ سیشنز کے لیے علیحدہ Redis مثال استعمال کرنی چاہیے؟

زیادہ تر ایپلی کیشنز کے لیے، مختلف کلیدی سابقوں کے ساتھ ایک Redis مثال ٹھیک ہے۔ الگ الگ مثالیں اس وقت معنی رکھتی ہیں جب: کیشے کو ریٹ کی حد اسٹور (کیشے: allkeys-lru، شرح کی حد: noeviction) سے مختلف maxmemory-policy کی ضرورت ہو، یا جب آپ کو آزاد اسکیلنگ اور فیل اوور کی ضرورت ہو۔ پیمانے پر، کام کے بوجھ کی قسم کے لیے الگ الگ کلسٹرز کے ساتھ Redis کلسٹر استعمال کریں۔

میں صفحہ بندی کی فہرست کے سوالات کے لیے کیشے کی غلط کاری کو کیسے ہینڈل کروں؟

صفحہ بندی کی فہرست کے کیشز مشکل ہیں - صفحہ 1 پر رابطہ شامل کرنا صفحہ 2+ پر موجود تمام رابطوں کو شفٹ کر دیتا ہے۔ عملی حل: مختصر TTLs (2-5 منٹ) استعمال کریں اور پیٹرن کی غلط کاری (contacts:{orgId}:*) کا استعمال کرتے ہوئے کسی بھی تحریر پر تنظیم کے لیے تمام صفحات کو باطل کریں۔ بھاری تحریری حجم والی بڑی تنظیموں کے لیے، کیشنگ صفحہ بندی کو مکمل طور پر چھوڑ دیں اور اس کے بجائے ڈیٹا بیس کی سطح کی اصلاح (مناسب اشاریہ جات، کورنگ اشاریہ جات) پر انحصار کریں۔

میں Vitest میں کیشے کے رویے کی جانچ کیسے کروں؟

vi.fn() کے ساتھ یونٹ ٹیسٹ میں CacheService کا مذاق اڑائیں ہٹ/مس منظرناموں کے لیے مناسب قدریں لوٹائیں۔ انضمام کے ٹیسٹ کے لیے، ایک حقیقی Redis مثال (Docker) استعمال کریں اور صاف شروع کرنے کے لیے beforeEach میں redis.flushdb() استعمال کریں۔ کیش ہٹ پاتھ (کوئی ڈیٹا بیس کال نہیں) اور کیش مس پاتھ (ڈیٹا بیس جسے + کیش پاپولڈ کہا جاتا ہے) دونوں کو واضح طور پر جانچیں۔


اگلے اقدامات

Redis کیشنگ ویب ایپلیکیشن میں اعلی ترین ROI کارکردگی کی سرمایہ کاری میں سے ایک ہے۔ 200ms ڈیٹا بیس استفسار اور 2ms Redis ہٹ کے درمیان فرق 100x ہے — پیمانے پر، جو براہ راست بنیادی ڈھانچے کی لاگت کی بچت اور صارف کے تجربے میں بہتری کا ترجمہ کرتا ہے۔

ECOSIRE ہر NestJS پروجیکٹ پر کیش-سائیڈ، رائٹ تھرو انیلیڈیشن، سٹیمپیڈ ​​سے بچاؤ، اور کیش ہٹ ریٹ مانیٹرنگ کے ساتھ Redis کیشنگ کو لاگو کرتا ہے۔ ہماری بیک اینڈ انجینئرنگ سروسز کو دریافت کریں یہ جاننے کے لیے کہ ہم کس طرح پرفارمنٹ، قابل توسیع APIs کو ڈیزائن کرتے ہیں۔

E

تحریر

ECOSIRE Research and Development Team

ECOSIRE میں انٹرپرائز گریڈ ڈیجیٹل مصنوعات بنانا۔ Odoo انٹیگریشنز، ای کامرس آٹومیشن، اور AI سے چلنے والے کاروباری حل پر بصیرت شیئر کرنا۔

Chat on WhatsApp