Redis والتخزين المؤقت المتقدم
استراتيجيات إبطال التخزين المؤقت
استراتيجيات إبطال التخزين المؤقت
إبطال التخزين المؤقت هو أحد أصعب المشاكل في علوم الحاسوب. يغطي هذا الدرس الاستراتيجيات المثبتة للحفاظ على التخزين المؤقت محدثاً ومتسقاً مع مصادر البيانات.
الانتهاء المبني على TTL
أبسط استراتيجية للإبطال هي تعيين وقت البقاء (TTL) على العناصر المخزنة مؤقتاً:
مثال PHP:
// تعيين التخزين المؤقت بمدة ساعة واحدة
Cache::put('user:' . $userId, $userData, 3600);
// أو استخدام Redis مباشرة
Redis::setex('product:' . $productId, 3600, json_encode($product));
// التحقق من الوقت المتبقي
$ttl = Redis::ttl('user:' . $userId); // يعيد الثواني المتبقيةأفضل ممارسة: اختر TTL بناءً على تقلب البيانات - البيانات المتغيرة بشكل متكرر تحتاج TTL أقصر، البيانات الثابتة يمكن أن تحتوي على TTL أطول (ساعات أو أيام).
الإبطال المبني على الأحداث
إبطال التخزين المؤقت عند تغيير البيانات الأساسية:
مثال Laravel:
// في النموذج أو المتحكم الخاص بك
public function updateUser($userId, $data)
{
// تحديث قاعدة البيانات
$user = User::findOrFail($userId);
$user->update($data);
// إبطال التخزين المؤقت
Cache::forget('user:' . $userId);
Cache::tags(['users'])->flush(); // إذا كنت تستخدم وسوم التخزين المؤقت
return $user;
}الإبطال المبني على الوسوم
تجميع مفاتيح التخزين المؤقت ذات الصلة تحت وسوم للإبطال الجماعي:
وسوم التخزين المؤقت في Laravel:
// التخزين مع الوسوم
Cache::tags(['users', 'user:' . $userId])->put('profile', $data, 3600);
Cache::tags(['users', 'admins'])->put('admin:list', $admins, 7200);
// إبطال جميع التخزينات المؤقتة المتعلقة بالمستخدمين
Cache::tags(['users'])->flush();
// إبطال مستخدم محدد
Cache::tags(['user:' . $userId])->flush();مهم: وسوم التخزين المؤقت غير مدعومة من قبل برامج تشغيل التخزين المؤقت للملفات أو قاعدة البيانات في Laravel. استخدم Redis أو Memcached لدعم الوسوم.
إزالة التخزين المؤقت المبنية على الإصدار
إلحاق رقم إصدار بمفاتيح التخزين المؤقت بدلاً من حذف البيانات القديمة:
مفاتيح التخزين المؤقت بالإصدار:
// تخزين الإصدار الحالي في Redis
Redis::set('cache_version:users', 1);
// إنشاء مفتاح التخزين المؤقت مع الإصدار
$version = Redis::get('cache_version:users');
$cacheKey = "users:list:v{$version}";
$users = Cache::get($cacheKey);
// للإبطال، زيادة الإصدار
Redis::incr('cache_version:users'); // الآن الإصدار هو 2
// التخزين المؤقت القديم في v1 يصبح قديماً تلقائياًميزة: الإبطال المبني على الإصدار يسمح لإدخالات التخزين المؤقت القديمة بالانتهاء بشكل طبيعي عبر TTL، مما يقلل من مخاطر الاندفاع نحو التخزين المؤقت.
منع اندفاع التخزين المؤقت
عندما ينتهي التخزين المؤقت على المفاتيح ذات الحركة المرتفعة، قد تحاول طلبات متعددة في وقت واحد إعادة بناء التخزين المؤقت (اندفاع). الحلول:
الحل 1: نهج مبني على القفل
public function getUserData($userId)
{
$cacheKey = 'user:' . $userId;
$lockKey = $cacheKey . ':lock';
// محاولة الحصول من التخزين المؤقت
$data = Cache::get($cacheKey);
if ($data) return $data;
// الحصول على القفل (مهلة 5 ثوانٍ)
$lock = Cache::lock($lockKey, 5);
try {
if ($lock->get()) {
// التحقق المزدوج من التخزين المؤقت (قد تكون عملية أخرى قد ملأته)
$data = Cache::get($cacheKey);
if ($data) return $data;
// إعادة بناء التخزين المؤقت
$data = User::find($userId);
Cache::put($cacheKey, $data, 3600);
return $data;
} else {
// لم يتمكن من الحصول على القفل، انتظر قليلاً وأعد المحاولة
usleep(100000); // 100 ملي ثانية
return Cache::get($cacheKey) ?? User::find($userId);
}
} finally {
$lock->release();
}
}الحل 2: الانتهاء المبكر الاحتمالي
public function getWithProbabilisticRefresh($key, $ttl, $callback)
{
$data = Redis::get($key);
if ($data) {
// الحصول على TTL المتبقي
$remaining = Redis::ttl($key);
// حساب احتمالية التحديث المبكر
// عندما يقترب TTL من 0، يزداد الاحتمال
$probability = 1 - ($remaining / $ttl);
if (mt_rand() / mt_getrandmax() < $probability) {
// تحديث التخزين المؤقت احتمالياً في الخلفية
dispatch(new RefreshCacheJob($key, $callback));
}
return json_decode($data, true);
}
// فقدان التخزين المؤقت - إعادة البناء
$data = $callback();
Redis::setex($key, $ttl, json_encode($data));
return $data;
}نمط القديم أثناء إعادة التحقق
تقديم التخزين المؤقت القديم أثناء التحديث في الخلفية:
التنفيذ:
public function getWithStaleWhileRevalidate($key, $freshTtl, $staleTtl, $callback)
{
$data = Redis::get($key);
$freshUntil = Redis::get($key . ':fresh_until');
if ($data) {
$now = time();
if ($freshUntil && $now < $freshUntil) {
// البيانات لا تزال طازجة
return json_decode($data, true);
}
// البيانات قديمة ولكن قابلة للاستخدام - تشغيل التحديث في الخلفية
if (Redis::set($key . ':refreshing', 1, 'EX', 60, 'NX')) {
// حصلنا على قفل التحديث
dispatch(new RefreshCacheJob($key, $freshTtl, $staleTtl, $callback));
}
// إرجاع البيانات القديمة فوراً
return json_decode($data, true);
}
// فقدان التخزين المؤقت - إعادة البناء المتزامن
$data = $callback();
Redis::setex($key, $staleTtl, json_encode($data));
Redis::setex($key . ':fresh_until', $staleTtl, time() + $freshTtl);
return $data;
}فوائد النمط: يحصل المستخدمون دائماً على استجابات سريعة (البيانات القديمة مقبولة للعديد من حالات الاستخدام)، يتم تحديث التخزين المؤقت بشكل استباقي، لا يوجد خطر الاندفاع.
شجرة قرار إبطال التخزين المؤقت
متى تستخدم كل استراتيجية:
1. الانتهاء المبني على TTL ✓ بيانات بسيطة وقابلة للتنبؤ ✓ متطلبات اتساق منخفضة ✓ مثال: لوحات تحليلات، التغذيات 2. الإبطال المبني على الأحداث ✓ متطلبات اتساق عالية ✓ أحداث تغيير بيانات واضحة ✓ مثال: ملفات المستخدمين، تفاصيل المنتجات 3. الإبطال المبني على الوسوم ✓ مجموعات بيانات ذات صلة ✓ احتياجات إبطال جماعي ✓ مثال: منتجات الفئات، أذونات المستخدمين 4. الإزالة المبنية على الإصدار ✓ منطق إبطال معقد ✓ احتياجات طرح تدريجي ✓ مثال: التكوينات، علامات الميزات 5. القديم أثناء إعادة التحقق ✓ توليد بيانات مكلف ✓ مفاتيح حركة مرتفعة ✓ بيانات قديمة مقبولة ✓ مثال: بيانات الصفحة الرئيسية، العناصر الرائجة
تمرين عملي:
- تنفيذ تخزين مؤقت للمنتجات مع إبطال مبني على الوسوم (الوسوم: products، category:{id}، brand:{id})
- إضافة منع اندفاع التخزين المؤقت باستخدام الأقفال
- إنشاء طريقة لإبطال جميع المنتجات في فئة عند تحديث الفئة
- تنفيذ القديم أثناء إعادة التحقق لتغذية الصفحة الرئيسية مع TTL طازج 5 دقائق وTTL قديم ساعة واحدة
- إضافة مراقبة لتتبع معدلات نجاح التخزين المؤقت وحدوث الاندفاع