Redis والتخزين المؤقت المتقدم

التخزين المؤقت لـ HTTP

16 دقيقة الدرس 8 من 30

التخزين المؤقت لـ HTTP

التخزين المؤقت لـ HTTP هو واحد من أقوى تقنيات تحسين الأداء وأقلها استخداماً. من خلال الاستفادة من ذاكرة التخزين المؤقت للمتصفح وCDN عبر رؤوس HTTP الصحيحة، يمكنك تقليل حمل الخادم بشكل كبير وتحسين تجربة المستخدم.

رأس Cache-Control

رأس Cache-Control هو الآلية الأساسية للتحكم في سلوك ذاكرة التخزين المؤقت:

// مثال Express.js
app.get('/api/products', (req, res) => {
  res.set('Cache-Control', 'public, max-age=3600');
  res.json(products);
});

توجيهات Cache-Control:

  • public - يمكن تخزين الاستجابة مؤقتاً بواسطة أي ذاكرة تخزين مؤقت (متصفح، CDN، وكيل)
  • private - يمكن تخزين الاستجابة مؤقتاً فقط بواسطة المتصفح (وليس CDN/وكيل)
  • no-cache - يجب إعادة التحقق من الخادم قبل استخدام الاستجابة المخزنة مؤقتاً
  • no-store - لا تخزن مؤقتاً على الإطلاق (بيانات حساسة)
  • max-age=N - ذاكرة التخزين المؤقت صالحة لـ N ثانية
  • s-maxage=N - مثل max-age ولكن فقط لذاكرات التخزين المؤقت المشتركة (CDN)
  • must-revalidate - يجب عدم استخدام ذاكرة تخزين مؤقت قديمة، يجب إعادة التحقق
  • immutable - لن يتغير المورد أبداً (مثالي للأصول المُصنفة)

استراتيجيات التخزين المؤقت الشائعة

// الأصول الثابتة (CSS، JS، صور مع تجزئة الإصدار)
// هذه الملفات لا تتغير أبداً (إصدار جديد = اسم ملف جديد)
app.use('/static', express.static('public', {
  maxAge: '1y', // سنة واحدة
  immutable: true
}));

// استجابات API التي تتغير أحياناً
app.get('/api/config', (req, res) => {
  res.set('Cache-Control', 'public, max-age=300'); // 5 دقائق
  res.json(config);
});

// بيانات خاصة بالمستخدم
app.get('/api/user/profile', (req, res) => {
  res.set('Cache-Control', 'private, max-age=60'); // دقيقة واحدة، متصفح فقط
  res.json(userProfile);
});

// بيانات حساسة
app.get('/api/user/credit-card', (req, res) => {
  res.set('Cache-Control', 'no-store'); // لا تخزن مؤقتاً أبداً
  res.json(sensitiveData);
});

// محتوى ديناميكي يجب إعادة التحقق منه
app.get('/api/news', (req, res) => {
  res.set('Cache-Control', 'public, max-age=60, must-revalidate');
  res.json(news);
});
نصيحة: استخدم public, max-age=31536000, immutable للأصول الثابتة مع أسماء ملفات تعتمد على المحتوى (مثل، app.a3f2b1.js). هذا يوفر أقصى فوائد التخزين المؤقت.

ETag (علامة الكيان)

تمكن ETags الطلبات الشرطية - يرسل الخادم معرفاً فريداً لكل إصدار من المورد:

const crypto = require('crypto');

app.get('/api/products', async (req, res) => {
  const products = await db.products.findAll();
  const content = JSON.stringify(products);

  // إنشاء ETag من تجزئة المحتوى
  const etag = crypto
    .createHash('md5')
    .update(content)
    .digest('hex');

  // تحقق مما إذا كان العميل لديه الإصدار الحالي
  if (req.headers['if-none-match'] === etag) {
    // لم يتغير المحتوى
    return res.status(304).end(); // لم يتم التعديل
  }

  // تغير المحتوى، أرسل إصداراً جديداً
  res.set({
    'ETag': etag,
    'Cache-Control': 'public, max-age=60'
  });
  res.json(products);
});
ملاحظة: ETags مثالية لاستجابات API حيث لا يمكنك التنبؤ بموعد تغيير البيانات ولكنك تريد تجنب إرسال بيانات لم تتغير.

رأس Last-Modified

مماثل لـ ETag ولكن يستخدم الطوابع الزمنية بدلاً من تجزئات المحتوى:

app.get('/api/article/:id', async (req, res) => {
  const article = await db.articles.findById(req.params.id);
  const lastModified = article.updated_at;

  // تحليل رأس If-Modified-Since
  const ifModifiedSince = req.headers['if-modified-since'];
  if (ifModifiedSince) {
    const clientDate = new Date(ifModifiedSince);
    const serverDate = new Date(lastModified);

    if (serverDate <= clientDate) {
      return res.status(304).end(); // لم يتم التعديل
    }
  }

  res.set({
    'Last-Modified': lastModified.toUTCString(),
    'Cache-Control': 'public, max-age=300'
  });
  res.json(article);
});

رأس Vary

يخبر رأس Vary ذاكرات التخزين المؤقت بأن الاستجابة تعتمد على رؤوس طلب معينة:

