يحدث تدافع ذاكرة التخزين المؤقت عندما تفشل طلبات متزامنة كثيرة في العثور على المفتاح نفسه بعد انتهاء صلاحيته، فتُعيد جميعها بناء البيانات في اللحظة ذاتها. يشرح هذا الدليل المشكلة عمليًا في Laravel وطرق منعها بالأقفال الذرية والتحديث في الخلفية وعشوائية مدة التخزين.
يحدث تدافع ذاكرة التخزين المؤقت (Cache Stampede) عندما تكتشف طلبات متزامنة كثيرة أن عنصرًا شائعًا في الذاكرة المؤقتة مفقود أو انتهت صلاحيته، فتحاول جميعها إعادة بنائه في الوقت نفسه. وبدلًا من أن تحمي الذاكرة المؤقتة قاعدة البيانات، تصبح سببًا في توجيه دفعة ضخمة من العمل المكرر إليها. تُعرف هذه المشكلة أيضًا باسم القطيع الهادر (Thundering Herd) أو Dog-pile Effect.
تخيّل تطبيق Laravel يخزّن نتيجة استعلام «المنتجات الأكثر مبيعًا» لمدة عشر دقائق. طوال وجود المفتاح، تحصل آلاف الطلبات على النتيجة بسرعة. لكن عند انتهاء صلاحيته تصل 200 طلبية في لحظة واحدة؛ ترى جميعها أن المفتاح غير موجود، فتشغّل الاستعلام نفسه وتحاول كتابة النتيجة نفسها. وهكذا يُنفذ الاستعلام الذي يفترض أن يعمل مرة واحدة 200 مرة.
قد تستنزف هذه الدفعة اتصالات قاعدة البيانات، وترفع استهلاك المعالج، وتزيد زمن الاستجابة، وتسبب انتهاء مهلة الخدمات التابعة. وكلما تباطأت الاستجابات بقيت الطلبات مفتوحة مدة أطول، فارتفع التزامن وتحول مفتاح منتهي الصلاحية إلى فشل متسلسل.
تُعد Cache::remember خيارًا افتراضيًا ممتازًا للتخزين المؤقت المعتاد في Laravel:
$products = Cache::remember('products:top', 600, function () {
return Product::query()
->where('is_active', true)
->orderByDesc('sales_count')
->limit(20)
->get();
});تتحقق الدالة من المفتاح، وتنفذ الإغلاق عند عدم العثور عليه، ثم تخزن النتيجة وتعيدها. لكن قراءة المفتاح وإعادة توليد البيانات ليستا عملية ذرية واحدة. فإذا رأت عدة عمليات PHP أن المفتاح مفقود قبل أن يُكمل العامل الأول الاستعلام، فقد تدخل كلها إلى الإغلاق. يكون الكود صحيحًا في الحمل المنخفض، لكنه يظل معرضًا للتدافع عندما تنتهي صلاحية مفتاح ساخن تحت ضغط مرتفع.
انتهاء صلاحية مفتاح ساخن: يصل عنصر كثير الطلبات إلى نهاية مدة TTL أثناء ذروة الزيارات.
استخدام المدة نفسها لمفاتيح كثيرة: يؤدي تسخين جماعي للكاش إلى انتهاء مئات المفاتيح معًا.
مسح الذاكرة المؤقتة: يحذف أمر صيانة أو نشر جميع المفاتيح الشائعة دفعة واحدة.
البداية الباردة: تستقبل خوادم التطبيق الجديدة الزيارات قبل تجهيز القيم المهمة.
بطء إعادة التوليد: يمنح الاستعلام البطيء أو استدعاء API الخارجي طلبات أكثر فرصة للتراكم.
تعطل مخزن الكاش: يؤدي انقطاع Redis أو Memcached إلى تحويل حمل أكبر من قدرة المصدر إلى قاعدة البيانات.
الفكرة الأساسية هي دمج الطلبات المتزامنة: نسمح لطلب واحد فقط بإعادة توليد القيمة، بينما تنتظر الطلبات الأخرى مدة قصيرة أو تستخدم قيمة بديلة. يوفر Laravel أقفالًا ذرية موزعة عبر Cache::lock.
use Illuminate\Support\Facades\Cache;
function topProducts()
{
$key = 'products:top';
if ($cached = Cache::get($key)) {
return $cached;
}
return Cache::lock("lock:{$key}", 15)->block(5, function () use ($key) {
// ربما ملأ عامل آخر المفتاح أثناء انتظارنا للقفل.
return Cache::get($key) ?? tap(
Product::query()
->where('is_active', true)
->orderByDesc('sales_count')
->limit(20)
->get(),
fn ($products) => Cache::put($key, $products, now()->addMinutes(10))
);
});
}الفحص الثاني للكاش داخل القفل ضروري. فمن دونه يحصل كل طلب منتظر على القفل بالتتابع ويعيد حساب القيمة، رغم أن الطلب الأول حدّثها بالفعل. يُسمى هذا النمط غالبًا القفل مزدوج التحقق.
اختر مدة للقفل أطول من الزمن المتوقع لإعادة التوليد، ولكن لا تجعلها طويلة جدًا كي لا يمنع عامل متعطل عمليات التحديث. واضبط حدًا قصيرًا للانتظار باستخدام block. إذا انتهت مدة الانتظار يرمي Laravel الاستثناء LockTimeoutException، وعندها ينبغي إرجاع بيانات قديمة أو استجابة مبسطة أو خطأ مضبوط.
عندما يكون التأخر البسيط في البيانات مقبولًا، يكون نمط Stale-While-Revalidate من أفضل الحلول لتجربة المستخدم. تتيح Cache::flexible في Laravel تحديد نافذة للبيانات الجديدة وأخرى للبيانات القديمة:
$products = Cache::flexible(
'products:top',
[300, 900],
fn () => Product::query()
->where('is_active', true)
->orderByDesc('sales_count')
->limit(20)
->get()
);خلال أول 300 ثانية يعيد Laravel القيمة الجديدة مباشرة. وبين 300 و900 ثانية يقدم القيمة القديمة فورًا ويسجل عملية مؤجلة لتحديثها بعد إرسال الاستجابة. بعد 900 ثانية تصبح القيمة منتهية تمامًا ويعاد حسابها بشكل متزامن.
يحافظ هذا الأسلوب على زمن استجابة متوقع، ويقلل عدد المستخدمين الذين ينتظرون العملية المكلفة. وهو مناسب لقوائم المقالات، وترتيب المنتجات، ولوحات المعلومات، والتوصيات، وكل بيانات يمكن أن تتأخر بضع دقائق دون ضرر.
إذا أُنشئت مفاتيح كثيرة في الوقت نفسه وبالمدة نفسها فقد تنتهي صلاحيتها معًا. أضف فرقًا عشوائيًا صغيرًا، يسمى TTL Jitter، لتوزيع التحديثات على الزمن:
$ttl = now()->addSeconds(600 + random_int(0, 120));
Cache::put("product:{$product->id}", $payload, $ttl);لا تمنع العشوائية وحدها عدة طلبات من التسابق على مفتاح ساخن واحد، لذلك فهي تكمل الأقفال أو التحديث في الخلفية ولا تستبدلهما. وظيفتها منع موجة انتهاء متزامنة لمجموعة كبيرة من المفاتيح.
تجمع الأنظمة ذات الحمل العالي غالبًا بين مفتاح عادي، ومفتاح قديم أطول عمرًا، وقفل ذري. يعيد عامل واحد حساب البيانات، بينما تنتظر بقية العمليات قليلًا أو تعيد النسخة القديمة.
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Cache;
function dashboardStats(): array
{
$key = 'dashboard:stats';
$staleKey = 'dashboard:stats:stale';
if ($value = Cache::get($key)) {
return $value;
}
try {
return Cache::lock("lock:{$key}", 20)->block(2, function () use ($key, $staleKey) {
if ($value = Cache::get($key)) {
return $value;
}
$value = app(StatsService::class)->calculate();
$freshTtl = now()->addSeconds(300 + random_int(0, 60));
Cache::put($key, $value, $freshTtl);
Cache::put($staleKey, $value, now()->addHours(2));
return $value;
});
} catch (LockTimeoutException) {
return Cache::get($staleKey, [
'status' => 'temporarily_unavailable',
]);
}
}يحمي هذا التصميم قاعدة البيانات ويوفر مسار تدهور واضحًا بدل الانهيار الكامل. اضبط المدد وفق تكلفة الاستعلام، ودرجة حداثة البيانات المقبولة، وحجم الزيارات، وطبيعة الأعطال المتوقعة في مخزن الكاش.
يجب أن تستخدم جميع خوادم التطبيق مخزنًا مشتركًا يدعم الأقفال الذرية. يدعم Laravel عدة برامج تشغيل، منها Redis وMemcached وDynamoDB وقاعدة البيانات والملفات، لكن الخوادم المتعددة يجب أن تتصل بالمخزن المركزي نفسه. ويُعد Redis خيارًا شائعًا في بيئات الإنتاج.
استخدم اسم قفل فريدًا لكل مورد. أضف رقم المستأجر أو اللغة أو عوامل التصفية أو معرّف النموذج عندما تغيّر النتيجة.
اجعل مدة القفل محدودة. قد يتعطل العامل، ولذلك يجب أن ينتهي القفل تلقائيًا.
فكر قبل تخزين القفل في قاعدة البيانات المرهقة نفسها. فقد يزيد ذلك نقطة الاختناق سوءًا.
لا تحرر قفل عملية أخرى. استخدم صيغة الإغلاق في Laravel ليُحرر القفل تلقائيًا متى أمكن.
تعامل مع النتائج الفارغة بوضوح. قد يعد فحص القيمة المنطقية مجموعة فارغة كأنها فقدان للكاش؛ استخدم Cache::has أو قيمة دالة.
تجنب القفل العام الواحد. فهو يجعل الأعمال غير المرتبطة تعمل بالتتابع ويرفع زمن الاستجابة بلا داعٍ.
ابحث عن ارتفاعات دورية تتزامن مع حدود TTL. تشمل المؤشرات المفيدة هبوطًا مفاجئًا في نسبة إصابة الكاش، ودفعة من استعلامات SQL المتطابقة، وارتفاع اتصالات قاعدة البيانات، والتنافس على الأقفال، وزيادة زمن الاستجابة p95 أو p99، وحدوث مهلات بعد مسح الكاش أو نشر إصدار جديد.
راقب الإغلاق المكلف نفسه، وليس استدعاء الكاش فقط. سجل عائلة المفتاح، ومدة إعادة التوليد، ووقت الحصول على القفل، وعدد مهلات القفل، وما إذا أُرسلت بيانات قديمة. تساعد أدوات تتبع الاستعلامات والمقاييس والمراقبة في Redis على إظهار النمط. ويمكن لاختبار حمل يرسل طلبات متزامنة مباشرة بعد حذف مفتاح ساخن أن يثبت نجاح الحل.
استخدم Cache::remember للقيم الرخيصة أو المفاتيح منخفضة التزامن.
استخدم Cache::lock عندما يجب تنفيذ إعادة التوليد مرة واحدة ويمكن للطلبات الانتظار قليلًا.
استخدم Cache::flexible عندما تكون البيانات المتأخرة قليلًا مقبولة وتهمك سرعة الاستجابة.
أضف TTL Jitter عندما تُكتب مفاتيح مترابطة كثيرة معًا.
استخدم التسخين الاستباقي لعدد صغير من المفاتيح الساخنة المهمة والمتوقعة.
اجمع بين القفل والقيمة القديمة والعشوائية والمراقبة في المسارات الأعلى حركة.
لا. فقدان الكاش حدث طبيعي: لا يجد طلب واحد القيمة فيعيد حسابها. أما التدافع فيحدث عندما تفشل طلبات متزامنة كثيرة في العثور على القيمة نفسها وتكرر العمل المكلف معًا.
لا. يجعل Redis الوصول إلى الكاش سريعًا ويوفر أدوات للأقفال الذرية، لكن كود التطبيق ما زال يحتاج إلى استراتيجية لإعادة التوليد. توفر واجهات Laravel الخاصة بالأقفال والكاش المرن اللبنات المطلوبة.
لا. تضيف الأقفال تنسيقًا وقد تزيد زمن الاستجابة. احمِ المفاتيح الساخنة التي يمكن أن يهدد حسابها المكلف النظام الخلفي عند ارتفاع التزامن.
إذا كانت البيانات القديمة مقبولة، فابدأ بـ Cache::flexible. وإذا كان يجب تنفيذ إعادة التوليد مرة واحدة، فاحمِ مسار فقدان الكاش بـ Cache::lock ثم افحص المفتاح مرة ثانية بعد الحصول على القفل.
تدافع الكاش مشكلة تزامن متنكرة في هيئة مشكلة تخزين مؤقت. ولا يكفي علاجها بإطالة مدة TTL فقط. نسّق إعادة التوليد، وقدم بيانات قديمة عندما يكون ذلك مقبولًا، ووزع أوقات الانتهاء بالعشوائية، وراقب ما يحدث عند اختفاء مفتاح ساخن. يمنحك Laravel الأدوات الأساسية—Cache::lock وCache::flexible ومخازن الكاش المرنة—لمنع مفتاح واحد منتهي الصلاحية من التحول إلى حادث إنتاجي.
لا توجد تعليقات معتمدة بعد. قد تنتظر الردود الجديدة المراجعة.