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

إبطال الكاش (Cache Invalidation)

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

إبطال الكاش (Cache Invalidation)

المقولة الشهيرة لفيل كارلتون — "لا يوجد في علوم الحاسوب سوى مشكلتين صعبتين حقًّا: إبطال الكاش، وتسمية الأشياء" — طريفةٌ لأنها دقيقة في آنٍ واحد. تقديم بيانات قديمة قد يكون كارثيًّا كتقديم لا شيء على الإطلاق. صفحة منتج تعرض سعرًا نفد مخزونه بالأمس، فحص صلاحيات يُعيد "مسموح" لمستخدم تمّ حظره، سجل DNS لا يزال يشير إلى خادم أُوقف عن العمل — كلّها إخفاقات إبطال كاش واقعية.

إبطال الكاش هو الانضباط المتمثّل في تحديد متى وكيف تُزال إدخالات الكاش أو تُستبدل حتى لا يرى العملاء بيانات أقدم مما يتحمّله النظام. المسألة صعبة فعلًا لأنها تتطلّب تنسيق مخزنَي بيانات أو أكثر — الكاش ومصدر الحقيقة — في ظلّ الكتابات المتزامنة والإخفاقات الجزئية وانقطاع الشبكة.

السبب الجذري: مشكلة الحالة المزدوجة

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

فكرة أساسية: إبطال الكاش ليس مجرّد مشكلة تقنية، بل هو قرار منتج. رصيد بنكي يتطلّب حداثة شبه فورية، بينما عداد مشاهدات مقالة يتحمّل دقائق من التأخّر. حدّد ميزانية العتاقة قبل اختيار أيّ إستراتيجية.
Cache Invalidation — the dual-state problem Writer / API updates DB write Origin DB price = $29 (source of truth) NOT synced Cache price = $49 (stale!) Reader sees $49 Stale window: DB was updated but cache was not
بعد الكتابة في قاعدة البيانات الأصلية، لا يزال الكاش يحتفظ بالقيمة القديمة — يرى القرّاء بيانات عتيقة حتى تُبطَل.

الإستراتيجية الأولى: انتهاء الصلاحية بالـ TTL (الإبطال السلبي)

أبسط نهج هو أن تحمل كلّ إدخالة في الكاش وقت حياة (TTL). عند انتهاء الـ TTL، تُفضي القراءة التالية إلى cache miss ويُجلب الإصدار الجديد. لا يلزم أيّ تنسيق صريح بين الكاتب والكاش.

  • المزايا: لا ترابط بين الكاتب والكاش، تنفيذ بالغ البساطة، مدعوم في Redis وMemcached وHTTP Headers (Cache-Control: max-age) وكلّ CDN.
  • العيوب: العتاقة مقيّدة بالـ TTL لا بالكتابة الفعلية. TTL مدّته 60 ثانية تعني أن كلّ قيمة مخزّنة يمكن أن تكون خاطئة بمقدار 60 ثانية. اختيار القيمة المناسبة يتطلّب معرفة تكرار الكتابة وحدّ التسامح مع العتاقة.

قاعدة عملية: اضبط الـ TTL على نحو نصف الفاصل الزمني المتوقّع للتغيير. فهرس منتجات يُحدَّث مرتين في اليوم يتحمّل TTL من 3 إلى 6 ساعات. أما شاشة عرض الأسعار في البورصة فلا ينبغي أن تتجاوز 1-2 ثانية.

نصيحة — التشويش في الـ TTL: إذا أُنشئت آلاف الإدخالات في آنٍ واحد (كاستيراد مجمّع)، فستنتهي صلاحيتها جميعًا في نفس الوقت مُسبّبةً قطيع الرعد (thundering herd). أضف قيمة عشوائية للـ TTL (base_ttl + rand(0, jitter)) لتوزيع انتهاءات الصلاحية بمرور الوقت.

الإستراتيجية الثانية: الإبطال عند الكتابة (النشط / الفوري)

يتحمّل الكاتب هنا مسؤولية إبقاء الكاش متسقًا. في كلّ كتابة إلى المصدر، يُنفّذ إمّا حذف إدخالة الكاش (إبطال) أو تحديثها في مكانها (write-through).

  • الحذف عند الكتابة (invalidate): أبسط وأكثر أمانًا. يستدعي الكاتب cache.del(key) بعد الحفظ في قاعدة البيانات. القارئ التالي يدفع ثمن cache miss لكنّه يحصل دائمًا على بيانات حديثة.
  • التحديث عند الكتابة (write-through): يحفظ الكاتب في قاعدة البيانات ويُحدّث الكاش بالقيمة الجديدة دفعةً واحدة. يُلغي الـ miss، لكنّه يُضيف تعقيدًا: ماذا لو نجحت الكتابة في قاعدة البيانات وفشلت في الكاش؟
حالة تسابق — الحذف عند الكتابة: يقرأ الخيط A قيمةً قديمة وعلى وشك إعادة كتابتها في الكاش. يكتب الخيط B القيمة الحقيقية الجديدة ويحذف مفتاح الكاش. ثم يُكمل الخيط A كتابةَ القيمة القديمة. الحلّ هو استخدام فحص الإصدار أو نمط الحذف المزدوج: احذف عند الكتابة، ثم احذف ثانيةً بعد ~500 مللي ثانية لمعالجة القراءات المتداخلة.