// تختلف الاستجابة حسب Accept-Language
app.get('/api/messages', (req, res) => {
  const lang = req.headers['accept-language']?.split(',')[0] || 'en';
  const messages = getMessagesForLanguage(lang);

  res.set({
    'Cache-Control': 'public, max-age=3600',
    'Vary': 'Accept-Language' // تخزين منفصل لكل لغة
  });
  res.json(messages);
});

// تختلف الاستجابة حسب Accept-Encoding
app.get('/api/large-data', (req, res) => {
  res.set({
    'Cache-Control': 'public, max-age=86400',
    'Vary': 'Accept-Encoding' // تخزين مضغوط وغير مضغوط منفصلاً
  });
  res.json(largeData);
});

// رؤوس vary متعددة
app.get('/api/content', (req, res) => {
  res.set({
    'Cache-Control': 'public, max-age=1800',
    'Vary': 'Accept-Language, Accept-Encoding'
  });
  res.json(content);
});
تحذير: استخدام Vary: Cookie أو Vary: Authorization يمكن أن يجعل التخزين المؤقت لـ CDN غير فعال. استخدم هذه بشكل مقتصد وفكر في أساليب بديلة للمحتوى الشخصي.

Express Middleware للتخزين المؤقت لـ HTTP

أنشئ middleware قابلة لإعادة الاستخدام لأنماط التخزين المؤقت الشائعة:

// middleware ذاكرة تخزين مؤقت عامة
function cache(seconds, options = {}) {
  return (req, res, next) => {
    const { isPublic = true, mustRevalidate = false, vary } = options;

    let cacheControl = isPublic ? 'public' : 'private';
    cacheControl += `, max-age=${seconds}`;
    if (mustRevalidate) cacheControl += ', must-revalidate';

    res.set('Cache-Control', cacheControl);
    if (vary) res.set('Vary', vary);

    next();
  };
}

// middleware ETag
function etag() {
  return (req, res, next) => {
    const originalJson = res.json.bind(res);

    res.json = function(data) {
      const content = JSON.stringify(data);
      const hash = crypto
        .createHash('md5')
        .update(content)
        .digest('hex');

      if (req.headers['if-none-match'] === hash) {
        return res.status(304).end();
      }

      res.set('ETag', hash);
      return originalJson(data);
    };

    next();
  };
}

// الاستخدام
app.get('/api/products',
  cache(300), // 5 دقائق
  etag(),
  async (req, res) => {
    const products = await db.products.findAll();
    res.json(products);
  }
);

app.get('/api/user/profile',
  cache(60, { isPublic: false }), // دقيقة واحدة، خاصة
  async (req, res) => {
    const profile = await getUserProfile(req.userId);
    res.json(profile);
  }
);

تكامل CDN

تحسين رؤوس ذاكرة التخزين المؤقت لـ CDN (CloudFlare، Fastly، AWS CloudFront):

// مدد تخزين مؤقت مختلفة للمتصفح مقابل CDN
app.get('/api/static/config', (req, res) => {
  res.set({
    // المتصفحات تخزن مؤقتاً لمدة 5 دقائق
    // CDN تخزن مؤقتاً لمدة ساعة واحدة
    'Cache-Control': 'public, max-age=300, s-maxage=3600',
    'CDN-Cache-Control': 'max-age=3600' // خاص بـ CloudFlare
  });
  res.json(config);
});

// تجاوز CDN للمحتوى الشخصي
app.get('/api/recommendations', (req, res) => {
  res.set({
    'Cache-Control': 'private, max-age=60',
    'Surrogate-Control': 'no-store' // أخبر CDN بعدم التخزين المؤقت
  });
  res.json(recommendations);
});

Stale-While-Revalidate

تقديم محتوى قديم أثناء جلب بيانات جديدة في الخلفية:

app.get('/api/feed', (req, res) => {
  res.set({
    // تخزين مؤقت لمدة 30 ثانية
    // السماح بتقديم قديم لمدة 60 ثانية أثناء إعادة التحقق
    'Cache-Control': 'public, max-age=30, stale-while-revalidate=60'
  });
  res.json(feed);
});
ملاحظة: stale-while-revalidate يوفر أفضل تجربة للمستخدم - يحصل المستخدمون دائماً على استجابات فورية بينما يتم تحديث ذاكرة التخزين المؤقت في الخلفية.

تصحيح أخطاء ذاكرة التخزين المؤقت

أضف رؤوس تصحيح لفهم سلوك ذاكرة التخزين المؤقت:

function cacheDebug() {
  return (req, res, next) => {
    const start = Date.now();

    res.on('finish', () => {
      const duration = Date.now() - start;
      res.set({
        'X-Cache-Status': res.statusCode === 304 ? 'HIT' : 'MISS',
        'X-Response-Time': `${duration}ms`
      });
    });

    next();
  };
}

// تمكين في التطوير
if (process.env.NODE_ENV === 'development') {
  app.use(cacheDebug());
}
تمرين: أنشئ Express API مع ثلاث نقاط نهاية: واحدة للبيانات العامة الثابتة (تخزين مؤقت لساعة واحدة)، واحدة للبيانات الخاصة بالمستخدم (تخزين مؤقت خاص 5 دقائق)، وواحدة للبيانات التي تتغير بشكل متكرر باستخدام ETags. اختبر مع curl أو Postman للتحقق من رؤوس ذاكرة التخزين المؤقت واستجابات 304.