التخزين المؤقّت وشبكات التوصيل

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

18 دقيقة الدرس 3 من 10

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

معرفة أين نخزّن البيانات مؤقتًا ليست سوى نصف الحل. النصف الآخر هو معرفة كيف تتفاعل ذاكرة التخزين المؤقت (Cache) مع قاعدة البيانات عند القراءة أو الكتابة. الخطأ في هذه النقطة يُفضي إلى بيانات منتهية الصلاحية، أو ما يُعرف بـ "cache stampede"، أو ذاكرة تخزين ممتلئة بمدخلات لا يطلبها أحد. هناك أربع استراتيجيات أساسية يجب أن يتقنها كل مصمم أنظمة: Cache-Aside، وRead-Through، وWrite-Through، وWrite-Back.

الاستراتيجية الأولى: Cache-Aside (التحميل الكسول)

Cache-Aside هو النمط الأكثر شيوعًا في التطبيقات الحقيقية — ريدس (Redis) في البنية التحتية النموذجية للتطبيقات يعمل بهذه الطريقة في معظم الأحيان. كود التطبيق يتولى إدارة التفاعل مع الكاش بالكامل: يجلس الكاش جانبًا بجوار قاعدة البيانات ولا يُعبّأ إلا عند الطلب.

تدفق القراءة:

  1. يبحث التطبيق عن المفتاح في الكاش.
  2. إصابة الكاش (Cache Hit) ← يُعيد القيمة مباشرةً. انتهى.
  3. خطأ الكاش (Cache Miss) ← يستعلم التطبيق من قاعدة البيانات، يخزّن النتيجة في الكاش بمفتاح مع مدة صلاحية (TTL)، ثم يُعيد النتيجة للمستدعي.

تدفق الكتابة: يكتب التطبيق مباشرةً إلى قاعدة البيانات. إما أن يُحذف مدخل الكاش (Invalidation) أو يُترك ليتوقف بانتهاء TTL. عند الطلب التالي ستُعاد تعبئته من قيمة قاعدة البيانات الحديثة.

Cache-Aside (Lazy Loading) flow Application Cache (e.g. Redis) Database 1. check HIT → return 2. MISS → query DB 3. result 4. populate Legend Request / check Cache hit Populate / return
Cache-Aside: يُدير التطبيق تعبئة الكاش يدويًا عند فشل البحث عن المفتاح.

مثال عملي: صفحة منتج في متجر إلكتروني. أول زائر بعد النشر يُثير cache miss ويدفع تكلفة استعلام قاعدة البيانات (~5 مللي ثانية). جميع الزوار اللاحقين خلال فترة TTL يُصيبون الكاش ويدفعون ~0.3 مللي ثانية. بوجود 50,000 صفحة منتج و TTL مدته خمس دقائق، فقط القليل منها سيكون "ساخنًا" فعلًا — Cache-Aside يُعبّئ تلقائيًا ما يُطلب فقط، مما يُحافظ على الذاكرة بكفاءة.

أفضل الممارسات: ضع دومًا مدة صلاحية TTL حتى في Cache-Aside. الاعتماد الكلي على الحذف الصريح (Invalidation) هش — مسار حذف مفقود يترك بيانات منتهية الصلاحية إلى الأبد. TTL قصير هو شبكة أمان.

المقايضات:

  • إيجابية: الكاش يحمل فقط البيانات المطلوبة فعلًا — لا إهدار في الذاكرة على بيانات باردة.
  • إيجابية: يتحمّل الكاش إعادة التشغيل الكاملة؛ التطبيق يُعيد التعبئة عند فشل البحث.
  • سلبية: أول طلب بعد فشل البحث (أو بعد إعادة تشغيل الكاش) يكون دومًا بطيئًا — مشكلة البداية الباردة.
  • سلبية: في حالات الضغط المفاجئ، قد يُخفق طلبات كثيرة متزامنة في نفس الوقت وتضرب قاعدة البيانات معًا — قطيع الرعد (Thundering Herd).

الاستراتيجية الثانية: Read-Through

تنقل Read-Through منطق فشل البحث من كود التطبيق إلى طبقة الكاش ذاتها (أو مكتبة تخزين مؤقت تُغلّف قاعدة البيانات). من منظور التطبيق، هناك مكان واحد فقط للاستعلام — الكاش. إن لم يكن المفتاح موجودًا، يستعلم الكاش من قاعدة البيانات ويخزّن النتيجة ويُعيدها بشكل شفاف.

هذا هو السلوك الافتراضي في أُطر التخزين المؤقت كـ Hibernate Second-Level Cache وVarnish.

المقايضات:

  • إيجابية: كود التطبيق أبسط — لا تفريع على فشل الكاش في كل دالة.
  • سلبية: أول طلب لأي مفتاح لا يزال بطيئًا (نفس مشكلة البداية الباردة).
  • سلبية: يتطلب كاشًا يدعم callback للتحميل، وهو ما لا توفره جميع البنى التحتية.
Cache-Aside مقابل Read-Through: تدفق البيانات عند فشل البحث متطابق — استعلام من قاعدة البيانات، تخزين في الكاش، إعادة النتيجة. الفرق هو من ينفّذ ذلك المنطق: التطبيق (Cache-Aside) أم طبقة الكاش ذاتها (Read-Through).

الاستراتيجية الثالثة: Write-Through

تعني Write-Through أن كل عملية كتابة تذهب إلى الكاش وقاعدة البيانات معًا بشكل متزامن في نفس الطلب قبل الإقرار للمستدعي. الكاش دائمًا متزامن مع قاعدة البيانات لأي مفتاح يحمله.

