التخزين المؤقت لاستعلامات قاعدة البيانات
التخزين المؤقت لاستعلامات قاعدة البيانات
يقلل التخزين المؤقت لاستعلامات قاعدة البيانات من حمل قاعدة البيانات عن طريق تخزين نتائج الاستعلامات في Redis. تعمل هذه التقنية على تحسين أداء التطبيق بشكل كبير من خلال تقديم النتائج المخزنة مؤقتًا بدلاً من تنفيذ استعلامات مكلفة بشكل متكرر.
لماذا نخزن استعلامات قاعدة البيانات مؤقتًا؟
غالبًا ما تكون استعلامات قاعدة البيانات هي الجزء الأبطأ في تطبيقات الويب. يوفر التخزين المؤقت لنتائج الاستعلامات استجابات فورية ويقلل من حمل خادم قاعدة البيانات.
التخزين المؤقت على مستوى 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);تدفئة الذاكرة المؤقتة
املأ الذاكرة المؤقتة بشكل استباقي بالبيانات التي يتم الوصول إليها بشكل متكرر قبل أن يطلبها المستخدمون.
// تدفئة الذاكرة المؤقتة عند بدء التشغيل\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});إبطال الذاكرة المؤقتة عند الكتابة
أبطل الاستعلامات المخزنة مؤقتًا تلقائيًا عند تغيير البيانات لمنع البيانات القديمة.
// إبطال الذاكرة المؤقتة عند تغييرات النموذج\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:*');تنفيذ التخزين المؤقت في 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});