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

التخزين المؤقت لاستعلامات قاعدة البيانات

20 دقيقة الدرس 17 من 30

التخزين المؤقت لاستعلامات قاعدة البيانات

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

لماذا نخزن استعلامات قاعدة البيانات مؤقتًا؟

غالبًا ما تكون استعلامات قاعدة البيانات هي الجزء الأبطأ في تطبيقات الويب. يوفر التخزين المؤقت لنتائج الاستعلامات استجابات فورية ويقلل من حمل خادم قاعدة البيانات.

تأثير الأداء: يمكن أن تكون الاستعلامات المخزنة مؤقتًا أسرع بـ 100 مرة من استعلامات قاعدة البيانات، مما يقلل أوقات الاستجابة من 100 مللي ثانية إلى 1 مللي ثانية.

التخزين المؤقت على مستوى ORM

نفّذ التخزين المؤقت على مستوى ORM لتخزين جميع الاستعلامات مؤقتًا بشفافية دون تغيير كود التطبيق.

// إضافة Mongoose للتخزين المؤقت في Redis\nconst redis = require('redis');\nconst client = redis.createClient();\n\nfunction cachePlugin(schema) {\n  schema.post('find', async function(docs) {\n    const key = `query:${this.getQuery()}`;\n    await client.setEx(key, 300, JSON.stringify(docs));\n  });\n  \n  schema.pre('find', async function() {\n    const key = `query:${this.getQuery()}`;\n    const cached = await client.get(key);\n    \n    if (cached) {\n      this._cachedResult = JSON.parse(cached);\n      return this._cachedResult;\n    }\n  });\n}\n\nUserSchema.plugin(cachePlugin);

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

خزّن استعلامات محددة مكلفة مؤقتًا بقيم TTL مخصصة بناءً على متطلبات حداثة البيانات.

