Redis Caching Patterns
17 دقیقه زمان برای خواندن این مطلب نیاز است.
فهرست مطالب
- Redis Caching Patterns: راهنمای جامع و تخصصی الگوهای کشینگ با Redis
- Redis چیست و چرا برای کشینگ مناسب است؟
- اهمیت استفاده از الگوهای کشینگ در Redis
- الگوی اول: Cache-Aside (Lazy Loading)
- الگوی دوم: Read-Through Cache
- الگوی سوم: Write-Through Cache
- الگوی چهارم: Write-Back (Write-Behind) Cache
- الگوی پنجم: Cache Stampede Prevention
- الگوی ششم: Cache Penetration Prevention
- الگوی هفتم: Time-To-Live (TTL) Management
- الگوی هشتم: Hot Key Detection و Handling
- الگوی نهم: Cache Invalidation Patterns
- ابزارهای مانیتورینگ برای Redis Caching Patterns
- اشتباهات رایج در پیادهسازی Redis Caching Patterns
Redis Caching Patterns: راهنمای جامع و تخصصی الگوهای کشینگ با Redis
در دنیای امروز، سرعت و پاسخدهی برنامههای کاربردی به یک نیاز اساسی تبدیل شده است. کاربران انتظار دارند صفحات در کسری از ثانیه بارگذاری شوند و درخواستهای آنها بلافاصله پاسخ داده شود. اینجاست که Redis Caching Patterns وارد میشوند. Redis به عنوان یک پایگاه داده درونحافظهای (In-Memory) با عملکرد فوقالعاده بالا، محبوبترین انتخاب برای پیادهسازی کشینگ در سیستمهای مدرن است. اما صرفاً نصب و راهاندازی Redis کافی نیست؛ آنچه یک سیستم را مقیاسپذیر و با عملکرد بالا نگه میدارد، انتخاب و پیادهسازی صحیح Redis Caching Patterns است. در این مقاله از دانا پدیا، قصد داریم به صورت عمیق و تخصصی به بررسی Redis Caching Patterns بپردازیم، هر الگو را با مثالهای عملی در کدهای واقعی توضیح دهیم، مزایا و معایب هر کدام را تحلیل کنیم، و در نهایت شما را به یک متخصص واقعی در طراحی سیستمهای کشینگ با Redis تبدیل کنیم.
اگر شما یک توسعهدهنده بکاند، معمار نرمافزار، یا مدیر فنی هستید که با چالشهای عملکردی در سیستمهای خود روبرو است، این مقاله از دانا پدیا دقیقاً برای شما نوشته شده است. Redis Caching Patterns نه تنها به شما کمک میکنند تا سرعت برنامههای خود را چندین برابر کنید، بلکه بار ترافیکی پایگاه داده اصلی را نیز به شدت کاهش میدهند. پس با ما همراه باشید تا دنیای شگفتانگیز Redis Caching Patterns را با هم فتح کنیم.
Redis چیست و چرا برای کشینگ مناسب است؟
قبل از پرداختن به جزئیات Redis Caching Patterns، بیایید ببینیم چرا Redis تا این حد برای کشینگ محبوب است. Redis (مخفف Remote Dictionary Server) یک ذخیرهساز ساختار داده درونحافظه با منبع باز است که به عنوان پایگاه داده، کش و پیامرسان استفاده میشود. ویژگیهای کلیدی Redis که آن را برای پیادهسازی Redis Caching Patterns ایدهآل میکند عبارتند از:
۱. سرعت فوقالعاده بالا
Redis دادهها را در حافظه RAM ذخیره میکند، نه روی دیسک. این یعنی زمان دسترسی به دادهها در حد میکروثانیه است. برای مقایسه، خواندن از یک پایگاه داده دیسکمحور مانند PostgreSQL یا MySQL معمولاً بین ۵ تا ۲۰ میلیثانیه زمان میبرد، در حالی که Redis این کار را در کمتر از ۰.۵ میلیثانیه انجام میدهد.
۲. پشتیبانی از ساختارهای داده پیشرفته
Redis فقط کلید-مقدار ساده نیست. این ابزار از ساختارهایی مانند:
- رشتهها (Strings)
- لیستها (Lists)
- مجموعهها (Sets)
- مجموعههای مرتب (Sorted Sets)
- هشها (Hashes)
- بیتمپها (Bitmaps)
- HyperLogLog
- جریانها (Streams)
این تنوع به شما امکان میدهد Redis Caching Patterns را برای سناریوهای بسیار متنوعی پیادهسازی کنید.
۳. قابلیت persistence (اختیاری)
اگرچه Redis یک کش درونحافظه است، اما میتواند به صورت دورهای یا با هر دستور، دادهها را روی دیسک ذخیره کند تا در صورت راهاندازی مجدد، دادهها از دست نروند.
۴. پشتیبانی از replication و high availability
Redis از معماری master-slave و Redis Sentinel برای failover خودکار پشتیبانی میکند. همچنین Redis Cluster امکان توزیع دادهها بین چندین نود را فراهم میکند.
۵. عملیات اتمی و Lua scripting
بسیاری از Redis Caching Patterns نیاز به عملیات چندمرحلهای دارند که باید به صورت اتمی انجام شوند. Redis با دستورات اتمی و پشتیبانی از اسکریپتهای Lua این امکان را فراهم میکند.

