Node.js و Express

استراتيجيات التخزين المؤقت باستخدام Redis

50 دقيقة الدرس 22 من 40

استراتيجيات التخزين المؤقت باستخدام Redis

Redis (خادم القاموس البعيد) هو مخزن هيكل بيانات في الذاكرة يمكن استخدامه كقاعدة بيانات وذاكرة تخزين مؤقت ووسيط رسائل وقائمة انتظار. إنه سريع للغاية، ويدعم هياكل بيانات غنية، ومثالي لتنفيذ استراتيجيات التخزين المؤقت التي تحسن أداء التطبيق بشكل كبير.

لماذا استخدام Redis للتخزين المؤقت؟

تأثير الأداء: عمليات Redis عادة ما تكتمل في أقل من ميلي ثانية. بالمقارنة مع استعلامات قاعدة البيانات (10-100 مللي ثانية) أو استدعاءات API (100-1000 مللي ثانية)، يمكن لـ Redis تقليل أوقات الاستجابة بمقدار 10-100 مرة، مما يجعلها ضرورية للتطبيقات عالية الأداء.

فوائد Redis:

  • التخزين في الذاكرة - عمليات قراءة/كتابة سريعة للغاية
  • هياكل بيانات غنية (السلاسل، الجداول المجزأة، القوائم، المجموعات، المجموعات المرتبة)
  • TTL مدمج (وقت البقاء) لانتهاء صلاحية ذاكرة التخزين المؤقت التلقائي
  • مراسلة Pub/Sub لميزات الوقت الفعلي
  • عمليات ومعاملات ذرية
  • خيارات الاستمرارية للمتانة
  • التجميع والنسخ المتماثل لقابلية التوسع

تثبيت Redis

قم بتثبيت Redis على نظامك:

# macOS brew install redis brew services start redis # Ubuntu/Debian sudo apt update sudo apt install redis-server sudo systemctl start redis-server # التحقق من التثبيت redis-cli ping # يجب أن يرجع: PONG

قم بتثبيت عميل Redis لـ Node.js:

# استخدام ioredis (موصى به) npm install ioredis # أو استخدام redis (العميل الرسمي) npm install redis

الاتصال بـ Redis من Node.js

أنشئ عميل Redis باستخدام ioredis:

// config/redis.js const Redis = require('ioredis'); const redis = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: process.env.REDIS_PORT || 6379, password: process.env.REDIS_PASSWORD, db: 0, retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); return delay; }, maxRetriesPerRequest: 3 }); redis.on('connect', () => { console.log('عميل Redis متصل'); }); redis.on('error', (err) => { console.error('خطأ Redis:', err); }); redis.on('reconnecting', () => { console.log('عميل Redis يعيد الاتصال'); }); module.exports = redis;

استخدام عميل redis الرسمي:

// config/redis.js const redis = require('redis'); const client = redis.createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379', socket: { reconnectStrategy: (retries) => Math.min(retries * 50, 2000) } }); client.on('error', (err) => console.error('خطأ عميل Redis', err)); client.on('connect', () => console.log('عميل Redis متصل')); (async () => { await client.connect(); })(); module.exports = client;

عمليات Redis الأساسية

أوامر Redis الشائعة في Node.js:

const redis = require('./config/redis'); // عمليات السلاسل async function basicOperations() { // SET - تخزين قيمة await redis.set('key', 'value'); // SET مع انتهاء الصلاحية (بالثواني) await redis.setex('key', 60, 'قيمة تنتهي صلاحيتها في 60 ثانية'); // أو استخدام ioredis: await redis.set('key', 'value', 'EX', 60); // GET - استرداد قيمة const value = await redis.get('key'); console.log(value); // "value" // DEL - حذف مفتاح await redis.del('key'); // EXISTS - التحقق من وجود المفتاح const exists = await redis.exists('key'); // 1 إذا كان موجوداً، 0 إذا لم يكن // INCR - زيادة رقم await redis.set('counter', 0); await redis.incr('counter'); // 1 await redis.incrby('counter', 5); // 6 // EXPIRE - تعيين انتهاء صلاحية على مفتاح موجود await redis.set('temp', 'data'); await redis.expire('temp', 300); // تنتهي صلاحيته في 5 دقائق // TTL - التحقق من وقت البقاء const ttl = await redis.ttl('temp'); // يرجع الثواني حتى انتهاء الصلاحية // MGET/MSET - عمليات متعددة await redis.mset('key1', 'value1', 'key2', 'value2'); const values = await redis.mget('key1', 'key2'); // ['value1', 'value2'] }

