Redis والتخزين المؤقت المتقدم
بناء مشروع طبقة تخزين مؤقت (الجزء الأول)
بناء مشروع طبقة تخزين مؤقت (الجزء الأول)
لنبني طبقة تخزين مؤقت جاهزة للإنتاج لواجهة برمجة تطبيقات RESTful باستخدام Express.js و Redis. سيوضح هذا المشروع أنماط التخزين المؤقت الواقعية وأفضل الممارسات.
بنية المشروع
سنبني واجهة برمجة تطبيقات لكتالوج المنتجات مع استراتيجيات تخزين مؤقت متعددة:
// هيكل المشروع
product-api/
├── src/
│ ├── config/
│ │ └── redis.js // إعدادات Redis
│ ├── middleware/
│ │ ├── cache.js // وسيط الذاكرة المؤقتة
│ │ └── rateLimiter.js // تحديد المعدل
│ ├── services/
│ │ ├── cacheService.js // طبقة تجريد الذاكرة المؤقتة
│ │ └── productService.js // منطق الأعمال
│ ├── controllers/
│ │ └── productController.js
│ ├── routes/
│ │ └── products.js
│ └── app.js
├── package.json
└── .env
product-api/
├── src/
│ ├── config/
│ │ └── redis.js // إعدادات Redis
│ ├── middleware/
│ │ ├── cache.js // وسيط الذاكرة المؤقتة
│ │ └── rateLimiter.js // تحديد المعدل
│ ├── services/
│ │ ├── cacheService.js // طبقة تجريد الذاكرة المؤقتة
│ │ └── productService.js // منطق الأعمال
│ ├── controllers/
│ │ └── productController.js
│ ├── routes/
│ │ └── products.js
│ └── app.js
├── package.json
└── .env
ميزات المشروع:
- نمط cache-aside لبيانات المنتج
- إبطال الذاكرة المؤقتة عند التحديثات
- تسخين تلقائي للذاكرة المؤقتة
- مقاييس نجاح/فشل الذاكرة المؤقتة
- تحديد المعدل باستخدام Redis
- معالجة اتصال Redis بأناقة
إعداد API باستخدام Express
أولاً، لنقم بإعداد تطبيق Express الأساسي:
// package.json
{
"name": "product-api",
"version": "1.0.0",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js"
},
"dependencies": {
"express": "^4.18.2",
"redis": "^4.6.0",
"dotenv": "^16.0.3",
"mongoose": "^7.0.0"
},
"devDependencies": {
"nodemon": "^2.0.20"
}
}
{
"name": "product-api",
"version": "1.0.0",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js"
},
"dependencies": {
"express": "^4.18.2",
"redis": "^4.6.0",
"dotenv": "^16.0.3",
"mongoose": "^7.0.0"
},
"devDependencies": {
"nodemon": "^2.0.20"
}
}
// .env
PORT=3000
MONGODB_URI=mongodb://localhost:27017/productdb
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
CACHE_TTL=3600
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/productdb
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
CACHE_TTL=3600
NODE_ENV=development
// src/app.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const redisClient = require('./config/redis');
const productRoutes = require('./routes/products');
const app = express();
const PORT = process.env.PORT || 3000;
// الوسيطات
app.use(express.json());
// نقطة نهاية فحص الصحة
app.get('/health', async (req, res) => {
const health = {
uptime: process.uptime(),
timestamp: Date.now(),
mongodb: mongoose.connection.readyState === 1 ? 'connected' : 'disconnected',
redis: redisClient.isReady ? 'connected' : 'disconnected'
};
res.json(health);
});
// المسارات
app.use('/api/products', productRoutes);
// معالجة الأخطاء
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
// الاتصال بـ MongoDB
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('MongoDB متصل'))
.catch(err => console.error('خطأ في اتصال MongoDB:', err));
// بدء الخادم
app.listen(PORT, () => {
console.log(`الخادم يعمل على المنفذ ${PORT}`);
});
// الإغلاق الأنيق
process.on('SIGTERM', async () => {
console.log('تلقي SIGTERM، الإغلاق بشكل أنيق...');
await redisClient.quit();
await mongoose.connection.close();
process.exit(0);
});
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const redisClient = require('./config/redis');
const productRoutes = require('./routes/products');
const app = express();
const PORT = process.env.PORT || 3000;
// الوسيطات
app.use(express.json());
// نقطة نهاية فحص الصحة
app.get('/health', async (req, res) => {
const health = {
uptime: process.uptime(),
timestamp: Date.now(),
mongodb: mongoose.connection.readyState === 1 ? 'connected' : 'disconnected',
redis: redisClient.isReady ? 'connected' : 'disconnected'
};
res.json(health);
});
// المسارات
app.use('/api/products', productRoutes);
// معالجة الأخطاء
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
// الاتصال بـ MongoDB
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('MongoDB متصل'))
.catch(err => console.error('خطأ في اتصال MongoDB:', err));
// بدء الخادم
app.listen(PORT, () => {
console.log(`الخادم يعمل على المنفذ ${PORT}`);
});
// الإغلاق الأنيق
process.on('SIGTERM', async () => {
console.log('تلقي SIGTERM، الإغلاق بشكل أنيق...');
await redisClient.quit();
await mongoose.connection.close();
process.exit(0);
});
إعداد اتصال Redis
أنشئ عميل Redis قوي مع معالجة الأخطاء ومنطق إعادة الاتصال:
// src/config/redis.js
const redis = require('redis');
const redisClient = redis.createClient({
socket: {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
reconnectStrategy: (retries) => {
if (retries > 10) {
console.error('تم الوصول إلى حد إعادة الاتصال بـ Redis');
return new Error('فشل إعادة الاتصال بـ Redis');
}
// التراجع الأسي
return Math.min(retries * 100, 3000);
}
},
password: process.env.REDIS_PASSWORD || undefined,
database: 0
});
// معالجات الأحداث
redisClient.on('connect', () => {
console.log('عميل Redis يتصل...');
});
redisClient.on('ready', () => {
console.log('عميل Redis جاهز');
});
redisClient.on('error', (err) => {
console.error('خطأ في Redis:', err);
});
redisClient.on('reconnecting', () => {
console.log('عميل Redis يعيد الاتصال...');
});
// الاتصال بـ Redis
redisClient.connect().catch(err => {
console.error('فشل الاتصال بـ Redis:', err);
});
module.exports = redisClient;
const redis = require('redis');
const redisClient = redis.createClient({
socket: {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
reconnectStrategy: (retries) => {
if (retries > 10) {
console.error('تم الوصول إلى حد إعادة الاتصال بـ Redis');
return new Error('فشل إعادة الاتصال بـ Redis');
}
// التراجع الأسي
return Math.min(retries * 100, 3000);
}
},
password: process.env.REDIS_PASSWORD || undefined,
database: 0
});
// معالجات الأحداث
redisClient.on('connect', () => {
console.log('عميل Redis يتصل...');
});
redisClient.on('ready', () => {
console.log('عميل Redis جاهز');
});
redisClient.on('error', (err) => {
console.error('خطأ في Redis:', err);
});
redisClient.on('reconnecting', () => {
console.log('عميل Redis يعيد الاتصال...');
});
// الاتصال بـ Redis
redisClient.connect().catch(err => {
console.error('فشل الاتصال بـ Redis:', err);
});
module.exports = redisClient;
تنفيذ نمط Cache-Aside
أنشئ خدمة تخزين مؤقت قابلة لإعادة الاستخدام مع نمط cache-aside:
// src/services/cacheService.js
const redisClient = require('../config/redis');
class CacheService {
constructor() {
this.defaultTTL = parseInt(process.env.CACHE_TTL) || 3600;
this.metrics = {
hits: 0,
misses: 0,
errors: 0
};
}
/**
* الحصول على البيانات من الذاكرة المؤقتة أو جلبها من المصدر
* @param {string} key - مفتاح الذاكرة المؤقتة
* @param {Function} fetchFn - دالة لجلب البيانات عند فقدان الذاكرة المؤقتة
* @param {number} ttl - وقت البقاء بالثواني
*/
async get(key, fetchFn, ttl = this.defaultTTL) {
try {
// محاولة الحصول من الذاكرة المؤقتة
const cached = await redisClient.get(key);
if (cached) {
this.metrics.hits++;
return JSON.parse(cached);
}
// فقدان الذاكرة المؤقتة - جلب من المصدر
this.metrics.misses++;
const data = await fetchFn();
// التخزين في الذاكرة المؤقتة (نار وننسى)
this.set(key, data, ttl).catch(err => {
console.error('خطأ في تعيين الذاكرة المؤقتة:', err);
});
return data;
} catch (err) {
this.metrics.errors++;
console.error('خطأ في الحصول على الذاكرة المؤقتة:', err);
// التراجع إلى جلب البيانات مباشرة
return await fetchFn();
}
}
/**
* تعيين البيانات في الذاكرة المؤقتة
*/
async set(key, value, ttl = this.defaultTTL) {
try {
const serialized = JSON.stringify(value);
await redisClient.setEx(key, ttl, serialized);
return true;
} catch (err) {
console.error('خطأ في تعيين الذاكرة المؤقتة:', err);
return false;
}
}
/**
* حذف المفتاح من الذاكرة المؤقتة
*/
async del(key) {
try {
await redisClient.del(key);
return true;
} catch (err) {
console.error('خطأ في حذف الذاكرة المؤقتة:', err);
return false;
}
}
/**
* حذف مفاتيح متعددة تطابق النمط
*/
async delPattern(pattern) {
try {
const keys = await redisClient.keys(pattern);
if (keys.length > 0) {
await redisClient.del(keys);
}
return keys.length;
} catch (err) {
console.error('خطأ في حذف نمط الذاكرة المؤقتة:', err);
return 0;
}
}
/**
* الحصول على مقاييس الذاكرة المؤقتة
*/
getMetrics() {
const total = this.metrics.hits + this.metrics.misses;
return {
hits: this.metrics.hits,
misses: this.metrics.misses,
errors: this.metrics.errors,
hitRate: total > 0 ? (this.metrics.hits / total * 100).toFixed(2) : 0
};
}
/**
* إعادة تعيين المقاييس
*/
resetMetrics() {
this.metrics = { hits: 0, misses: 0, errors: 0 };
}
}
module.exports = new CacheService();
const redisClient = require('../config/redis');
class CacheService {
constructor() {
this.defaultTTL = parseInt(process.env.CACHE_TTL) || 3600;
this.metrics = {
hits: 0,
misses: 0,
errors: 0
};
}
/**
* الحصول على البيانات من الذاكرة المؤقتة أو جلبها من المصدر
* @param {string} key - مفتاح الذاكرة المؤقتة
* @param {Function} fetchFn - دالة لجلب البيانات عند فقدان الذاكرة المؤقتة
* @param {number} ttl - وقت البقاء بالثواني
*/
async get(key, fetchFn, ttl = this.defaultTTL) {
try {
// محاولة الحصول من الذاكرة المؤقتة
const cached = await redisClient.get(key);
if (cached) {
this.metrics.hits++;
return JSON.parse(cached);
}
// فقدان الذاكرة المؤقتة - جلب من المصدر
this.metrics.misses++;
const data = await fetchFn();
// التخزين في الذاكرة المؤقتة (نار وننسى)
this.set(key, data, ttl).catch(err => {
console.error('خطأ في تعيين الذاكرة المؤقتة:', err);
});
return data;
} catch (err) {
this.metrics.errors++;
console.error('خطأ في الحصول على الذاكرة المؤقتة:', err);
// التراجع إلى جلب البيانات مباشرة
return await fetchFn();
}
}
/**
* تعيين البيانات في الذاكرة المؤقتة
*/
async set(key, value, ttl = this.defaultTTL) {
try {
const serialized = JSON.stringify(value);
await redisClient.setEx(key, ttl, serialized);
return true;
} catch (err) {
console.error('خطأ في تعيين الذاكرة المؤقتة:', err);
return false;
}
}
/**
* حذف المفتاح من الذاكرة المؤقتة
*/
async del(key) {
try {
await redisClient.del(key);
return true;
} catch (err) {
console.error('خطأ في حذف الذاكرة المؤقتة:', err);
return false;
}
}
/**
* حذف مفاتيح متعددة تطابق النمط
*/
async delPattern(pattern) {
try {
const keys = await redisClient.keys(pattern);
if (keys.length > 0) {
await redisClient.del(keys);
}
return keys.length;
} catch (err) {
console.error('خطأ في حذف نمط الذاكرة المؤقتة:', err);
return 0;
}
}
/**
* الحصول على مقاييس الذاكرة المؤقتة
*/
getMetrics() {
const total = this.metrics.hits + this.metrics.misses;
return {
hits: this.metrics.hits,
misses: this.metrics.misses,
errors: this.metrics.errors,
hitRate: total > 0 ? (this.metrics.hits / total * 100).toFixed(2) : 0
};
}
/**
* إعادة تعيين المقاييس
*/
resetMetrics() {
this.metrics = { hits: 0, misses: 0, errors: 0 };
}
}
module.exports = new CacheService();
وسيط الذاكرة المؤقتة
أنشئ وسيط Express للتخزين المؤقت التلقائي للاستجابة:
// src/middleware/cache.js
const cacheService = require('../services/cacheService');
/**
* وسيط الذاكرة المؤقتة لمسارات Express
* @param {number} ttl - وقت البقاء بالثواني
* @param {Function} keyFn - دالة اختيارية لتوليد مفتاح الذاكرة المؤقتة
*/
const cacheMiddleware = (ttl = 3600, keyFn = null) => {
return async (req, res, next) => {
// تخزين مؤقت لطلبات GET فقط
if (req.method !== 'GET') {
return next();
}
// توليد مفتاح الذاكرة المؤقتة
const cacheKey = keyFn
? keyFn(req)
: `cache:${req.originalUrl || req.url}`;
try {
// محاولة الحصول من الذاكرة المؤقتة
const cached = await cacheService.get(
cacheKey,
() => null, // لا تجلب عند الفقدان، دع معالج المسار يفعل ذلك
ttl
);
if (cached) {
// تعيين رأس الذاكرة المؤقتة
res.set('X-Cache', 'HIT');
return res.json(cached);
}
// فقدان الذاكرة المؤقتة - اعترض الاستجابة
res.set('X-Cache', 'MISS');
const originalJson = res.json.bind(res);
res.json = (data) => {
// تخزين مؤقت للاستجابات الناجحة فقط
if (res.statusCode === 200 && data) {
cacheService.set(cacheKey, data, ttl).catch(err => {
console.error('فشل تخزين الاستجابة مؤقتاً:', err);
});
}
return originalJson(data);
};
next();
} catch (err) {
console.error('خطأ في وسيط الذاكرة المؤقتة:', err);
// الاستمرار دون تخزين مؤقت عند الخطأ
next();
}
};
};
/**
* وسيط لإبطال الذاكرة المؤقتة حسب النمط
*/
const invalidateCacheMiddleware = (patternFn) => {
return async (req, res, next) => {
// تخزين res.json الأصلي للاستدعاء بعد الإبطال
const originalJson = res.json.bind(res);
res.json = async (data) => {
// الإبطال فقط عند العمليات الناجحة
if (res.statusCode >= 200 && res.statusCode < 300) {
const pattern = patternFn(req);
await cacheService.delPattern(pattern);
}
return originalJson(data);
};
next();
};
};
module.exports = {
cacheMiddleware,
invalidateCacheMiddleware
};
const cacheService = require('../services/cacheService');
/**
* وسيط الذاكرة المؤقتة لمسارات Express
* @param {number} ttl - وقت البقاء بالثواني
* @param {Function} keyFn - دالة اختيارية لتوليد مفتاح الذاكرة المؤقتة
*/
const cacheMiddleware = (ttl = 3600, keyFn = null) => {
return async (req, res, next) => {
// تخزين مؤقت لطلبات GET فقط
if (req.method !== 'GET') {
return next();
}
// توليد مفتاح الذاكرة المؤقتة
const cacheKey = keyFn
? keyFn(req)
: `cache:${req.originalUrl || req.url}`;
try {
// محاولة الحصول من الذاكرة المؤقتة
const cached = await cacheService.get(
cacheKey,
() => null, // لا تجلب عند الفقدان، دع معالج المسار يفعل ذلك
ttl
);
if (cached) {
// تعيين رأس الذاكرة المؤقتة
res.set('X-Cache', 'HIT');
return res.json(cached);
}
// فقدان الذاكرة المؤقتة - اعترض الاستجابة
res.set('X-Cache', 'MISS');
const originalJson = res.json.bind(res);
res.json = (data) => {
// تخزين مؤقت للاستجابات الناجحة فقط
if (res.statusCode === 200 && data) {
cacheService.set(cacheKey, data, ttl).catch(err => {
console.error('فشل تخزين الاستجابة مؤقتاً:', err);
});
}
return originalJson(data);
};
next();
} catch (err) {
console.error('خطأ في وسيط الذاكرة المؤقتة:', err);
// الاستمرار دون تخزين مؤقت عند الخطأ
next();
}
};
};
/**
* وسيط لإبطال الذاكرة المؤقتة حسب النمط
*/
const invalidateCacheMiddleware = (patternFn) => {
return async (req, res, next) => {
// تخزين res.json الأصلي للاستدعاء بعد الإبطال
const originalJson = res.json.bind(res);
res.json = async (data) => {
// الإبطال فقط عند العمليات الناجحة
if (res.statusCode >= 200 && res.statusCode < 300) {
const pattern = patternFn(req);
await cacheService.delPattern(pattern);
}
return originalJson(data);
};
next();
};
};
module.exports = {
cacheMiddleware,
invalidateCacheMiddleware
};
استراتيجية مفتاح الذاكرة المؤقتة: استخدم مفاتيح وصفية وهرمية مثل `cache:products:list`, `cache:products:123`, `cache:categories:5:products` لتمكين الإبطال القائم على النمط وسهولة التصحيح.
نموذج المنتج والخدمة
أنشئ نموذج بيانات المنتج وطبقة الخدمة:
// src/models/product.js (نموذج Mongoose)
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
name: { type: String, required: true, index: true },
description: String,
price: { type: Number, required: true, min: 0 },
category: { type: String, required: true, index: true },
stock: { type: Number, default: 0 },
imageUrl: String,
tags: [String],
isActive: { type: Boolean, default: true }
}, {
timestamps: true
});
// فهارس للاستعلامات الشائعة
productSchema.index({ category: 1, price: 1 });
productSchema.index({ name: 'text', description: 'text' });
module.exports = mongoose.model('Product', productSchema);
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
name: { type: String, required: true, index: true },
description: String,
price: { type: Number, required: true, min: 0 },
category: { type: String, required: true, index: true },
stock: { type: Number, default: 0 },
imageUrl: String,
tags: [String],
isActive: { type: Boolean, default: true }
}, {
timestamps: true
});
// فهارس للاستعلامات الشائعة
productSchema.index({ category: 1, price: 1 });
productSchema.index({ name: 'text', description: 'text' });
module.exports = mongoose.model('Product', productSchema);
// src/services/productService.js
const Product = require('../models/product');
const cacheService = require('./cacheService');
class ProductService {
/**
* الحصول على جميع المنتجات مع التخزين المؤقت
*/
async getAll(filters = {}) {
const cacheKey = `products:list:${JSON.stringify(filters)}`;
return await cacheService.get(
cacheKey,
async () => {
const query = { isActive: true };
if (filters.category) query.category = filters.category;
if (filters.minPrice) query.price = { $gte: filters.minPrice };
if (filters.maxPrice) query.price = { ...query.price, $lte: filters.maxPrice };
return await Product.find(query)
.sort({ createdAt: -1 })
.limit(100)
.lean();
},
300 // 5 دقائق TTL للقائمة
);
}
/**
* الحصول على المنتج بالمعرف مع التخزين المؤقت
*/
async getById(id) {
const cacheKey = `products:${id}`;
return await cacheService.get(
cacheKey,
async () => {
const product = await Product.findById(id).lean();
if (!product) {
throw new Error('المنتج غير موجود');
}
return product;
},
3600 // ساعة واحدة TTL للمنتجات الفردية
);
}
/**
* إنشاء منتج جديد
*/
async create(productData) {
const product = await Product.create(productData);
// إبطال ذاكرة القائمة المؤقتة
await cacheService.delPattern('products:list:*');
return product;
}
/**
* تحديث المنتج وإبطال الذاكرة المؤقتة
*/
async update(id, updates) {
const product = await Product.findByIdAndUpdate(id, updates, { new: true });
if (!product) {
throw new Error('المنتج غير موجود');
}
// إبطال ذاكرة المنتج المحدد والقوائم المؤقتة
await cacheService.del(`products:${id}`);
await cacheService.delPattern('products:list:*');
return product;
}
/**
* حذف المنتج
*/
async delete(id) {
const product = await Product.findByIdAndDelete(id);
if (!product) {
throw new Error('المنتج غير موجود');
}
// إبطال الذاكرة المؤقتة
await cacheService.del(`products:${id}`);
await cacheService.delPattern('products:list:*');
return product;
}
}
module.exports = new ProductService();
const Product = require('../models/product');
const cacheService = require('./cacheService');
class ProductService {
/**
* الحصول على جميع المنتجات مع التخزين المؤقت
*/
async getAll(filters = {}) {
const cacheKey = `products:list:${JSON.stringify(filters)}`;
return await cacheService.get(
cacheKey,
async () => {
const query = { isActive: true };
if (filters.category) query.category = filters.category;
if (filters.minPrice) query.price = { $gte: filters.minPrice };
if (filters.maxPrice) query.price = { ...query.price, $lte: filters.maxPrice };
return await Product.find(query)
.sort({ createdAt: -1 })
.limit(100)
.lean();
},
300 // 5 دقائق TTL للقائمة
);
}
/**
* الحصول على المنتج بالمعرف مع التخزين المؤقت
*/
async getById(id) {
const cacheKey = `products:${id}`;
return await cacheService.get(
cacheKey,
async () => {
const product = await Product.findById(id).lean();
if (!product) {
throw new Error('المنتج غير موجود');
}
return product;
},
3600 // ساعة واحدة TTL للمنتجات الفردية
);
}
/**
* إنشاء منتج جديد
*/
async create(productData) {
const product = await Product.create(productData);
// إبطال ذاكرة القائمة المؤقتة
await cacheService.delPattern('products:list:*');
return product;
}
/**
* تحديث المنتج وإبطال الذاكرة المؤقتة
*/
async update(id, updates) {
const product = await Product.findByIdAndUpdate(id, updates, { new: true });
if (!product) {
throw new Error('المنتج غير موجود');
}
// إبطال ذاكرة المنتج المحدد والقوائم المؤقتة
await cacheService.del(`products:${id}`);
await cacheService.delPattern('products:list:*');
return product;
}
/**
* حذف المنتج
*/
async delete(id) {
const product = await Product.findByIdAndDelete(id);
if (!product) {
throw new Error('المنتج غير موجود');
}
// إبطال الذاكرة المؤقتة
await cacheService.del(`products:${id}`);
await cacheService.delPattern('products:list:*');
return product;
}
}
module.exports = new ProductService();
تمرين: أنشئ متحكم منتج يستخدم productService وينفذ نقاط النهاية التالية:
- GET /api/products - قائمة جميع المنتجات (مع فلتر فئة اختياري)
- GET /api/products/:id - الحصول على منتج واحد
- POST /api/products - إنشاء منتج جديد
- PUT /api/products/:id - تحديث المنتج
- DELETE /api/products/:id - حذف المنتج
ملخص الجزء الأول: لقد بنينا أساس طبقة التخزين المؤقت لدينا مع إدارة اتصال Redis، خدمة ذاكرة مؤقتة قابلة لإعادة الاستخدام تنفذ نمط cache-aside، وسيط Express للتخزين المؤقت التلقائي، وخدمة منتج مع إبطال ذاكرة مؤقتة متكامل. في الجزء الثاني، سنضيف إدارة الجلسات وتحديد المعدل والميزات في الوقت الفعلي.