اهمیت استفاده از الگوهای کشینگ در Redis
بسیاری از توسعهدهندگان تازهکار فکر میکنند که کشینگ با Redis به همین سادگی است: قبل از خواندن از دیتابیس، ابتدا در Redis نگاه کن، اگر بود برگردان، اگر نبود از دیتابیس بخوان و در Redis ذخیره کن. اما در دنیای واقعی با مقیاس بالا، این رویکرد ساده با مشکلات متعددی روبرو میشود: هجوم همزمان (Cache Stampede)، نفوذ سوراخ سوراخ (Cache Penetration)، بهروزرسانی همزمان (Race Condition)، و دهها چالش دیگر.
Redis Caching Patterns راهکارهای استاندارد و تستشدهای برای این مشکلات ارائه میدهند. با استفاده از Redis Caching Patterns شما میتوانید:
- از از کار افتادن دیتابیس در لحظات پیک ترافیک جلوگیری کنید.
- نرخ هیت کش (Cache Hit Ratio) را تا بیش از ۹۰٪ افزایش دهید.
- زمان پاسخدهی APIهای خود را تا ۱۰ برابر کاهش دهید.
- هزینههای زیرساختی را با کاهش نیاز به دیتابیسهای بزرگ کاهش دهید.
- سیستم خود را مقاوم در برابر نوسانات ترافیک بسازید.
در ادامه مهمترین Redis Caching Patterns را یکی یکی بررسی خواهیم کرد.
الگوی اول: Cache-Aside (Lazy Loading)
Cache-Aside که با نام Lazy Loading نیز شناخته میشود، رایجترین و سادهترین الگو در میان Redis Caching Patterns است. در این الگو، برنامه کاربردی مسئولیت مدیریت کش را بر عهده دارد.
نحوه عملکرد
۱. برنامه ابتدا از Redis درخواست داده میکند.
۲. اگر داده در Redis وجود داشت (Cache Hit)، بلافاصله برگردانده میشود.
۳. اگر داده وجود نداشت (Cache Miss)، برنامه از دیتابیس اصلی (مانند PostgreSQL) میخواند.
۴. داده خوانده شده در Redis ذخیره میشود (با یک TTL مناسب).
۵. داده به کاربر برگردانده میشود.
پیادهسازی در کد (با Node.js و Redis)
// الگوی Cache-Aside با Redis
async function getUserProfile(userId) {
const cacheKey = `user:${userId}:profile`;
// مرحله 1: تلاش برای خواندن از کش
const cachedData = await redis.get(cacheKey);
if (cachedData) {
// Cache Hit - داده در Redis وجود دارد
return JSON.parse(cachedData);
}
// Cache Miss - داده در Redis نیست
// مرحله 2: خواندن از دیتابیس اصلی
const userFromDb = await db.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
if (!userFromDb) {
return null;
}
// مرحله 3: ذخیره در Redis با TTL 3600 ثانیه (1 ساعت)
await redis.setex(cacheKey, 3600, JSON.stringify(userFromDb));
return userFromDb;
}
مزایا
- سادگی پیادهسازی
- فقط دادههای واقعاً پرتقاضا در کش ذخیره میشوند (صرفهجویی در حافظه)
- مقاوم در برابر خطا (در صورت از کار افتادن Redis، برنامه همچنان از دیتابیس اصلی میخواند)
معایب
- اولین درخواست همیشه کند است (Cache Miss)
- احتمال هجوم همزمان (Cache Stampede) در زمان انقضای کش
- دادههای کش شده ممکن است با دیتابیس اصلی نامنسجم شوند (Stale Data)
کاربردهای مناسب
- پروفایل کاربران
- صفحات محصول در فروشگاههای اینترنتی
- تنظیمات (configurations) که به ندرت تغییر میکنند
در ادامه سایر Redis Caching Patterns را بررسی خواهیم کرد که این مشکلات را حل میکنند.
الگوی دوم: Read-Through Cache
Read-Through یکی از پیشرفتهترهای Redis Caching Patterns است که پیچیدگی مدیریت کش را از برنامه به لایه کش واگذار میکند.
نحوه عملکرد
در این الگو، برنامه مستقیماً با Redis صحبت میکند، اما Redis به گونهای پیکربندی شده که در صورت نبود داده، خودش از دیتابیس اصلی بخواند و در کش ذخیره کند. این کار معمولاً با استفاده از ماژولهایی مانند RedisJSON یا RediSearch یا با پیادهسازی یک لایه میانی انجام میشود.
پیادهسازی با استفاده از الگوی Client-Side Caching
// پیادهسازی Read-Through با Node.js
class ReadThroughCache {
constructor(redisClient, dbClient) {
this.redis = redisClient;
this.db = dbClient;
}
async get(key, ttl = 3600) {
// تلاش برای خواندن از کش
let value = await this.redis.get(key);
if (value !== null) {
return JSON.parse(value);
}
// اگر در کش نبود، از دیتابیس بخوان
// فرض میکنیم key شامل نوع داده است، مثل "user:123"
const [entityType, id] = key.split(':');
let dbValue;
switch (entityType) {
case 'user':
dbValue = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
break;
case 'product':
dbValue = await this.db.query('SELECT * FROM products WHERE id = $1', [id]);
break;
default:
return null;
}
if (dbValue) {
// ذخیره در کش
await this.redis.setex(key, ttl, JSON.stringify(dbValue));
return dbValue;
}
return null;
}
}
// استفاده
const cache = new ReadThroughCache(redis, db);
const user = await cache.get('user:123');
مزایا
- کد برنامه سادهتر و تمیزتر میشود
- منطق کشینگ متمرکز و قابل استفاده مجدد است
- کاهش خطاهای انسانی در مدیریت کش
معایب
- پیادهسازی اولیه پیچیدهتر از Cache-Aside
- نیاز به نگاشت بین کلیدهای کش و دیتابیس اصلی
- اگر لایه کش از کار بیفتد، کل سیستم مختل میشود
الگوی سوم: Write-Through Cache
در Redis Caching Patterns، تا اینجا فقط الگوهای خواندنی را بررسی کردیم. اما الگوهای نوشتنی نیز حیاتی هستند. Write-Through الگویی است که در آن هر نوشتن روی دیتابیس اصلی، به صورت همزمان در Redis نیز اعمال میشود.
نحوه عملکرد
۱. برنامه درخواست نوشتن (ایجاد، بهروزرسانی یا حذف) را دریافت میکند.
۲. ابتدا داده در دیتابیس اصلی ذخیره میشود.
۳. بلافاصله همان داده در Redis ذخیره (یا بهروزرسانی) میشود.
۴. سپس پاسخ موفقیت به کاربر برگردانده میشود.
پیادهسازی
// الگوی Write-Through با Redis
async function updateUserProfile(userId, newData) {
const cacheKey = `user:${userId}:profile`;
// مرحله 1: بهروزرسانی در دیتابیس اصلی
await db.query(
'UPDATE users SET name = $1, email = $2 WHERE id = $3',
[newData.name, newData.email, userId]
);
// مرحله 2: بهروزرسانی همزمان در Redis
const updatedUser = { id: userId, ...newData };
await redis.setex(cacheKey, 3600, JSON.stringify(updatedUser));
// مرحله 3: (اختیاری) حذف کشهای وابسته
await redis.del(`user:${userId}:posts`);
return updatedUser;
}
مزایا
- دادههای کش همیشه با دیتابیس اصلی سازگار هستند (strong consistency)
- Cache Hit بالا برای دادههای جدید یا بهروز شده
- سادگی درک و پیادهسازی
معایب
- افزایش latency در عملیات نوشتن (چون دو عملیات پشت سر هم انجام میشود)
- اگر Redis از کار بیفتد، عملیات نوشتن ممکن است ناموفق شود یا دادهها ناهماهنگ شوند
- مصرف حافظه بیشتر در Redis (چون همه دادهها ذخیره میشوند، نه فقط دادههای پرمخاطب)
کاربردهای مناسب
- سیستمهایی که سازگاری قوی (strong consistency) نیاز دارند
- دادههایی که نسبت خواندن به نوشتن بالاست (مثلاً ۸۰٪ خواندن، ۲۰٪ نوشتن)
- پنلهای مدیریتی که به روز بودن دادهها حیاتی است
الگوی چهارم: Write-Back (Write-Behind) Cache
Write-Back یکی از قدرتمندترین Redis Caching Patterns برای سناریوهایی است که نرخ نوشتن بسیار بالا است. در این الگو، داده ابتدا در Redis ذخیره میشود و سپس به صورت ناهمگام (asynchronous) به دیتابیس اصلی نوشته میشود.
نحوه عملکرد
۱. برنامه درخواست نوشتن را دریافت میکند.
۲. داده فقط در Redis ذخیره میشود (با یک علامت “dirty”).
۳. بلافاصله پاسخ موفقیت به کاربر برگردانده میشود.
۴. یک پردازش پسزمینه (مثلاً هر ۵ ثانیه یا هر ۱۰۰۰ رکورد) دادههای dirty را به دیتابیس اصلی مینویسد.
پیادهسازی با Redis Streams
// الگوی Write-Back با استفاده از Redis Streams
class WriteBackCache {
constructor(redisClient, dbClient) {
this.redis = redisClient;
this.db = dbClient;
this.startBackgroundFlusher();
}
async write(key, value) {
const cacheKey = `wb:${key}`;
// ذخیره در Redis با نشانگر dirty
await this.redis.hset(cacheKey, {
data: JSON.stringify(value),
dirty: 'true',
timestamp: Date.now()
});
// اضافه کردن به استریم برای پردازش ناهمگام
await this.redis.xadd('writeback-stream', '*', 'key', cacheKey, 'value', JSON.stringify(value));
// بلافاصله برگردان (بدون منتظر ماندن برای دیتابیس)
return value;
}
async startBackgroundFlusher() {
setInterval(async () => {
// خواندن از استریم
const entries = await this.redis.xrange('writeback-stream', '-', '+', 'COUNT', 100);
for (const [id, fields] of entries) {
const key = fields[1]; // 'key'
const value = JSON.parse(fields[3]); // 'value'
// ذخیره در دیتابیس اصلی
await this.db.query('INSERT INTO cache_data (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2', [key, value]);
// پاک کردن dirty flag
await this.redis.hset(key, 'dirty', 'false');
// حذف از استریم (اختیاری)
await this.redis.xdel('writeback-stream', id);
}
}, 5000); // هر 5 ثانیه یک بار
}
}
مزایا
- فوقالعاده سریع برای عملیات نوشتن (چون دیتابیس دیسکی درگیر نمیشود)
- کاهش بار روی دیتابیس اصلی به شدت (به صورت batch نوشته میشود)
- مناسب برای سیستمهای با نرخ نوشتن بسیار بالا (لاگها، رویدادها، شمارندهها)
معایب
- ریسک از دست دادن داده در صورت از کار افتادن Redis قبل از flush (مگر اینکه persistence فعال باشد)
- پیچیدگی بالای پیادهسازی
- احتمال ناهماهنگی موقت بین Redis و دیتابیس اصلی
کاربردهای مناسب
- سیستمهای ثبت لاگ و رویداد
- شمارندهها و متریکهای لحظهای
- سیستمهای رأیگیری و لایک
- سناریوهایی که کمی تأخیر در consistency قابل قبول است
الگوی طراحی Chain of Responsibility
الگوی پنجم: Cache Stampede Prevention
یکی از خطرناکترین مشکلات در Redis Caching Patterns، پدیده Cache Stampede است. این اتفاق زمانی میافتد که یک کلید محبوب در Redis منقضی میشود و ناگهان هزاران درخواست همزمان به دیتابیس اصلی هجوم میآورند تا داده را دوباره بخوانند. نتیجه: از کار افتادن دیتابیس اصلی.
راهکار ۱: قفل توزیع شده (Distributed Lock)
// جلوگیری از Cache Stampede با قفل توزیع شده در Redis
async function getWithLock(key, fetchFunction, ttl = 3600) {
const lockKey = `lock:${key}`;
const lockValue = uuidv4();
const lockTtl = 5; // قفل حداکثر 5 ثانیه نگه داشته میشود
// تلاش برای گرفتن قفل
const acquired = await redis.set(lockKey, lockValue, 'NX', 'EX', lockTtl);
if (acquired) {
// این درخواست وظیفه بازسازی کش را دارد
try {
const freshData = await fetchFunction();
await redis.setex(key, ttl, JSON.stringify(freshData));
return freshData;
} finally {
// آزاد کردن قفل (فقط اگر مالک خودمان باشیم)
const currentLock = await redis.get(lockKey);
if (currentLock === lockValue) {
await redis.del(lockKey);
}
}
} else {
// قفل در اختیار درخواست دیگر است، کمی صبر میکنیم
await sleep(100); // 100 میلیثانیه
return getWithLock(key, fetchFunction, ttl); // تلاش مجدد
}
}
راهکار ۲: External Recomputation (نصیحت شده توسط فیسبوک)
در این روش، به جای اینکه یکی از درخواستهای همزمان کش را بازسازی کند، یک پردازش خارجی (background job) این کار را قبل از انقضای کش انجام میدهد.
// جلوگیری از Cache Stampede با بازسازی زودهنگام
async function getWithEarlyRecomputation(key, fetchFunction, ttl = 3600) {
const cacheData = await redis.get(key);
if (cacheData) {
const data = JSON.parse(cacheData);
const timeToLive = await redis.ttl(key);
// اگر کمتر از 20% زمان باقی مانده، در پسزمینه بازسازی کن
if (timeToLive < ttl * 0.2) {
// بازسازی ناهمگام (بدون منتظر ماندن کاربر)
setImmediate(async () => {
const freshData = await fetchFunction();
await redis.setex(key, ttl, JSON.stringify(freshData));
});
}
return data;
}
// اگر کش وجود نداشت، به روش معمولی بازسازی کن
const freshData = await fetchFunction();
await redis.setex(key, ttl, JSON.stringify(freshData));
return freshData;
}
الگوی ششم: Cache Penetration Prevention
Cache Penetration زمانی رخ میدهد که درخواستها برای دادهای که اصلاً در دیتابیس اصلی وجود ندارد (مثلاً ID نامعتبر) مرتباً تکرار میشوند. این درخواستها همیشه Cache Miss خواهند داشت و مستقیماً به دیتابیس میروند.
راهکار: Bloom Filter در Redis
Bloom Filter یک ساختار داده احتمالی است که به شما میگوید یک کلید “قطعاً وجود ندارد” یا “احتمالاً وجود دارد”. در Redis میتوانید از ماژول RedisBloom استفاده کنید.
// جلوگیری از Cache Penetration با Bloom Filter در Redis
const { RedisBloom } = require('redisbloom');
async function getUserWithBloomFilter(userId) {
const bloomKey = 'user:bloom';
const cacheKey = `user:${userId}:profile`;
// مرحله 1: بررسی در Bloom Filter
const mightExist = await redisBloom.exists(bloomKey, userId);
if (!mightExist) {
// قطعاً وجود ندارد، بدون مراجعه به دیتابیس برگردان
return null;
}
// مرحله 2: بررسی در کش معمولی
let user = await redis.get(cacheKey);
if (user) {
return JSON.parse(user);
}
// مرحله 3: بررسی در دیتابیس اصلی
user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
if (user) {
await redis.setex(cacheKey, 3600, JSON.stringify(user));
return user;
} else {
// حتی عدم وجود را هم کش کن (برای مدت کوتاه)
await redis.setex(`null:${cacheKey}`, 300, 'null');
return null;
}
}
// تابع برای افزودن userIdهای واقعی به Bloom Filter
async function addUserToBloom(userId) {
await redisBloom.add('user:bloom', userId);
}
الگوی هفتم: Time-To-Live (TTL) Management
مدیریت صحیح TTL یکی از ظریفترین Redis Caching Patterns است. انتخاب TTL مناسب تأثیر مستقیم بر نرخ هیت کش و میزان استفاده از حافظه دارد.
استراتژیهای TTL
1. TTL ثابت (Fixed TTL)
سادهترین روش: همه کلیدها TTL یکسان دارند (مثلاً ۱ ساعت).
2. TTL متغیر بر اساس محتوا (Content-Based TTL)
دادههای پویاتر TTL کوتاهتر، دادههای ایستا TTL بلندتر:
function getTTLByContentType(dataType) {
switch (dataType) {
case 'user-profile': return 3600; // 1 ساعت
case 'product-price': return 300; // 5 دقیقه
case 'static-config': return 86400; // 24 ساعت
case 'real-time-stats': return 10; // 10 ثانیه
default: return 3600;
}
}
3. TTL تصادفی (Randomized TTL) برای جلوگیری از Stampede
به جای اینکه همه کلیدها همزمان منقضی شوند، به TTL پایه یک مقدار تصادفی اضافه کنید:
const baseTTL = 3600;
const jitter = Math.floor(Math.random() * 600); // 0 تا 600 ثانیه تصادفی
const finalTTL = baseTTL + jitter;
await redis.setex(key, finalTTL, value);
4. TTL پویا بر اساس فرکانس دسترسی (Access Frequency-Based TTL)
کلیدهای پرطرفدار TTL بلندتر، کلیدهای کمطرفدار TTL کوتاهتر:
class AdaptiveTTLCache {
async getWithAdaptiveTTL(key, fetchFunction) {
let accessCount = await redis.hincrby('access-counts', key, 1);
// محاسبه TTL بر اساس تعداد دسترسیها (حداکثر 24 ساعت)
let ttl = Math.min(3600 + (accessCount * 10), 86400);
let data = await redis.get(key);
if (data) {
// اگر TTL جدید بزرگتر از TTL فعلی است، آن را افزایش بده
const currentTTL = await redis.ttl(key);
if (ttl > currentTTL) {
await redis.expire(key, ttl);
}
return JSON.parse(data);
}
// Cache Miss - بازسازی کن
data = await fetchFunction();
await redis.setex(key, ttl, JSON.stringify(data));
return data;
}
}
الگوی هشتم: Hot Key Detection و Handling
در سیستمهای واقعی، معمولاً تعداد کمی از کلیدها (کمتر از ۱٪) مسئول بیش از ۸۰٪ ترافیک هستند. این کلیدها Hot Keys نامیده میشوند و میتوانند باعث ایجاد گلوگاه در یک نود خاص از Redis Cluster شوند.
راهکار ۱: Local Cache (کش سطح برنامه)
// استفاده از کش محلی در کنار Redis برای Hot Keys
const NodeCache = require('node-cache');
const localCache = new NodeCache({ stdTTL: 60, checkperiod: 120 });
async function getWithLocalCache(key, fetchFunction) {
// ابتدا کش محلی را بررسی کن
let value = localCache.get(key);
if (value !== undefined) {
return value;
}
// سپس Redis
value = await redis.get(key);
if (value) {
localCache.set(key, value, 60); // ذخیره در کش محلی برای 60 ثانیه
return JSON.parse(value);
}
// نهایتاً دیتابیس اصلی
value = await fetchFunction();
await redis.setex(key, 3600, JSON.stringify(value));
localCache.set(key, value, 60);
return value;
}
راهکار ۲: Replication (کپی کردن کلید داغ روی چند نود)
در Redis Cluster میتوانید کلیدهای داغ را روی چندین replica نیز قرار دهید تا ترافیک توزیع شود.
// تشخیص خودکار Hot Keys با Redis Monitor
async function detectHotKeys() {
const monitor = redis.monitor();
const accessCount = new Map();
monitor.on('monitor', (time, args) => {
if (args[0] === 'get' || args[0] === 'getex') {
const key = args[1];
accessCount.set(key, (accessCount.get(key) || 0) + 1);
}
});
// هر 10 ثانیه گزارش بده
setInterval(() => {
const sorted = [...accessCount.entries()].sort((a, b) => b[1] - a[1]);
console.log('Hot Keys:', sorted.slice(0, 10));
accessCount.clear();
}, 10000);
}
الگوی نهم: Cache Invalidation Patterns
یکی از دشوارترین بخشهای Redis Caching Patterns، مدیریت بیاعتبارسازی کش (Cache Invalidation) است. دو قانون مشهور در علوم کامپیوتر میگوید: “Two hard things in computer science: cache invalidation and naming things.”
استراتژیهای Invalidation
1. Invalidation بر اساس زمان (TTL)
سادهترین روش: اجازه بده کش خودش منقضی شود.
2. Invalidation صریح (Explicit Invalidation)
// وقتی داده در دیتابیس تغییر میکند، کش را حذف کن
async function updateProduct(productId, newData) {
// بهروزرسانی در دیتابیس
await db.query('UPDATE products SET ... WHERE id = $1', [productId, newData]);
// حذف کش محصول
await redis.del(`product:${productId}`);
// حذف کش لیست محصولات (چون ترتیب ممکن است تغییر کند)
await redis.del('products:list:page1');
await redis.del('products:list:page2');
// حذف کش جستجوها (با استفاده از الگو)
const searchKeys = await redis.keys('search:*');
if (searchKeys.length > 0) {
await redis.del(searchKeys);
}
}
3. Invalidation آبشاری (Cascade Invalidation)
گاهی تغییر یک داده باعث میشود چندین کش مختلف نامعتبر شوند. از Redis Pub/Sub برای اعلان استفاده کنید:
// هنگام تغییر دستهبندی محصولات
async function updateCategory(categoryId, newName) {
await db.query('UPDATE categories SET name = $1 WHERE id = $2', [newName, categoryId]);
// انتشار رویداد در Redis
await redis.publish('cache-invalidation', JSON.stringify({
type: 'category-updated',
categoryId: categoryId,
timestamp: Date.now()
}));
}
// در سایر سرویسها که به این دستهبندی وابسته هستند
redis.subscribe('cache-invalidation', (message) => {
const event = JSON.parse(message);
if (event.type === 'category-updated') {
// حذف تمام کشهای مربوط به این دسته
redis.del(`category:${event.categoryId}`);
redis.del(`products:by-category:${event.categoryId}`);
}
});
4. Invalidation مبتنی بر نسخه (Version-Based)
به جای حذف کش، از نسخهگذاری استفاده کنید:
// ذخیره با نسخه
async function saveWithVersion(key, value, version) {
await redis.hset(key, {
data: JSON.stringify(value),
version: version
});
}
// خواندن با بررسی نسخه
async function getWithVersion(key, currentVersion) {
const data = await redis.hgetall(key);
if (data && data.version === currentVersion) {
return JSON.parse(data.data);
}
return null; // کش نامعتبر است
}