استراتيجيات التخزين المؤقت

1. Cache-Aside (التحميل الكسول):

نمط التخزين المؤقت الأكثر شيوعاً - يدير كود التطبيق كلاً من ذاكرة التخزين المؤقت وقاعدة البيانات:

// نمط Cache-aside async function getUser(userId) { const cacheKey = `user:${userId}`; // حاول الحصول من ذاكرة التخزين المؤقت أولاً const cached = await redis.get(cacheKey); if (cached) { console.log('إصابة ذاكرة التخزين المؤقت!'); return JSON.parse(cached); } // فقدان ذاكرة التخزين المؤقت - جلب من قاعدة البيانات console.log('فقدان ذاكرة التخزين المؤقت - جلب من قاعدة البيانات'); const user = await db.users.findById(userId); if (user) { // تخزين في ذاكرة التخزين المؤقت لمدة ساعة await redis.setex(cacheKey, 3600, JSON.stringify(user)); } return user; } // الاستخدام const user = await getUser(123);
إيجابيات Cache-Aside: سهل التنفيذ، يخزن مؤقتاً فقط ما هو مطلوب، مرن لفشل ذاكرة التخزين المؤقت (يعود إلى قاعدة البيانات).
السلبيات: الطلبات الأولية بطيئة (فقدان ذاكرة التخزين المؤقت)، يمكن أن تصبح ذاكرة التخزين المؤقت قديمة إذا لم يتم إبطالها بشكل صحيح.

2. Write-Through Cache:

تتم كتابة البيانات إلى ذاكرة التخزين المؤقت وقاعدة البيانات في وقت واحد:

// التخزين المؤقت Write-through async function updateUser(userId, updates) { const cacheKey = `user:${userId}`; // تحديث قاعدة البيانات const user = await db.users.findByIdAndUpdate(userId, updates, { new: true }); // تحديث ذاكرة التخزين المؤقت فوراً await redis.setex(cacheKey, 3600, JSON.stringify(user)); return user; } // متسق دائماً ولكن عمليات كتابة أبطأ const updatedUser = await updateUser(123, { name: 'أحمد محمد' });

3. Write-Behind (Write-Back) Cache:

تتم كتابة البيانات إلى ذاكرة التخزين المؤقت أولاً، ثم تتم كتابتها بشكل غير متزامن إلى قاعدة البيانات:

// التخزين المؤقت Write-behind const pendingWrites = new Map(); async function updateUserWriteBehind(userId, updates) { const cacheKey = `user:${userId}`; // تحديث ذاكرة التخزين المؤقت فوراً const user = { id: userId, ...updates, updatedAt: Date.now() }; await redis.setex(cacheKey, 3600, JSON.stringify(user)); // وضع كتابة قاعدة البيانات في قائمة الانتظار pendingWrites.set(userId, { user, timestamp: Date.now() }); return user; } // عملية خلفية للمزامنة مع قاعدة البيانات setInterval(async () => { for (const [userId, data] of pendingWrites.entries()) { try { await db.users.findByIdAndUpdate(userId, data.user); pendingWrites.delete(userId); } catch (error) { console.error('فشل الكتابة إلى قاعدة البيانات:', error); } } }, 5000); // مزامنة كل 5 ثوان
تحذير: التخزين المؤقت Write-behind سريع لكنه محفوف بالمخاطر. إذا تعطلت ذاكرة التخزين المؤقت قبل كتابة البيانات إلى قاعدة البيانات، ستفقد البيانات. استخدم فقط عندما يمكنك تحمل فقدان البيانات أو تنفيذ استمرارية مناسبة.

أنماط التخزين المؤقت المتقدمة

إبطال ذاكرة التخزين المؤقت:

// أدوات إبطال ذاكرة التخزين المؤقت class CacheManager { constructor(redis) { this.redis = redis; } // إبطال مفتاح محدد async invalidate(key) { await this.redis.del(key); } // إبطال المفاتيح حسب النمط async invalidatePattern(pattern) { const keys = await this.redis.keys(pattern); if (keys.length > 0) { await this.redis.del(...keys); } } // إبطال مفاتيح متعددة مرتبطة async invalidateUser(userId) { await this.invalidatePattern(`user:${userId}:*`); await this.invalidate(`user:${userId}`); } // اللمس (تحديث TTL دون تغيير القيمة) async touch(key, ttl = 3600) { await this.redis.expire(key, ttl); } } // الاستخدام const cacheManager = new CacheManager(redis); // عندما يحدث المستخدم الملف الشخصي await db.users.update(userId, updates); await cacheManager.invalidateUser(userId); // عندما ينشر المستخدم شيئاً await db.posts.create(postData); await cacheManager.invalidate(`user:${userId}:posts`);

