हमारी Performance & Scalability श्रृंखला का हिस्सा
पूरी गाइड पढ़ेंवेब अनुप्रयोगों के लिए रेडिस कैशिंग पैटर्न
डेटाबेस क्वेरीज़ वेब अनुप्रयोगों में सबसे आम प्रदर्शन बाधा हैं। एक खराब अनुक्रमित क्वेरी जो 200 एमएस लेती है, 1,000 समवर्ती उपयोगकर्ताओं के लिए प्रत्येक पृष्ठ लोड पर कॉल की जाती है, प्रति सेकंड 200 सेकंड डेटाबेस सीपीयू उत्पन्न करती है - एक मौत सर्पिल। रेडिस मारक है: एक उप-मिलीसेकंड इन-मेमोरी स्टोर जो रीड लोड को अवशोषित करता है जो अन्यथा आपके डेटाबेस को समान डेटा के लिए बार-बार हिट करेगा।
लेकिन कैशिंग केवल "चीजों को रेडिस में डालना" नहीं है। गलत पैटर्न पुराने डेटा बग, गड़गड़ाहट झुंड आपदाओं, या असीमित मेमोरी वृद्धि का कारण बनता है। यह मार्गदर्शिका चार कैनोनिकल कैशिंग पैटर्न, कैश अमान्यकरण रणनीतियों, भगदड़ की रोकथाम और उत्पादन NestJS कार्यान्वयन को शामिल करती है ताकि आप शुरू से ही सही तरीके से कैश कर सकें।
मुख्य बातें
- कैश-असाइड (आलसी लोडिंग) सबसे सुरक्षित डिफ़ॉल्ट है - केवल वही कैश करें जो वास्तव में अनुरोध किया गया है
- राइट-थ्रू कैश और डेटाबेस को सिंक में रखता है लेकिन लिखने में विलंबता जोड़ता है - बार-बार पढ़े जाने वाले, कभी-कभार लिखे जाने वाले डेटा के लिए उपयोग करें
- किसी लोकप्रिय कुंजी की समय सीमा समाप्त होने पर कैश भगदड़ (झुंड की गड़गड़ाहट) डेटाबेस को नष्ट कर देती है - संभाव्य शीघ्र समाप्ति या म्यूटेक्स लॉक का उपयोग करें
- टीटीएल को हर कुंजी पर सेट किया जाना चाहिए - असीमित रेडिस विकास एक ऐसी घटना है जो घटित होने की प्रतीक्षा कर रही है
- कैश अमान्यकरण कठिन है: पूरी तरह से अमान्य करने की कोशिश करने के बजाय लघु टीटीएल और ईवेंट-संचालित अमान्यकरण को प्राथमिकता दें
- कैश कुंजियों में सभी क्वेरी आयाम शामिल होने चाहिए:
contacts:{orgId}:{page}:{limit}:{search}:{sortField}:{sortDir}- बल्क अमान्यकरण को सक्षम करने के लिए रेडिस नेमस्पेस और कुंजी पैटर्न का उपयोग करें
- कैश हिट दर की निगरानी करें; 80% से नीचे का मतलब है कि आपके टीटीएल बहुत छोटे हैं या आपकी मुख्य संरचना गलत है
NestJS में रेडिस सेटअप
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: कैश-असाइड (आलसी लोडिंग)
सबसे आम पैटर्न. कैश से पढ़ें; मिस होने पर, डेटाबेस से पढ़ें और कैश भरें।
// 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;
});
}
पेशेवर: सरल; कैश में केवल वही शामिल होता है जो वास्तव में अनुरोध किया गया है। नुकसान: पहला अनुरोध हमेशा धीमा होता है (कैश मिस); टीटीएल समाप्त होने तक डेटा पुराना हो सकता है।
कब अमान्य करें: बनाने/अपडेट/डिलीट करने के बाद, 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: कैश भगदड़ की रोकथाम
जब एक लोकप्रिय कैश कुंजी समाप्त हो जाती है, तो सैकड़ों समवर्ती अनुरोध एक साथ कैश से छूट जाते हैं और सभी डेटाबेस - "थंडरिंग हर्ड" से टकराते हैं। अचानक लोड के कारण डेटाबेस ध्वस्त हो जाता है.
म्यूटेक्स लॉक पैटर्न
केवल एक अनुरोध कैश का पुनर्निर्माण करता है; अन्य लोग प्रतीक्षा करें और पुनः प्रयास करें:
// 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}`);
सत्र और प्रामाणिक कैशिंग
रेडिस ऑथ सत्र भंडारण के लिए आदर्श है - प्रत्येक अनुरोध पर डेटाबेस लुकअप को समाप्त करें:
// 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}`);
}
डेटा प्रकार द्वारा टीटीएल रणनीति
| डेटा प्रकार | अनुशंसित टीटीएल | अमान्यकरण रणनीति |
|---|---|---|
| उपयोगकर्ता प्रोफ़ाइल | 15 मिनट | प्रोफ़ाइल अपडेट पर |
| संगठन सेटिंग्स | 1 घंटा | सेटिंग्स बदलने पर |
| उत्पाद सूची | 24 घंटे | उत्पाद निर्माण/अद्यतन पर |
| ब्लॉग पोस्ट सूची | 10 मिनट | पोस्ट पर प्रकाशित |
| एपीआई दर सीमा काउंटर | प्रति विंडो (60s) | स्वचालित समाप्ति |
| प्रामाणिक सत्र | 15 मिनट | लॉगआउट या टोकन निरस्त करने पर |
| खोज परिणाम | 5 मिनट | केवल टीटीएल |
| एकत्रित मेट्रिक्स | 1 मिनट | केवल टीटीएल |
| एक बार के कोड (auth) | 60 सेकंड | उपयोग या समाप्ति के बाद |
| पृष्ठांकित सूची क्वेरी | 5 मिनट | संगठन में किसी भी उत्परिवर्तन पर |
कैश प्रदर्शन की निगरानी करना
// 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- मैक्समेमोरी नीति द्वारा निकाली गई कुंजियाँ (कैश-असाइड के लिए 0 के करीब होनी चाहिए)connected_clients- कनेक्शन पूल उपयोगinstantaneous_ops_per_sec- वर्तमान थ्रूपुट
अक्सर पूछे जाने वाले प्रश्न
कैश के लिए मुझे किस मैक्समेमोरी नीति का उपयोग करना चाहिए?
allkeys-lru (सभी कुंजियों से कम से कम हाल ही में उपयोग की गई कुंजियों को हटाएं) या volatile-lru (उन LRU कुंजियों को हटाएं जिनमें TTL सेट है) का उपयोग करें। शुद्ध कैश वर्कलोड के लिए, allkeys-lru मानक है - मेमोरी भर जाने पर Redis स्वचालित रूप से कोल्ड कुंजियाँ निकाल देता है। कैश के लिए कभी भी noeviction का उपयोग न करें - यह पुराने डेटा को निकालने के बजाय मेमोरी भर जाने पर त्रुटियां लौटाता है।
मैं रेडिस में संवेदनशील डेटा संग्रहीत करने से कैसे बचूं?
कच्चे पासवर्ड, निजी कुंजी, भुगतान कार्ड डेटा या एसएसएन को कभी भी कैश न करें। प्रमाणीकरण सत्रों के लिए, न्यूनतम डेटा कैश करें: उपयोगकर्ता आईडी, भूमिका, संगठन आईडी, टोकन संस्करण - पूर्ण जेडब्ल्यूटी पेलोड या एपीआई क्रेडेंशियल नहीं। उत्पादन में Redis AUTH और TLS सक्षम करें। यदि आपके रेडिस इंस्टेंस से समझौता किया गया है, तो इस अनुशासन का पालन करने पर केवल मेटाडेटा ही उजागर होता है।
पैटर्न हटाने के लिए स्कैन और कुंजी के बीच क्या अंतर है?
KEYS pattern एक ब्लॉकिंग O(n) ऑपरेशन है जो स्कैन करते समय सभी रेडिस कमांड को रोक देता है - यह बड़े कुंजी स्थानों पर कुछ सेकंड के लिए डाउनटाइम का कारण बन सकता है। SCAN गैर-अवरुद्ध है, एक कर्सर के साथ छोटे टुकड़ों में पुनरावृत्त होता है। उत्पादन पैटर्न हटाने के लिए हमेशा SCAN का उपयोग करें। ट्रेडऑफ़ यह है कि SCAN सभी कुंजियाँ वापस नहीं कर सकता है यदि उन्हें स्कैन के दौरान जोड़ा या हटा दिया जाता है - कैश अमान्यकरण के लिए स्वीकार्य।
क्या मुझे कैशिंग बनाम दर सीमित बनाम सत्र के लिए एक अलग रेडिस उदाहरण का उपयोग करना चाहिए?
अधिकांश अनुप्रयोगों के लिए, विभिन्न कुंजी उपसर्गों वाला एक Redis उदाहरण ठीक है। अलग-अलग उदाहरण तब समझ में आते हैं जब: कैश को दर सीमा स्टोर (कैश: allkeys-lru, दर सीमा: noeviction) से भिन्न maxmemory-policy की आवश्यकता होती है, या जब आपको स्वतंत्र स्केलिंग और फेलओवर की आवश्यकता होती है। पैमाने पर, प्रत्येक कार्यभार प्रकार के अनुसार अलग-अलग क्लस्टर के साथ रेडिस क्लस्टर का उपयोग करें।
मैं पृष्ठांकित सूची प्रश्नों के लिए कैश अमान्यकरण को कैसे प्रबंधित करूं?
पृष्ठांकित सूची कैश मुश्किल हैं - पृष्ठ 1 पर एक संपर्क जोड़ने से पृष्ठ 2+ पर सभी संपर्क स्थानांतरित हो जाते हैं। व्यावहारिक समाधान: लघु टीटीएल (2-5 मिनट) का उपयोग करें और पैटर्न अमान्यकरण (contacts:{orgId}:*) का उपयोग करके किसी भी लेखन पर संगठन के सभी पृष्ठों को अमान्य करें। भारी लेखन मात्रा वाले बड़े संगठनों के लिए, कैशिंग पेजिनेशन को पूरी तरह से छोड़ दें और इसके बजाय डेटाबेस-स्तरीय अनुकूलन (उचित इंडेक्स, कवरिंग इंडेक्स) पर भरोसा करें।
मैं विटेस्ट में कैश व्यवहार का परीक्षण कैसे करूं?
हिट/मिस परिदृश्यों के लिए उचित मान लौटाने वाले vi.fn() के साथ यूनिट परीक्षणों में CacheService को मॉक करें। एकीकरण परीक्षणों के लिए, वास्तविक रेडिस इंस्टेंस (डॉकर) का उपयोग करें और क्लीन शुरू करने के लिए beforeEach में redis.flushdb() का उपयोग करें। कैश-हिट पथ (कोई डेटाबेस कॉल नहीं) और कैश-मिस पथ (डेटाबेस जिसे + कैश पॉपुलेटेड कहा जाता है) दोनों का स्पष्ट रूप से परीक्षण करें।
अगले चरण
रेडिस कैशिंग वेब एप्लिकेशन में उच्चतम-आरओआई प्रदर्शन निवेशों में से एक है। 200ms डेटाबेस क्वेरी और 2ms रेडिस हिट के बीच का अंतर 100x है - पैमाने पर, जो सीधे बुनियादी ढांचे की लागत बचत और उपयोगकर्ता अनुभव में सुधार में तब्दील होता है।
ECOSIRE प्रत्येक NestJS प्रोजेक्ट पर कैश-एसाइड, राइट-थ्रू अमान्यकरण, भगदड़ की रोकथाम और कैश हिट रेट मॉनिटरिंग के साथ रेडिस कैशिंग लागू करता है। हमारी बैकएंड इंजीनियरिंग सेवाओं का अन्वेषण करें यह जानने के लिए कि हम परफ़ॉर्मेंट, स्केलेबल एपीआई कैसे डिज़ाइन करते हैं।
लेखक
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 के साथ अपना व्यवसाय बढ़ाएं
ईआरपी, ईकॉमर्स, एआई, एनालिटिक्स और ऑटोमेशन में एंटरप्राइज समाधान।
संबंधित लेख
Odoo 19 HR: Skills Matrix, Career Plans, Performance Cycles
Odoo 19 HR upgrade: native skills matrix, career path planning, performance review cycles, 9-box grid, succession planning, HRIS integration.
Odoo 19 Performance Benchmarks: PostgreSQL 17 Tuning Numbers
Real-world Odoo 19 performance benchmarks: web client speed, ORM throughput, PG17 tuning settings, connection pooling, worker counts, scaling thresholds.
OpenClaw Cost Optimization and Token Efficiency at Scale
OpenClaw token cost optimization: prompt caching, model routing, response caching, batch APIs, and per-tenant cost guardrails for production agents.
Performance & Scalability से और अधिक
Odoo 19 HR: Skills Matrix, Career Plans, Performance Cycles
Odoo 19 HR upgrade: native skills matrix, career path planning, performance review cycles, 9-box grid, succession planning, HRIS integration.
Odoo 19 Performance Benchmarks: PostgreSQL 17 Tuning Numbers
Real-world Odoo 19 performance benchmarks: web client speed, ORM throughput, PG17 tuning settings, connection pooling, worker counts, scaling thresholds.
OpenClaw Cost Optimization and Token Efficiency at Scale
OpenClaw token cost optimization: prompt caching, model routing, response caching, batch APIs, and per-tenant cost guardrails for production agents.
Power BI Incremental Refresh for Tables Over 10 Million Rows
Power BI Incremental Refresh playbook for 10M+ row tables: partition design, RangeStart/RangeEnd, refresh policies, query folding, and DirectQuery hybrids.
वेबहुक डिबगिंग और मॉनिटरिंग: संपूर्ण समस्या निवारण मार्गदर्शिका
विफलता पैटर्न, डिबगिंग टूल, पुनः प्रयास रणनीतियाँ, मॉनिटरिंग डैशबोर्ड और सुरक्षा सर्वोत्तम प्रथाओं को कवर करने वाली इस संपूर्ण मार्गदर्शिका के साथ वेबहुक डिबगिंग में महारत हासिल करें।
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.