ابزارهای مانیتورینگ برای Redis Caching Patterns
برای اطمینان از کارایی صحیح Redis Caching Patterns، مانیتورینگ ضروری است. مهمترین متریکها:
متریکهای کلیدی
- Cache Hit Ratio: نسبت درخواستهایی که در کش پیدا شدهاند. ایدهآل بالای ۹۰٪.
- حافظه استفاده شده: نباید از حافظه فیزیکی سرور بیشتر شود.
- Eviction Rate: نرخی که Redis کلیدها را به دلیل پر شدن حافظه حذف میکند.
- Latency: زمان پاسخدهی Redis (باید زیر ۱ میلیثانیه باشد).
- Connected Clients: تعداد کلاینتهای متصل.
دستورات مفید Redis برای مانیتورینگ
# آمار کلی
INFO stats
# میزان هیت و میس کش
INFO stats | grep keyspace
# مشاهده کلیدهای داغ (نیازمند Redis 4.0+)
redis-cli --hotkeys
# مانیتور لحظهای
redis-cli monitor
# مشاهده حافظه مصرفی هر کلید (نیازمند redis-rdb-tools)
redis-memory-usage key:name
ابزارهای پیشنهادی از دانا پدیا
- Redis Insight: ابزار رسمی GUI برای مانیتورینگ Redis
- Prometheus + Grafana: برای ذخیرهسازی و نمایش متریکها
- Datadog: مانیتورینگ حرفهای (پولی)
- redis-stat: ابزار خط فرمان ساده
اشتباهات رایج در پیادهسازی Redis Caching Patterns
بر اساس تجربیات تیم فنی دانا پدیا، رایجترین اشتباهات هنگام کار با Redis Caching Patterns عبارتند از:
۱. انتخاب TTL نامناسب
TTL خیلی کوتاه → کش مفید نیست. TTL خیلی بلند → دادههای stale. راهکار: از TTL پویا استفاده کنید.
۲. فراموش کردن serialization/deserialization
// اشتباه
await redis.set('key', { name: 'John' }); // ارور!
// درست
await redis.set('key', JSON.stringify({ name: 'John' }));
۳. استفاده از KEYS در production
دستور KEYS * در Redis کل دیتاست را اسکن میکند و اجرای آن در production با دادههای زیاد، Redis را از کار میاندازد. به جای آن از SCAN استفاده کنید.
۴. نداشتن fallback برای خرابی Redis
همیشه فرض کنید Redis ممکن است از کار بیفتد. کد شما باید در این شرایط نیز به درستی کار کند (مثلاً مستقیماً به دیتابیس برود).