منع تدافع ذاكرة التخزين المؤقت:

منع طلبات متعددة متزامنة من الوصول إلى قاعدة البيانات عند انتهاء صلاحية ذاكرة التخزين المؤقت:

// منع تدافع ذاكرة التخزين المؤقت باستخدام الأقفال const locks = new Map(); async function getWithLock(key, fetchFunction, ttl = 3600) { // جرب ذاكرة التخزين المؤقت أولاً const cached = await redis.get(key); if (cached) { return JSON.parse(cached); } // الحصول على القفل const lockKey = `lock:${key}`; const lockId = Math.random().toString(36); // محاولة تعيين القفل (NX = فقط إذا لم يكن موجوداً، EX = انتهاء الصلاحية) const acquired = await redis.set(lockKey, lockId, 'NX', 'EX', 10); if (acquired) { try { // لدينا القفل - جلب البيانات const data = await fetchFunction(); await redis.setex(key, ttl, JSON.stringify(data)); return data; } finally { // تحرير القفل (فقط إذا كنا لا نزال نمتلكه) const script = ` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end `; await redis.eval(script, 1, lockKey, lockId); } } else { // القفل محتفظ به بواسطة طلب آخر - انتظر وأعد المحاولة await new Promise(resolve => setTimeout(resolve, 50)); return getWithLock(key, fetchFunction, ttl); } } // الاستخدام const user = await getWithLock( `user:${userId}`, () => db.users.findById(userId), 3600 );

هياكل بيانات Redis للتخزين المؤقت

الجداول المجزأة - مثالية لتخزين الكائنات:

// تخزين المستخدم كجدول مجزأ async function cacheUserAsHash(userId, user) { const key = `user:${userId}`; // تخزين الكائن بأكمله await redis.hset(key, { id: user.id, name: user.name, email: user.email, role: user.role }); // تعيين انتهاء الصلاحية await redis.expire(key, 3600); } // الحصول على حقول محددة async function getUserFields(userId, fields) { const key = `user:${userId}`; const values = await redis.hmget(key, ...fields); return values; } // تحديث حقل واحد async function updateUserField(userId, field, value) { const key = `user:${userId}`; await redis.hset(key, field, value); } // الاستخدام await cacheUserAsHash(123, { id: 123, name: 'أحمد', email: 'ahmad@example.com', role: 'admin' }); const name = await redis.hget('user:123', 'name'); const [name, email] = await getUserFields(123, ['name', 'email']);

القوائم - لقوائم الانتظار والعناصر الأخيرة:

// التخزين المؤقت للمنشورات الأخيرة async function addRecentPost(userId, post) { const key = `user:${userId}:recent_posts`; // إضافة إلى بداية القائمة await redis.lpush(key, JSON.stringify(post)); // الاحتفاظ فقط بآخر 10 منشورات await redis.ltrim(key, 0, 9); // تعيين انتهاء الصلاحية await redis.expire(key, 3600); } // الحصول على المنشورات الأخيرة async function getRecentPosts(userId, limit = 10) { const key = `user:${userId}:recent_posts`; const posts = await redis.lrange(key, 0, limit - 1); return posts.map(p => JSON.parse(p)); }

المجموعات - للمجموعات الفريدة:

// التخزين المؤقت لمتابعي المستخدم async function cacheFollowers(userId, followerIds) { const key = `user:${userId}:followers`; await redis.sadd(key, ...followerIds); await redis.expire(key, 3600); } // التحقق مما إذا كان المستخدم يتابع async function isFollowing(userId, targetId) { const key = `user:${userId}:followers`; return await redis.sismember(key, targetId); } // الحصول على المتابعين المشتركين async function getMutualFollowers(userId1, userId2) { const key1 = `user:${userId1}:followers`; const key2 = `user:${userId2}:followers`; return await redis.sinter(key1, key2); }

المجموعات المرتبة - للتصنيفات ولوحات الصدارة:

// التخزين المؤقت للوحة الصدارة async function updateScore(userId, score) { await redis.zadd('leaderboard', score, userId); } // الحصول على أفضل 10 مستخدمين async function getTopUsers(limit = 10) { const results = await redis.zrevrange('leaderboard', 0, limit - 1, 'WITHSCORES'); // تحويل إلى مصفوفة من الكائنات const users = []; for (let i = 0; i < results.length; i += 2) { users.push({ userId: results[i], score: parseInt(results[i + 1]) }); } return users; } // الحصول على رتبة المستخدم async function getUserRank(userId) { const rank = await redis.zrevrank('leaderboard', userId); return rank !== null ? rank + 1 : null; }