تدفق الكتابة:

  1. يكتب التطبيق إلى الكاش.
  2. يكتب الكاش (أو التطبيق) نفس البيانات فورًا إلى قاعدة البيانات.
  3. تنجح كلتا الكتابتين ← يُرسل إقرار النجاح للمستدعي.
Write-through vs Write-back comparison Write-Through App Cache Database write sync ACK both Write latency = DB latency Write-Back (Write-Behind) App Cache Database write ACK fast async Write latency = cache only
Write-Through (يسار): الكاش وقاعدة البيانات دائمًا متزامنان بتكلفة زمن كتابة أعلى. Write-Back (يمين): الإقرار فوري على الكاش والمزامنة مع قاعدة البيانات تتم بشكل غير متزامن.

مثال عملي: خدمة ملف المستخدم. حين يُحدّث المستخدم اسمه، تريد أن يعود أي قراءة لاحقة — سواء من الكاش أو مباشرةً من قاعدة البيانات — بالاسم الجديد. Write-Through يضمن ذلك.

المقايضات:

  • إيجابية: الكاش لا يحوي بيانات منتهية الصلاحية أبدًا للمفاتيح التي يحملها — القراءات دائمًا متسقة.
  • سلبية: كل كتابة بطيئة بقدر بطء قاعدة البيانات. إن كانت زمن كتابة قاعدة البيانات 10 مللي ثانية، فزمن كتابة API لا يقل عن 10 مللي ثانية.
  • سلبية: الكاش يمتلئ ببيانات نادرة القراءة كُتبت ولم تُطلب لاحقًا — الأعمال الكثيفة الكتابة وخفيفة القراءة تُهدر ذاكرة الكاش.

الاستراتيجية الرابعة: Write-Back (الكتابة المؤجّلة)

تُقايض Write-Back الأمان بالسرعة. يكتب التطبيق إلى الكاش فقط ويحصل على إقرار فوري. ثم يُفرغ الكاش البيانات "القذرة" (غير المكتوبة في قاعدة البيانات) بشكل غير متزامن — وفق جدول زمني، أو عند ضغط الذاكرة، أو عند إيقاف التشغيل بشكل أنيق.

مثال عملي: لعبة متعددة اللاعبين تتتبع النقاط في الوقت الفعلي. تتغير النقاط آلاف المرات في الثانية. الكتابة المتزامنة لكل زيادة إلى قاعدة بيانات علائقية ستُشبع قاعدة البيانات خلال ثوانٍ. بدلًا من ذلك: اكتب إلى Redis فورًا (أقل من مللي ثانية)، وادفع النقاط المجمّعة إلى MySQL كل 10 ثوانٍ.

خطر — Write-Back يفقد البيانات عند الفشل. إذا تعطّل نود الكاش قبل مزامنة المدخلات القذرة مع قاعدة البيانات، تلك الكتابات ستضيع بشكل دائم. اختر Write-Back فقط حين تتقبّل فقدان بعض البيانات (نقاط الألعاب، عدادات المشاهدات، أحداث التحليل)، أو حين يوجد سجل كتابة مسبقة (WAL) في طبقة الكاش.

المقايضات:

  • إيجابية: يُقلّل زمن الكتابة بشكل كبير — التطبيق لا يُحجب بـ I/O قاعدة البيانات إطلاقًا.
  • إيجابية: يُجمّع الكتابات ويُدمجها مما يُخفف الحمل على قاعدة البيانات — ضروري في الأعمال الكثيفة الكتابة.
  • سلبية: احتمال فقدان البيانات عند فشل الكاش — المدخلات القذرة (غير المُمررة) تختفي.
  • سلبية: قاعدة البيانات تتأخر خلف الكاش؛ أي عملية تقرأ مباشرةً من قاعدة البيانات ترى قيمًا قديمة.

اختيار الاستراتيجية الصحيحة

لا يوجد خيار صحيح بشكل مطلق — الاستراتيجية المناسبة تعتمد على نسبة القراءة/الكتابة في الحمل والتسامح مع البيانات القديمة أو فقدان البيانات:

  • قراءة كثيفة، كتابة متفرقة، تقبّل لبعض القِدَمCache-Aside مع TTL. معظم واجهات API للويب.
  • قراءة كثيفة، يجب أن تكون الكتابات متسقة فورًاWrite-Through. ملفات المستخدمين، الصلاحيات.
  • كتابة كثيفة، زمن كتابة منخفض مطلوب، تقبّل لفقدان البياناتWrite-Back. عدادات التحليل، نقاط الألعاب.
  • بنية مدفوعة بإطار عمل، طرق خدمة كثيرةRead-Through للحفاظ على نظافة كود التطبيق.

في الأنظمة الكبيرة ستُمزج الاستراتيجيات عادةً عبر أنواع بيانات مختلفة. خدمة إدارة الطلبات قد تستخدم Write-Through لحالة الطلب (تتطلب تناسقًا قويًا) بينما تستخدم Write-Back لعدادات مشاهدات صفحات المنتجات (التناسق المؤجّل مقبول، حجم الكتابة مرتفع).

الفكرة المحورية: كل استراتيجية تخزين مؤقت هي في نهاية المطاف قرار حول من المسؤول عن إبقاء الكاش متسقًا مع مصدر الحقيقة — كود التطبيق، أم طبقة الكاش، أم عملية خلفية غير متزامنة. إدراك هذا الإطار يجعل المقايضات واضحة.