كيف تستخدم Redis لتعزيز أداء واجهات برمجة التطبيقات (APIs) الخاصة بك
Redis لتعزيز أداء واجهات برمجة التطبيقات (APIs) الخاصة بنا، والتي يتم بناؤها باستخدام Node.js و MongoDB.
Redis: نظرة عامة مبسطة
وفقًا للوثائق الرسمية، يُعرّف Redis بأنه متجر هياكل بيانات موجود في الذاكرة (in-memory data structure store) يُستخدم كقاعدة بيانات، أو وسيط رسائل (message broker)، أو مخزن مؤقت (cache storage). يدعم Redis هياكل بيانات متنوعة مثل السلاسل النصية (strings)، الهاشات (hashes)، القوائم (lists)، المجموعات (sets)، المجموعات المرتبة مع استعلامات النطاق (sorted sets with range queries)، الخرائط النقطية (bitmaps)، hyperloglogs، الفهارس الجغرافية المكانية مع استعلامات نصف القطر (geospatial indexes with radius queries)، والتدفقات (streams).
قد تبدو هذه المجموعة الكبيرة من هياكل البيانات معقدة للوهلة الأولى. لتبسيط الأمر، يمكن تكثيف معظم هياكل البيانات المدعومة في شكل سلسلة نصية أو آخر. ستتضح الصورة بشكل أكبر مع تقدمنا في التطبيق العملي. لكن شيئًا واحدًا مؤكد: Redis أداة قوية، وعند استخدامها بشكل صحيح، يمكنها جعل تطبيقاتنا ليست أسرع فحسب، بل أكثر كفاءة بشكل مدهش. كفى حديثًا، لنبدأ بالجانب العملي.
لنبدأ بالجانب العملي: إعداد Redis
قبل البدء، ستحتاج إلى إعداد Redis في نظامك المحلي. يمكنك اتباع عملية الإعداد السريعة هذه لتشغيل Redis. هل انتهيت؟ رائع. لنبدأ.
لدينا تطبيق بسيط تم إنشاؤه باستخدام إطار عمل Express، والذي يستخدم نسخة من MongoDB Atlas لقراءة وكتابة البيانات. لقد أنشأنا واجهتي برمجة تطبيقات رئيسيتين (APIs) في ملف المسار /blogs.
...
// GET - Fetches all blog posts for required user
blogsRouter.route( '/:user' )
.get( async (req, res, next) => {
const blogs = await Blog.find({ user : req.params.user });
res.status( 200 ).json({ blogs, });
});
// POST - Creates a new blog post
blogsRouter.route( '/' )
.post( async (req, res, next) => {
const existingBlog = await Blog.findOne({ title : req.body.title });
if (!existingBlog) {
let newBlog = new Blog(req.body);
const result = await newBlog.save();
return res.status( 200 ).json({ message : `Blog ${result.id} is successfully created` , result, });
}
res.status( 200 ).json({ message : 'Blog with same title exists' , });
});
...
دمج Redis لتحسين الأداء
نبدأ بتنزيل حزمة npm المسماة redis للاتصال بخادم Redis المحلي.
const mongoose = require ( 'mongoose' );
const redis = require ( 'redis' );
const util = require ( 'util' );
const redisUrl = 'redis://127.0.0.1:6379' ;
const client = redis.createClient(redisUrl);
client.hget = util.promisify(client.hget);
...
نستخدم الدالة util.promisify لتحويل الدالة client.hget لتعيد وعدًا (promise) بدلاً من دالة رد نداء (callback). يمكنك قراءة المزيد حول عملية تحويل الدوال إلى وعود (promisification) هنا.
الآن، أصبح اتصال Redis جاهزًا. قبل أن نبدأ بكتابة المزيد من تعليمات التخزين المؤقت، دعنا نعود خطوة إلى الوراء ونحاول فهم المتطلبات التي نحتاج إلى تلبيتها والتحديات المحتملة التي قد نواجهها. يجب أن تكون استراتيجية التخزين المؤقت لدينا قادرة على معالجة النقاط التالية:
- تخزين مؤقت لطلب جميع منشورات المدونة لمستخدم معين.
- مسح التخزين المؤقت في كل مرة يتم فيها إنشاء منشور مدونة جديد.
التحديات المحتملة التي يجب أن نكون حذرين منها أثناء تطبيق استراتيجيتنا هي:
- الطريقة الصحيحة للتعامل مع إنشاء المفاتيح لتخزين بيانات التخزين المؤقت.
- منطق انتهاء صلاحية التخزين المؤقت والانتهاء الإجباري للحفاظ على تحديث البيانات.
- تنفيذ قابل لإعادة الاستخدام لمنطق التخزين المؤقت.
حسنًا. لقد سجلنا نقاطنا وتم توصيل Redis. لننتقل إلى الخطوة التالية.
تجاوز دالة Mongoose Exec الافتراضية
نريد أن يكون منطق التخزين المؤقت لدينا قابلًا لإعادة الاستخدام. وليس فقط قابلًا لإعادة الاستخدام، بل نريده أيضًا أن يكون نقطة الفحص الأولى قبل إجراء أي استعلام لقاعدة البيانات. يمكن تحقيق ذلك بسهولة باستخدام خدعة بسيطة تتمثل في التعديل على دالة mongoose exec.
...
const exec = mongoose.Query.prototype.exec;
...
mongoose.Query.prototype.exec = async function ( ) {
...
const result = await exec.apply( this , arguments );
console .log( 'Data Source: Database' );
return result;
}
...
نستخدم كائن النموذج الأولي (prototype object) الخاص بـ Mongoose لإضافة رمز منطق التخزين المؤقت الخاص بنا كأول تنفيذ في الاستعلام.
إضافة التخزين المؤقت كاستعلام
لتحديد الاستعلامات التي يجب أن تكون جاهزة للتخزين المؤقت، نقوم بإنشاء استعلام Mongoose. نوفر القدرة على تمرير user لاستخدامه كمفتاح تجزئة (hash-key) عبر كائن الخيارات (options object).
ملاحظة: يعمل مفتاح التجزئة كمعرف لهيكل بيانات التجزئة، والذي، بعبارات بسيطة، يمكن تعريفه على أنه المفتاح الأب لمجموعة من أزواج المفتاح-القيمة. وبالتالي، يتيح التخزين المؤقت لعدد أكبر من مجموعات الاستعلام-القيمة. يمكنك قراءة المزيد حول الهاشات في Redis هنا.
...
mongoose.Query.prototype.cache = function ( options = {} ) {
this .enableCache = true ;
this .hashKey = JSON .stringify(options.key || 'default' );
return this ;
};
...
بعد القيام بذلك، يمكننا بسهولة استخدام استعلام cache(<options argument>) جنبًا إلى جنب مع الاستعلامات التي نريد تخزينها مؤقتًا بالطريقة التالية:
...
const blogs = await Blog
.find({ user : req.params.user })
.cache({ key : req.params.user });
...
صياغة منطق التخزين المؤقت
لقد قمنا بإعداد استعلام مشترك قابل لإعادة الاستخدام لتحديد الاستعلامات التي تحتاج إلى التخزين المؤقت. دعنا نمضي قدمًا ونكتب منطق التخزين المؤقت المركزي.
...
mongoose.Query.prototype.exec = async function ( ) {
if (! this .enableCache) {
console .log( 'Data Source: Database' );
return exec.apply( this , arguments );
}
const key = JSON .stringify( Object .assign({}, this .getQuery(), {
collection : this .mongooseCollection.name,
}));
const cachedValue = await client.hget( this .hashKey, key);
if (cachedValue) {
const parsedCache = JSON .parse(cachedValue);
console .log( 'Data Source: Cache' );
return Array .isArray(parsedCache) ? parsedCache.map( doc => new this .model(doc)) : new this .model(parsedCache);
}
const result = await exec.apply( this , arguments );
client.hmset( this .hashKey, key, JSON .stringify(result), 'EX' , 300 );
console .log( 'Data Source: Database' );
return result;
};
...
كلما استخدمنا استعلام cache() جنبًا إلى جنب مع استعلامنا الرئيسي، نقوم بتعيين المفتاح enableCache ليكون true. إذا كان المفتاح false، فإننا نعيد استعلام exec الرئيسي كإعداد افتراضي.
إذا لم يكن كذلك، فإننا نشكل أولاً المفتاح لجلب بيانات التخزين المؤقت وتخزينها/تحديثها. نستخدم اسم المجموعة (collection name) جنبًا إلى جنب مع الاستعلام الافتراضي كاسم مفتاح لضمان التفرد. مفتاح التجزئة (hash-key) المستخدم هو اسم المستخدم (user) الذي قمنا بتعيينه سابقًا في تعريف دالة cache().
يتم جلب البيانات المخزنة مؤقتًا باستخدام دالة client.hget() التي تتطلب مفتاح التجزئة والمفتاح الناتج كمعاملات.
ملاحظة: نستخدم دائمًا JSON.parse() عند جلب أي بيانات من Redis. وبالمثل، نستخدم JSON.stringify() على المفتاح والبيانات قبل تخزين أي شيء في Redis. يتم ذلك لأن Redis لا يدعم هياكل بيانات JSON بشكل مباشر.
بمجرد حصولنا على البيانات المخزنة مؤقتًا، يجب علينا تحويل كل كائن مخزن مؤقتًا إلى نموذج Mongoose. يمكن القيام بذلك ببساطة باستخدام new this.model(<object>).
إذا لم يحتوِ التخزين المؤقت على البيانات المطلوبة، فإننا نجري استعلامًا لقاعدة البيانات. ثم، بعد إعادة البيانات إلى API، نقوم بتحديث التخزين المؤقت باستخدام client.hmset(). نقوم أيضًا بتعيين وقت انتهاء صلاحية افتراضي للتخزين المؤقت يبلغ 300 ثانية. هذا قابل للتخصيص بناءً على استراتيجية التخزين المؤقت الخاصة بك.
لقد أصبح منطق التخزين المؤقت جاهزًا. لقد قمنا أيضًا بتعيين وقت انتهاء صلاحية افتراضي. بعد ذلك، سننظر في فرض انتهاء صلاحية التخزين المؤقت كلما تم إنشاء منشور مدونة جديد.
فرض انتهاء صلاحية التخزين المؤقت
في حالات معينة، مثل عندما ينشئ المستخدم منشور مدونة جديدًا، يتوقع المستخدم أن يكون المنشور الجديد متاحًا عند جلب جميع المنشورات. لتحقيق ذلك، يجب علينا مسح التخزين المؤقت المتعلق بذلك المستخدم وتحديثه ببيانات جديدة. لذا يجب علينا فرض انتهاء الصلاحية. يمكننا القيام بذلك عن طريق استدعاء الدالة del() التي يوفرها Redis.
...
module.exports = {
clearCache(hashKey) {
console .log( 'Cache cleaned' );
client.del( JSON .stringify(hashKey));
}
}
...
يجب أن نضع في اعتبارنا أيضًا أننا سنفرض انتهاء الصلاحية على مسارات متعددة. إحدى الطرق القابلة للتوسع هي استخدام clearCache() هذه كبرمجية وسيطة (middleware) واستدعائها بمجرد انتهاء تنفيذ أي استعلام متعلق بمسار معين.
const { clearCache } = require ( '../services/cache' );
module .exports = async (req, res, next) => {
// wait for route handler to finish running
await next();
clearCache(req.body.user);
}
يمكن استدعاء هذه البرمجية الوسيطة بسهولة على مسار معين بالطريقة التالية:
...
blogsRouter.route( '/' )
.post(cleanCache, async (req, res, next) => {
...
}
...
وبهذا نكون قد انتهينا. أوافق على أن هذا كان قدرًا كبيرًا من التعليمات البرمجية. ولكن مع هذا الجزء الأخير، قمنا بإعداد Redis مع تطبيقنا واهتممنا بجميع التحديات المحتملة تقريبًا. حان الوقت لرؤية استراتيجية التخزين المؤقت لدينا في العمل.
Redis في العمل: مشاهدة الأداء المحسن
نستخدم Postman كعميل API لرؤية استراتيجية التخزين المؤقت لدينا وهي تعمل. لنستعرض عمليات API، واحدة تلو الأخرى.
نقوم بإنشاء منشور مدونة جديد باستخدام المسار /blogs.

إنشاء منشور مدونة جديد.
ثم نقوم بجلب جميع منشورات المدونة المتعلقة بالمستخدم tejaz.

جلب جميع منشورات المدونة للمستخدم tejaz.
نقوم بجلب جميع منشورات المدونة للمستخدم tejaz مرة أخرى.

جلب جميع منشورات المدونة للمستخدم tejaz مرة أخرى.
يمكنك أن ترى بوضوح أنه عندما نجلب البيانات من التخزين المؤقت، انخفض الوقت المستغرق من 409 مللي ثانية إلى 24 مللي ثانية. هذا يعزز أداء API الخاص بك عن طريق تقليل الوقت المستغرق بنسبة 95٪ تقريبًا. بالإضافة إلى ذلك، يمكننا أن نرى بوضوح أن عمليات انتهاء صلاحية التخزين المؤقت والتحديث تعمل كما هو متوقع.
يمكنك العثور على الكود المصدري الكامل في مجلد redis-express هنا: https://github.com/tarique93102/article-snippets/tree/master/redis-express
الخلاصة
يُعد التخزين المؤقت خطوة إلزامية لأي تطبيق يهدف إلى كفاءة الأداء ويحتوي على كميات كبيرة من البيانات. يساعدك Redis على تحقيق ذلك بسهولة في تطبيقات الويب الخاصة بك. إنها أداة فائقة القوة، وإذا تم استخدامها بشكل صحيح، يمكنها بالتأكيد توفير تجربة ممتازة للمطورين والمستخدمين على حد سواء.
يمكنك العثور على المجموعة الكاملة من أوامر Redis هنا. يمكنك استخدامها مع redis-cli لمراقبة بيانات التخزين المؤقت وعمليات التطبيق الخاصة بك. إن الإمكانيات التي توفرها أي تقنية معينة لا حصر لها حقًا.
إذا كانت لديك أي استفسارات، يمكنك التواصل معي عبر LinkedIn. في هذه الأثناء، استمر في البرمجة.
الخلاصة التقنية
يُظهر هذا المقال بوضوح كيف يمكن لـ Redis أن يُحدث تحولًا جذريًا في أداء واجهات برمجة التطبيقات (APIs) المبنية باستخدام Node.js و MongoDB. من خلال استراتيجية التخزين المؤقت الذكية التي تتضمن تجاوز دالة exec في Mongoose وتطبيق منطق التخزين المؤقت المخصص، يمكن للمطورين تحقيق سرعات استجابة مذهلة. إن القدرة على تخزين البيانات مؤقتًا في الذاكرة ومسحها بشكل انتقائي عند تحديث البيانات تضمن أن المستخدمين يحصلون دائمًا على أحدث المعلومات بأقصى سرعة ممكنة. هذا النهج لا يقلل فقط من الحمل على قاعدة البيانات، بل يعزز أيضًا تجربة المستخدم بشكل كبير، مما يجعله مكونًا أساسيًا في تطوير التطبيقات الحديثة عالية الأداء.