إدارة الجلسات مع Redis

تخزين جلسات Express في Redis لقابلية التوسع:

// تثبيت التبعيات // npm install express-session connect-redis const session = require('express-session'); const RedisStore = require('connect-redis').default; const redis = require('./config/redis'); app.use(session({ store: new RedisStore({ client: redis }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 1000 * 60 * 60 * 24 // 24 ساعة } })); // استخدام الجلسات app.post('/login', async (req, res) => { const user = await authenticateUser(req.body); req.session.userId = user.id; req.session.role = user.role; res.json({ success: true }); }); app.get('/profile', (req, res) => { if (!req.session.userId) { return res.status(401).json({ error: 'غير مصادق' }); } res.json({ userId: req.session.userId }); }); app.post('/logout', (req, res) => { req.session.destroy((err) => { if (err) { return res.status(500).json({ error: 'فشل تسجيل الخروج' }); } res.json({ success: true }); }); });

Redis Pub/Sub

استخدام Redis للمراسلة في الوقت الفعلي بين الخدمات:

// الناشر const Redis = require('ioredis'); const publisher = new Redis(); async function publishEvent(channel, data) { await publisher.publish(channel, JSON.stringify(data)); } // نشر الأحداث await publishEvent('user:created', { userId: 123, name: 'أحمد' }); await publishEvent('order:completed', { orderId: 456, total: 99.99 }); // المشترك const subscriber = new Redis(); subscriber.subscribe('user:created', 'order:completed', (err) => { if (err) { console.error('خطأ في الاشتراك:', err); } }); subscriber.on('message', (channel, message) => { console.log(`رسالة مستلمة على ${channel}:`, JSON.parse(message)); // معالجة الأحداث if (channel === 'user:created') { // إرسال بريد إلكتروني ترحيبي، إنشاء ملف تعريف المستخدم، إلخ. } else if (channel === 'order:completed') { // إرسال تأكيد، تحديث المخزون، إلخ. } });

تمرين: بناء API محدد المعدل مع Redis

  1. أنشئ API Express مع نقاط نهاية متعددة
  2. نفذ تحديد المعدل باستخدام Redis (مثلاً، 100 طلب في الساعة لكل IP)
  3. قم بتخزين استجابات API مؤقتاً لطلبات GET مع TTLs مناسبة
  4. نفذ إبطال ذاكرة التخزين المؤقت عند تحديث البيانات عبر POST/PUT
  5. أضف نقطة نهاية فحص صحة Redis
  6. تتبع إحصاءات استخدام API في مجموعات Redis المرتبة
  7. أنشئ لوحة معلومات تعرض إصابات ذاكرة التخزين المؤقت مقابل قاعدة البيانات

أفضل الممارسات

أفضل ممارسات Redis:
  • استخدم TTLs مناسبة - ليست قصيرة جداً (تهزم التخزين المؤقت) أو طويلة جداً (بيانات قديمة)
  • استخدم مساحة أسماء لمفاتيحك: user:123:profile بدلاً من 123profile
  • استخدم هياكل بيانات Redis بشكل مناسب (جداول مجزأة للكائنات، مجموعات للتجميعات)
  • نفذ تسخين ذاكرة التخزين المؤقت للبيانات التي يتم الوصول إليها بشكل متكرر
  • راقب استخدام الذاكرة ونفذ سياسات الإخلاء
  • استخدم الأنابيب لعمليات متعددة لتقليل رحلات الشبكة
  • قم بتمكين الاستمرارية (RDB/AOF) للبيانات المخزنة مؤقتاً المهمة
  • قم بإعداد النسخ المتماثل لتوفر عالي
// خط أنابيب لعمليات متعددة const pipeline = redis.pipeline(); pipeline.set('key1', 'value1'); pipeline.set('key2', 'value2'); pipeline.incr('counter'); pipeline.expire('key1', 60); const results = await pipeline.exec(); // معاملة (عمليات ذرية) const result = await redis .multi() .incr('counter') .expire('counter', 60) .exec();

Redis أداة قوية يمكن أن تحسن أداء تطبيقك بشكل كبير. أتقن استراتيجيات التخزين المؤقت هذه وستبني تطبيقات أسرع وأكثر قابلية للتوسع.