التخزين المؤقت لاستجابات API
التخزين المؤقت لاستجابات API
التخزين المؤقت لاستجابات API أمر بالغ الأهمية لبناء خدمات ويب عالية الأداء. من خلال التخزين المؤقت لاستجابات API في Redis، يمكنك تقليل حمل قاعدة البيانات، وتقليل أوقات الاستجابة، وتحسين قابلية التوسع. يغطي هذا الدرس أنماطاً عملية للتخزين المؤقت لاستجابات REST API في Node.js.
Middleware التخزين المؤقت الأساسي لـ API
أنشئ middleware بسيطة تخزن طلبات GET مؤقتاً:
const Redis = require('ioredis');
const app = express();
const redis = new Redis();
// middleware التخزين المؤقت الأساسية
function apiCache(durationSeconds) {
return async (req, res, next) => {
// تخزين طلبات GET فقط مؤقتاً
if (req.method !== 'GET') {
return next();
}
const key = `cache:${req.originalUrl}`;
try {
// حاول الحصول على الاستجابة المخزنة مؤقتاً
const cachedResponse = await redis.get(key);
if (cachedResponse) {
console.log('إصابة ذاكرة التخزين المؤقت:', key);
return res.json(JSON.parse(cachedResponse));
}
console.log('فشل ذاكرة التخزين المؤقت:', key);
// اعترض res.json لتخزين الاستجابة مؤقتاً
const originalJson = res.json.bind(res);
res.json = function(data) {
// تخزين الاستجابة مؤقتاً
redis.setex(key, durationSeconds, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (error) {
console.error('خطأ ذاكرة التخزين المؤقت:', error);
next(); // الاستمرار بدون ذاكرة تخزين مؤقت عند الخطأ
}
};
}
// الاستخدام
app.get('/api/products',
apiCache(300), // التخزين المؤقت لمدة 5 دقائق
async (req, res) => {
const products = await db.query('SELECT * FROM products');
res.json(products);
}
);
تصميم مفتاح ذاكرة التخزين المؤقت المتقدم
صمم مفاتيح ذاكرة تخزين مؤقت تأخذ في الاعتبار معاملات الاستعلام والترقيم والمرشحات:
const baseKey = req.path;
const params = req.query;
// فرز معاملات الاستعلام للحصول على مفاتيح متسقة
const sortedParams = Object.keys(params)
.sort()
.map(key => `${key}=${params[key]}`)
.join('&');
return sortedParams
? `cache:${baseKey}?${sortedParams}`
: `cache:${baseKey}`;
}
// middleware محسنة مع توليد مفتاح مخصص
function smartApiCache(durationSeconds, options = {}) {
const { keyGenerator = generateCacheKey } = options;
return async (req, res, next) => {
if (req.method !== 'GET') return next();
const key = keyGenerator(req);
try {
const cachedResponse = await redis.get(key);
if (cachedResponse) {
const data = JSON.parse(cachedResponse);
res.set('X-Cache', 'HIT');
return res.json(data);
}
res.set('X-Cache', 'MISS');
const originalJson = res.json.bind(res);
res.json = function(data) {
redis.setex(key, durationSeconds, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (error) {
console.error('خطأ ذاكرة التخزين المؤقت:', error);
next();
}
};
}
// الاستخدام مع معاملات الاستعلام
app.get('/api/products', smartApiCache(300), async (req, res) => {
const { category, page = 1, limit = 20 } = req.query;
const products = await db.products.find({ category, page, limit });
res.json(products);
});
/api/products?page=1&limit=20 و /api/products?limit=20&page=1 تولد نفس مفتاح ذاكرة التخزين المؤقت.التخزين المؤقت الشرطي
تخزين الاستجابات مؤقتاً فقط بناءً على رموز الحالة وأنواع المحتوى:
return async (req, res, next) => {
if (req.method !== 'GET') return next();
const key = generateCacheKey(req);
try {
const cached = await redis.get(key);
if (cached) {
const { status, headers, body } = JSON.parse(cached);
res.status(status).set(headers).json(body);
return;
}
const originalJson = res.json.bind(res);
res.json = function(data) {
// تخزين الاستجابات الناجحة فقط مؤقتاً (رموز حالة 2xx)
if (res.statusCode >= 200 && res.statusCode < 300) {
const cacheData = {
status: res.statusCode,
headers: res.getHeaders(),
body: data
};
redis.setex(key, durationSeconds, JSON.stringify(cacheData));
}
return originalJson(data);
};
next();
} catch (error) {
console.error('خطأ ذاكرة التخزين المؤقت:', error);
next();
}
};
}
التخزين المؤقت الخاص بالمستخدم
تخزين الاستجابات مؤقتاً لكل مستخدم لـ APIs المصادق عليها:
return async (req, res, next) => {
if (req.method !== 'GET') return next();
// تضمين معرف المستخدم في مفتاح ذاكرة التخزين المؤقت
const userId = req.user?.id || 'anonymous';
const baseKey = generateCacheKey(req);
const key = `${baseKey}:user:${userId}`;
try {
const cached = await redis.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
const originalJson = res.json.bind(res);
res.json = function(data) {
redis.setex(key, durationSeconds, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (error) {
console.error('خطأ ذاكرة التخزين المؤقت:', error);
next();
}
};
}
// الاستخدام
app.get('/api/user/dashboard',
authenticateUser, // middleware المصادقة
userSpecificCache(120), // التخزين المؤقت لمدة دقيقتين لكل مستخدم
async (req, res) => {
const dashboard = await getDashboardData(req.user.id);
res.json(dashboard);
}
);
إبطال ذاكرة التخزين المؤقت
نفذ إبطال ذاكرة التخزين المؤقت عند تعديل البيانات:
async function invalidateCache(pattern) {
try {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
console.log(`تم إبطال ${keys.length} إدخال ذاكرة تخزين مؤقت`);
}
} catch (error) {
console.error('خطأ إبطال ذاكرة التخزين المؤقت:', error);
}
}
// الإبطال على POST/PUT/DELETE
app.post('/api/products', async (req, res) => {
const product = await db.products.create(req.body);
// إبطال جميع ذاكرات التخزين المؤقت لقائمة المنتجات
await invalidateCache('cache:/api/products*');
res.status(201).json(product);
});
app.put('/api/products/:id', async (req, res) => {
const product = await db.products.update(req.params.id, req.body);
// إبطال ذاكرات التخزين المؤقت للمنتج المحدد والقائمة
await invalidateCache(`cache:/api/products/${req.params.id}*`);
await invalidateCache('cache:/api/products?*');
await invalidateCache('cache:/api/products');
res.json(product);
});
app.delete('/api/products/:id', async (req, res) => {
await db.products.delete(req.params.id);
// إبطال جميع ذاكرات التخزين المؤقت ذات الصلة
await invalidateCache('cache:/api/products*');
res.status(204).send();
});
رؤوس ذاكرة التخزين المؤقت للتخزين المؤقت من جانب العميل
ادمج التخزين المؤقت في Redis مع رؤوس ذاكرة التخزين المؤقت لـ HTTP:
return async (req, res, next) => {
if (req.method !== 'GET') return next();
const key = generateCacheKey(req);
try {
const cached = await redis.get(key);
if (cached) {
res.set({
'Cache-Control': `public, max-age=${httpMaxAge}`,
'X-Cache': 'HIT'
});
return res.json(JSON.parse(cached));
}
const originalJson = res.json.bind(res);
res.json = function(data) {
res.set({
'Cache-Control': `public, max-age=${httpMaxAge}`,
'X-Cache': 'MISS'
});
redis.setex(key, redisTTL, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (error) {
console.error('خطأ ذاكرة التخزين المؤقت:', error);
next();
}
};
}
// التخزين المؤقت في Redis لمدة 5 دقائق، المتصفح لمدة دقيقة واحدة
app.get('/api/config',
apiCacheWithHeaders(300, 60),
async (req, res) => {
const config = await getAppConfig();
res.json(config);
}
);
تحليلات ذاكرة التخزين المؤقت
تتبع مقاييس أداء ذاكرة التخزين المؤقت:
hits: 0,
misses: 0,
errors: 0
};
function analyticsCache(durationSeconds) {
return async (req, res, next) => {
if (req.method !== 'GET') return next();
const key = generateCacheKey(req);
const startTime = Date.now();
try {
const cached = await redis.get(key);
if (cached) {
cacheStats.hits++;
const duration = Date.now() - startTime;
res.set({
'X-Cache': 'HIT',
'X-Cache-Time': `${duration}ms`
});
return res.json(JSON.parse(cached));
}
cacheStats.misses++;
const originalJson = res.json.bind(res);
res.json = function(data) {
const duration = Date.now() - startTime;
res.set({
'X-Cache': 'MISS',
'X-Response-Time': `${duration}ms`
});
redis.setex(key, durationSeconds, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (error) {
cacheStats.errors++;
console.error('خطأ ذاكرة التخزين المؤقت:', error);
next();
}
};
}
// نقطة نهاية إحصائيات ذاكرة التخزين المؤقت
app.get('/api/cache/stats', (req, res) => {
const total = cacheStats.hits + cacheStats.misses;
const hitRate = total > 0 ? (cacheStats.hits / total * 100).toFixed(2) : 0;
res.json({
hits: cacheStats.hits,
misses: cacheStats.misses,
errors: cacheStats.errors,
hitRate: `${hitRate}%`,
total
});
});