المستوى: متوسط — مناسب لمن يعرف أساسيات الكاش (Redis / Memcached) ويشتغل على خدمة تحت ضغط حقيقي.
Cache Stampede: ليه انتهاء صلاحية مفتاح واحد بيوقّع قاعدة بياناتك
لو خدمتك بتمشي حلو، وفجأة الـ CPU بتاع قاعدة البيانات بيقفز لـ 100% لمدة ثواني كل شوية، من غير زيادة في عدد الزوّار — ركّز في التوقيت. غالبًا اللحظة دي بتتزامن مع انتهاء صلاحية مفتاح كاش مهم. المقال ده هيوريك ليه بيحصل، وهيديك حل قابل للنسخ يمنعه في أقل من ربع ساعة.
المشكلة باختصار
الكاش بيخبّي نتيجة عملية غالية (query تقيل أو حساب معقّد) لمدة محددة. طول ما المفتاح موجود، كل الطلبات بتترد من الذاكرة في أقل من مللي ثانية. لكن في اللحظة اللي بتنتهي فيها صلاحية المفتاح، كل الطلبات اللي بتوصل في نفس اللحظة بتلاقي الكاش فاضي، فبتروح كلها لقاعدة البيانات في نفس الوقت عشان تعيد الحساب. ده اسمه Cache Stampede (أو thundering herd أو dogpile).
الفكرة بمثال بسيط الأول
تخيّل مطعم فيه طبق مشهور. الشيف بيحضّر منه صينية كبيرة كل ساعة، وبيحطها على البار. أي زبون بيطلب الطبق، الجرسون بياخد منه على طول — ثانية واحدة. لكن في اللحظة اللي بتخلص فيها الصينية، لو 200 زبون طالبين في نفس الدقيقة، الجرسونات الـ 200 كلهم هيجروا على المطبخ في نفس الوقت يقولوا للشيف "اعمل الطبق". الشيف الواحد مش هيقدر، فالمطبخ بيقف، وكل الطلبات بتتأخّر — حتى الطلبات التانية اللي مالهاش علاقة.
"الصينية" هي الكاش. "خلاص الصينية" هو انتهاء الصلاحية. "الجرسونات الـ 200 اللي جِروا مع بعض" هي طلباتك المتزامنة. "الشيف الواحد" هو قاعدة البيانات.
ليه بيحصل فعلاً؟ التفسير الدقيق
الكود الشائع للكاش بيكون بالشكل ده: اقرا من الكاش، لو مش موجود احسب من قاعدة البيانات ثم خزّن. المشكلة إن الخطوة "لو مش موجود" مفيهاش تنسيق بين الطلبات. مفيش طلب بيقول للطلبات التانية "استنّوا، أنا بحسبها دلوقتي".
خلّينا نحط أرقام. لو عندك خدمة بتستقبل 10,000 طلب/ثانية على نفس المفتاح، والـ query اللي بتعيد الحساب بتاخد 200 مللي ثانية. في الـ 200 مللي ثانية اللي بعد انتهاء الصلاحية مباشرة، بيوصل حوالي 2,000 طلب — وكلهم بيلاقوا الكاش فاضي فبيضربوا قاعدة البيانات مع بعض. لو قاعدة بياناتك بتتحمّل 500 query/ثانية مريحة، فجأة اتحطّ عليها 2,000 في لحظة. النتيجة: طابور، بطء، ووقت استجابة بيتصاعد لكل المستخدمين، مش بس اللي طلبوا المفتاح ده. أسوأ سيناريو إن الحمل بيمنع الحساب من الانتهاء، فالكاش يفضل فاضي، والدنيا بتلف في دايرة انهيار.
الحل الأساسي: قفل الرحلة الواحدة (single-flight lock)
الفكرة بسيطة: أول طلب بيلاقي الكاش فاضي بياخد قفل. هو بس اللي بيروح لقاعدة البيانات. باقي الطلبات بتلاقي القفل مقفول، فبتستنّى شوية ثم تقرا النتيجة الجاهزة من الكاش. بدل 2,000 ضربة على قاعدة البيانات، بتبقى ضربة واحدة.
ده مثال شغّال بـ Python و Redis. القفل بيتعمل بـ SET key value NX EX — يعني "اكتب المفتاح فقط لو مش موجود، وحطله مهلة":