الإستراتيجية الثالثة: الإبطال القائم على الأحداث (CDC / Pub-Sub)

بدلًا من ربط كلّ كاتب بمنطق مسح الكاش، يمكن استخدام خط أنابيب التقاط تغيير البيانات (CDC) أو ناقل رسائل. عند تثبيت قاعدة البيانات لتغيير في صف ما، يُبثّ حدث (عبر Kafka أو Redis Pub/Sub أو أداة CDC كـ Debezium) وتُبطل كلّ عُقد الكاش المشتركة في ذلك النمط إدخالاتها.

هكذا تُحافظ المنصّات الكبرى على اتساق الكاش عبر الخدمات المصغّرة دون أن تعلم كلّ خدمة بكلّ الكاشات. تستخدم Netflix وAirbnb وShopify هذا النهج على نطاق واسع.

Event-Driven Cache Invalidation via CDC API Service Primary DB (MySQL / Postgres) write CDC Agent (Debezium) binlog Message Bus (Kafka / Redis) publish Cache A (Redis node) Cache B (Redis node) invalidate invalidate
يقرأ وكيل CDC سجلّ الـ binlog في قاعدة البيانات، يُنشر أحداث التغيير في ناقل رسائل، وتشترك كلّ عُقد الكاش للإبطال الفوري.

الإستراتيجية الرابعة: وسوم الكاش (الإبطال القائم على التبعية)

تدعم كثير من أُطر العمل (Laravel، Symfony، Varnish) وسوم الكاش (cache tags): تُرفق بطاقات تجميعية منطقية بالإدخالات عند الكتابة، وتُبطل جميع الإدخالات المشاركة في وسم واحد باستدعاء واحد.

// Tag a cache entry with multiple logical groups cache()->tags(['product:42', 'category:electronics'])->put('product_42_detail', $data, 3600); // When product 42 changes — clear everything tagged to it cache()->tags(['product:42'])->flush(); // When the whole electronics category is restructured cache()->tags(['category:electronics'])->flush();

هذا قويّ جدًّا للرسوم البيانية للكائنات: صفحة مُصيَّرة في الكاش قد تعتمد على منتج وتصنيفه وملف تعريف مؤلّفه واللافتة الترويجية الحالية. وسوم الصفحة بكلّ هذه العناصر يعني أن أيّ تغيير في أيٍّ منها يُبطل الصفحة تلقائيًّا.

ليست كلّ مخازن الكاش تدعم الوسوم: يدعمها Redis وMemcached، لكنّ الكاشات البسيطة داخل العمليات أو HTTP الأساسي لا يدعمانها. التطبيق يحتفظ بخريطة وسم-إلى-مفتاح بجانب الكاش؛ مسح الوسم يزيل جميع المفاتيح في تلك المجموعة.

الحالة الأصعب: الإبطال في الكاش الموزَّع

عند تشغيل خوادم تطبيقات متعددة، قد يمتلك كلّ منها كاشًا محليًّا داخل العملية (L1). كتابة على الخادم 1 تُبطل كاشه المحلي لكنّها تترك الخوادم 2 و3 و4 ببيانات قديمة. الحلول تشمل:

  • الاكتفاء بـ L2 مشترك (Redis): جميع الخوادم تشترك في كاش واحد. أبسط، لكنّه يضيف زمن رحلة شبكة لكلّ cache hit.
  • بثّ إبطال L1: عند الكتابة، انشر رسالة إبطال عبر قناة Pub/Sub تشترك فيها جميع الخوادم. هذا ما يصفه ورق بحث Memcache الشهير لـ Facebook على نطاقهم.
  • TTL قصير على L1: قبول أن الكاشات المحلية قد تكون قديمة بحدّ أقصى N ثانية. بسيط وكافٍ غالبًا للبيانات القابلة للتسامح مع التناسق النهائي.

اختيار الإستراتيجية

في الممارسة، تُطبّق معظم الأنظمة إستراتيجيات متعددة: TTL كشبكة أمان (لا تبقى البيانات قديمة للأبد)، مع إبطال صريح عند الكتابة (البيانات حديثة فورًا بعد التغيير). الإبطال القائم على الأحداث ووسوم الكاش يُضيفان دقّةً لرسوم بيانية الكائنات المعقّدة. الاختيار الصحيح يعتمد على متطلّبات التناسق وتكرار الكتابة وميزانية التعقيد التشغيلي.

ملخّص المقايضات:
  • TTL فقط — بسيط، لكنّ العتاقة مقيّدة بالوقت بصرف النظر عن الكتابات الفعلية.
  • الحذف عند الكتابة — حديث بعد الكتابات، لكنّه يُضيف ترابطًا وحالات تسابق تحت التزامن العالي.
  • write-through — لا cache miss بعد الكتابة، لكن مع مخاطر الكتابة المزدوجة.
  • CDC / الأحداث — منفصل وقابل للتوسع، لكنّه معقّد تشغيليًّا.
  • وسوم الكاش — فعّال لرسوم بيانية الكائنات، لكنّه يتطلّب انضباطًا في الوسوم ومخزنًا داعمًا.