LRU Cache في JavaScript: سرّع القراءات المتكررة بـ Map
هتطلع من المقال ده بكلاس JavaScript صغير يعمل LRU Cache فعلي، ويقلل قراءات قاعدة البيانات المتكررة من عشرات المللي ثانية إلى 2 مللي ثانية تقريبًا في الحالات المناسبة.
مستوى القارئ: متوسط
المشكلة باختصار
لو صفحة المنتج عندك بتطلب نفس بيانات المنتج كل شوية، قراءة قاعدة البيانات في كل مرة مش أفضل حل. الافتراض إن عندك API بيدخل له 50 ألف زائر يوميًا، وأشهر 1000 منتج بياخدوا 70% من الطلبات. اللي بيحصل فعلاً إنك بتعيد نفس الاستعلام آلاف المرات، رغم إن النتيجة غالبًا لم تتغير خلال دقيقة أو دقيقتين.
الحل هنا مش إنك ترمي Redis في أي مكان. أحيانًا كاش صغير داخل نفس عملية Node.js يكفي. التكلفة: الكاش يضيع عند restart، ومش مشترك بين أكثر من instance. المكسب: تنفيذ سريع جدًا، وتعقيد أقل، ومناسب للبيانات غير الحساسة التي تتحمل stale بسيط.
الفكرة بمثال واضح
ركز في المثال ده. عندك رف صغير على مكتبك يسع 3 ملفات فقط. كل مرة تحتاج ملف، لو موجود على المكتب تاخده فورًا. لو مش موجود، تقوم تجيبه من الأرشيف البعيد. لو المكتب امتلأ، تشيل أقل ملف استخدمته مؤخرًا وتحط الملف الجديد مكانه.
ده بالظبط معنى LRU: Least Recently Used. أنت لا تحذف أقدم عنصر بالزمن المطلق فقط، بل تحذف العنصر الذي لم يُستخدم منذ أطول فترة. في JavaScript، Map مناسب للفكرة لأنه يحفظ ترتيب إدخال المفاتيح، ويدعم get وset وdelete. عند قراءة عنصر موجود، نحذفه ونضيفه مرة أخرى في آخر الـ Map. كده يبقى الأحدث استخدامًا في آخر الترتيب.
كود LRU Cache قابل للنسخ
الكود التالي مناسب كبداية داخل خدمة Node.js واحدة. الرقم 1000 هنا مش مقدس. اختاره بناءً على حجم العنصر في الذاكرة وعدد العناصر الساخنة عندك.
class LRUCache {
constructor(limit = 1000) {
this.limit = limit;
this.store = new Map();
}
get(key) {
if (!this.store.has(key)) return undefined;
const value = this.store.get(key);
this.store.delete(key);
this.store.set(key, value);
return value;
}
set(key, value) {
if (this.store.has(key)) {
this.store.delete(key);
}
this.store.set(key, value);
if (this.store.size > this.limit) {
const oldestKey = this.store.keys().next().value;
this.store.delete(oldestKey);
}
}
size() {
return this.store.size;
}
}
const productCache = new LRUCache(1000);
async function getProduct(productId) {
const cacheKey = `product:${productId}`;
const cached = productCache.get(cacheKey);
if (cached) return { source: "cache", data: cached };
const product = await db.products.findById(productId);
productCache.set(cacheKey, product);
return { source: "db", data: product };
}