const redis = require('redis');\nconst client = redis.createClient();\n\nasync function getCachedQuery(key, queryFn, ttl = 300) {\n  // جرّب الذاكرة المؤقتة أولاً\n  const cached = await client.get(key);\n  if (cached) {\n    return JSON.parse(cached);\n  }\n  \n  // تنفيذ الاستعلام\n  const result = await queryFn();\n  \n  // تخزين النتيجة مؤقتاً\n  await client.setEx(key, ttl, JSON.stringify(result));\n  \n  return result;\n}\n\n// الاستخدام\nconst users = await getCachedQuery(\n  'users:active',\n  () => User.find({ active: true }),\n  600 // 10 دقائق\n);
نصيحة: استخدم قيم TTL أطول للبيانات نادرة التغيير (الفئات، الإعدادات) وقيم TTL أقصر للبيانات المحدثة بشكل متكرر (أعداد المستخدمين، المشاركات الأخيرة).

تدفئة الذاكرة المؤقتة

املأ الذاكرة المؤقتة بشكل استباقي بالبيانات التي يتم الوصول إليها بشكل متكرر قبل أن يطلبها المستخدمون.

// تدفئة الذاكرة المؤقتة عند بدء التشغيل\nasync function warmCache() {\n  console.log('تدفئة الذاكرة المؤقتة...');\n  \n  // الاستعلامات الشائعة\n  const queries = [\n    { key: 'products:featured', fn: () => Product.find({ featured: true }) },\n    { key: 'categories:all', fn: () => Category.find() },\n    { key: 'users:top', fn: () => User.find().sort({ points: -1 }).limit(10) }\n  ];\n  \n  for (const { key, fn } of queries) {\n    const data = await fn();\n    await client.setEx(key, 3600, JSON.stringify(data));\n  }\n  \n  console.log('تمت تدفئة الذاكرة المؤقتة!');\n}\n\n// التشغيل عند بدء الخادم\nwarmCache();

التخزين المؤقت لمشكلة N+1

تحدث مشكلة استعلام N+1 عند جلب البيانات ذات الصلة في حلقات. يمكن أن يخفف التخزين المؤقت من هذه المشكلة.

// بدون تخزين مؤقت - مشكلة N+1\nconst posts = await Post.find();\nfor (const post of posts) {\n  post.author = await User.findById(post.authorId); // N استعلامات!\n}\n\n// مع التخزين المؤقت\nconst posts = await Post.find();\nconst authorIds = [...new Set(posts.map(p => p.authorId))];\n\n// جلب دفعي مع ذاكرة مؤقتة\nconst authors = await Promise.all(\n  authorIds.map(id => getCachedQuery(\n    `user:${id}`,\n    () => User.findById(id),\n    3600\n  ))\n);\n\nconst authorMap = Object.fromEntries(\n  authors.map(a => [a._id, a])\n);\n\nposts.forEach(post => {\n  post.author = authorMap[post.authorId];\n});
حل N+1: ادمج نمط DataLoader مع التخزين المؤقت في Redis للحصول على أداء مثالي مع البيانات ذات الصلة.

إبطال الذاكرة المؤقتة عند الكتابة

أبطل الاستعلامات المخزنة مؤقتًا تلقائيًا عند تغيير البيانات لمنع البيانات القديمة.

// إبطال الذاكرة المؤقتة عند تغييرات النموذج\nUserSchema.post('save', async function(doc) {\n  // مسح الذاكرة المؤقتة الخاصة بالمستخدم\n  await client.del(`user:${doc._id}`);\n  \n  // مسح ذاكرة القوائم المؤقتة التي قد تتضمن هذا المستخدم\n  await client.del('users:active');\n  await client.del('users:all');\n});\n\nUserSchema.post('remove', async function(doc) {\n  await client.del(`user:${doc._id}`);\n  await client.del('users:active');\n});\n\n// إبطال قائم على النمط\nasync function invalidatePattern(pattern) {\n  const keys = await client.keys(pattern);\n  if (keys.length > 0) {\n    await client.del(keys);\n  }\n}\n\n// إبطال جميع ذاكرات المستخدم المؤقتة\nawait invalidatePattern('user:*');
تحذير: يمكن أن يكون الأمر KEYS بطيئًا على مجموعات البيانات الكبيرة. استخدم SCAN لبيئات الإنتاج أو احتفظ بفهرس منفصل لمفاتيح الذاكرة المؤقتة.

تنفيذ التخزين المؤقت في Mongoose

مثال كامل للتخزين المؤقت لاستعلامات Mongoose مع إبطال تلقائي.

const mongoose = require('mongoose');\nconst redis = require('redis');\nconst client = redis.createClient();\n\n// توسيع نموذج Query الأولي\nmongoose.Query.prototype.cache = function(ttl = 300) {\n  this._cache = true;\n  this._cacheTTL = ttl;\n  return this;\n};\n\nconst exec = mongoose.Query.prototype.exec;\n\nmongoose.Query.prototype.exec = async function() {\n  if (!this._cache) {\n    return exec.apply(this, arguments);\n  }\n  \n  const key = JSON.stringify({\n    collection: this.mongooseCollection.name,\n    query: this.getQuery(),\n    options: this.getOptions()\n  });\n  \n  // فحص الذاكرة المؤقتة\n  const cached = await client.get(key);\n  if (cached) {\n    const doc = JSON.parse(cached);\n    return Array.isArray(doc)\n      ? doc.map(d => new this.model(d))\n      : new this.model(doc);\n  }\n  \n  // تنفيذ الاستعلام\n  const result = await exec.apply(this, arguments);\n  \n  // تخزين النتيجة مؤقتاً\n  await client.setEx(key, this._cacheTTL, JSON.stringify(result));\n  \n  return result;\n};\n\n// الاستخدام\nconst users = await User\n  .find({ active: true })\n  .cache(600); // التخزين المؤقت لمدة 10 دقائق

تنفيذ التخزين المؤقت في Sequelize

نفّذ التخزين المؤقت للاستعلامات لـ Sequelize ORM بأنماط مماثلة.

const Sequelize = require('sequelize');\nconst redis = require('redis');\nconst client = redis.createClient();\n\n// دالة مغلفة\nasync function cachedQuery(model, query, options = {}) {\n  const { ttl = 300, cacheKey } = options;\n  \n  const key = cacheKey || `${model.name}:${JSON.stringify(query)}`;\n  \n  // فحص الذاكرة المؤقتة\n  const cached = await client.get(key);\n  if (cached) {\n    return JSON.parse(cached);\n  }\n  \n  // تنفيذ الاستعلام\n  const result = await model.findAll(query);\n  \n  // تخزين النتيجة مؤقتاً\n  await client.setEx(key, ttl, JSON.stringify(result));\n  \n  return result;\n}\n\n// الاستخدام\nconst users = await cachedQuery(\n  User,\n  { where: { active: true } },\n  { ttl: 600, cacheKey: 'users:active' }\n);
نصيحة: قم بتضمين معاملات الاستعلام والخيارات في مفاتيح الذاكرة المؤقتة للتأكد من عدم تصادم الاستعلامات المختلفة.

إبطال ذكي للذاكرة المؤقتة

نفّذ إبطالًا ذكيًا للذاكرة المؤقتة بناءً على علاقات البيانات.

class CacheManager {\n  constructor(redisClient) {\n    this.client = redisClient;\n    this.dependencies = new Map();\n  }\n  \n  // تسجيل تبعيات الذاكرة المؤقتة\n  addDependency(entity, cacheKeys) {\n    this.dependencies.set(entity, cacheKeys);\n  }\n  \n  // الإبطال بناءً على تغييرات الكيان\n  async invalidateEntity(entity, id) {\n    const keys = this.dependencies.get(entity) || [];\n    \n    // إضافة مفتاح خاص بالكيان\n    keys.push(`${entity}:${id}`);\n    \n    // حذف جميع المفاتيح ذات الصلة\n    if (keys.length > 0) {\n      await this.client.del(keys);\n    }\n  }\n}\n\n// إعداد التبعيات\nconst cache = new CacheManager(client);\ncache.addDependency('User', ['users:all', 'users:active']);\ncache.addDependency('Post', ['posts:recent', 'posts:featured']);\n\n// الإبطال عند التغييرات\nUser.afterSave(async (user) => {\n  await cache.invalidateEntity('User', user.id);\n});
تمرين: أنشئ نظام تخزين مؤقت لتطبيق مدونة. خزّن قوائم المشاركات والمشاركات الفردية وبيانات المؤلفين مؤقتًا. نفّذ إبطالًا تلقائيًا عند إنشاء المشاركات أو تحديثها أو حذفها. أضف تدفئة للذاكرة المؤقتة للمشاركات الـ 10 الأكثر شعبية. راقب معدلات نجاح الذاكرة المؤقتة وحسّن قيم TTL بناءً على أنماط الوصول إلى البيانات.