تطوير واجهات REST API

استراتيجيات التخزين المؤقت للواجهات البرمجية

20 دقيقة الدرس 18 من 35

استراتيجيات التخزين المؤقت للواجهات البرمجية

يعد التخزين المؤقت من أكثر الاستراتيجيات فعالية لتحسين أداء الواجهات البرمجية، وتقليل حمل الخادم، وتوفير أوقات استجابة أسرع للعملاء. في هذا الدرس، سنستكشف استراتيجيات التخزين المؤقت الشاملة بما في ذلك رؤوس التخزين المؤقت HTTP، وETags، وتوجيهات Cache-Control، وآليات التخزين المؤقت في Laravel، والطلبات الشرطية.

لماذا يهم التخزين المؤقت للواجهات البرمجية

بدون التخزين المؤقت المناسب، يؤدي كل طلب API إلى استعلامات قاعدة البيانات، ومعالجة منطق الأعمال، وتنسيق الاستجابة. يصبح هذا مشكلة مع توسع الواجهة البرمجية:

  • الأداء: الطلبات المتكررة للبيانات غير المتغيرة تهدر موارد الخادم
  • النطاق الترددي: نقل استجابات متطابقة يستهلك نطاق ترددي غير ضروري
  • تجربة المستخدم: الاستجابات البطيئة تحبط المستخدمين وتضر بالمشاركة
  • التكلفة: أحمال الخادم الأعلى تتطلب المزيد من الاستثمار في البنية التحتية
المعيار الصناعي: تعتمد الواجهات البرمجية الرئيسية مثل Twitter و GitHub و Stripe بشكل كبير على التخزين المؤقت لخدمة ملايين الطلبات بكفاءة. على سبيل المثال، تستخدم واجهة GitHub API التخزين المؤقت القوي لتقليل حمل قاعدة البيانات بأكثر من 80٪.

نظرة عامة على رؤوس التخزين المؤقت HTTP

توفر HTTP آليات تخزين مؤقت مدمجة من خلال رؤوس موحدة تتحكم في كيفية تخزين الاستجابات مؤقتًا بواسطة المتصفحات وCDN والخوادم الوكيلة:

// استجابة مع رؤوس التخزين المؤقت HTTP/1.1 200 OK Cache-Control: public, max-age=3600 ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" Last-Modified: Wed, 14 Feb 2026 10:00:00 GMT Expires: Wed, 14 Feb 2026 11:00:00 GMT Vary: Accept-Encoding, Accept-Language

دعنا نحلل كل رأس ونفهم غرضه:

رأس Cache-Control

رأس Cache-Control هو الآلية الأساسية لتحديد سياسات التخزين المؤقت. يدعم توجيهات متعددة تتحكم في سلوك التخزين المؤقت:

توجيهات Cache-Control الشائعة

  • public - يمكن تخزين الاستجابة مؤقتًا بواسطة أي ذاكرة تخزين مؤقت (متصفح، CDN، وكيل)
  • private - يمكن تخزين الاستجابة مؤقتًا فقط بواسطة المتصفح (وليس ذاكرات التخزين المشتركة)
  • no-cache - يجب على ذاكرة التخزين المؤقت التحقق من الخادم قبل استخدام الاستجابة المخزنة مؤقتًا
  • no-store - يجب عدم تخزين الاستجابة مؤقتًا في أي مكان
  • max-age=N - الاستجابة جديدة لمدة N ثانية
  • s-maxage=N - مثل max-age ولكن فقط لذاكرات التخزين المشتركة (CDNs)
  • must-revalidate - يجب إعادة التحقق من ذاكرة التخزين المؤقت القديمة قبل الاستخدام
  • immutable - لن تتغير الاستجابة أبدًا (مفيد للأصول المنسقة)
<?php namespace App\Http\Controllers\Api; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use App\Models\Product; class ProductController extends Controller { /** * الحصول على جميع المنتجات مع التخزين المؤقت */ public function index(Request $request) { $products = Product::with('category')->get(); return response()->json([ 'data' => $products ]) ->header('Cache-Control', 'public, max-age=3600') ->header('Vary', 'Accept, Accept-Language'); } /** * الحصول على منتج واحد مع التخزين المؤقت الخاص */ public function show($id) { $product = Product::with('reviews')->findOrFail($id); // ذاكرة تخزين مؤقت خاصة للبيانات الخاصة بالمستخدم return response()->json([ 'data' => $product ]) ->header('Cache-Control', 'private, max-age=600'); } /** * بيانات حساسة - بدون تخزين مؤقت */ public function privateData(Request $request) { $userData = $request->user()->privateInformation(); return response()->json([ 'data' => $userData ]) ->header('Cache-Control', 'no-store, no-cache, must-revalidate') ->header('Pragma', 'no-cache'); } } </div>

تنفيذ ETag (علامة الكيان)

ETags هي معرفات فريدة يتم إنشاؤها لكل إصدار من المورد. عندما يتغير المحتوى، يتغير ETag، مما يسمح للعملاء بتحديد ما إذا كانت نسختهم المخزنة مؤقتًا لا تزال صالحة:

<?php namespace App\Http\Controllers\Api; use Illuminate\Http\Request; use App\Models\Article; class ArticleController extends Controller { /** * الحصول على المقال مع دعم ETag */ public function show(Request $request, $id) { $article = Article::with('author', 'tags')->findOrFail($id); // إنشاء ETag بناءً على المحتوى $etag = md5(json_encode($article) . $article->updated_at); // التحقق مما إذا كان لدى العميل نسخة صالحة مخزنة مؤقتًا if ($request->header('If-None-Match') === $etag) { return response()->noContent()->setStatusCode(304) ->header('ETag', $etag) ->header('Cache-Control', 'public, max-age=3600'); } // إرجاع استجابة جديدة مع ETag return response()->json([ 'data' => $article ]) ->header('ETag', $etag) ->header('Cache-Control', 'public, max-age=3600'); } /** * الحصول على قائمة المقالات مع ETag قوي */ public function index(Request $request) { $page = $request->get('page', 1); $perPage = $request->get('per_page', 20); $articles = Article::with('author') ->published() ->paginate($perPage); // إنشاء ETag قوي (يتضمن جميع المحتويات) $etag = '"' . md5( $articles->items()->toJson() . $articles->lastPage() . $articles->total() ) . '"'; // معالجة الطلب الشرطي if ($request->header('If-None-Match') === $etag) { return response()->noContent() ->setStatusCode(304) ->header('ETag', $etag); } return response()->json($articles) ->header('ETag', $etag) ->header('Cache-Control', 'public, max-age=600'); } } </div>
ETags القوية مقابل الضعيفة: تشير ETags القوية (بدون بادئة W/) إلى محتوى متطابق بايت بايت. تشير ETags الضعيفة (W/"...") إلى محتوى معادل دلاليًا ولكن قد يكون لها اختلافات طفيفة مثل تغييرات المسافات البيضاء.

رأس Last-Modified والطلبات الشرطية

يوفر رأس Last-Modified بديلاً قائمًا على الطابع الزمني لـ ETags. يمكن للعملاء استخدام If-Modified-Since لطلب الموارد فقط إذا تغيرت:

<?php namespace App\Http\Controllers\Api; use Illuminate\Http\Request; use App\Models\BlogPost; use Carbon\Carbon; class BlogPostController extends Controller { /** * الحصول على منشور المدونة مع دعم Last-Modified */ public function show(Request $request, $id) { $post = BlogPost::with('comments')->findOrFail($id); $lastModified = $post->updated_at->toRfc7231String(); // التحقق من رأس If-Modified-Since $ifModifiedSince = $request->header('If-Modified-Since'); if ($ifModifiedSince && strtotime($ifModifiedSince) >= $post->updated_at->timestamp) { return response()->noContent() ->setStatusCode(304) ->header('Last-Modified', $lastModified) ->header('Cache-Control', 'public, max-age=1800'); } return response()->json([ 'data' => $post ]) ->header('Last-Modified', $lastModified) ->header('Cache-Control', 'public, max-age=1800'); } /** * نهج مشترك لـ ETag و Last-Modified */ public function showWithBoth(Request $request, $id) { $post = BlogPost::findOrFail($id); $etag = md5($post->content . $post->updated_at); $lastModified = $post->updated_at->toRfc7231String(); // التحقق من كلا الرأسين الشرطيين $noneMatch = $request->header('If-None-Match') === $etag; $notModified = $request->header('If-Modified-Since') && strtotime($request->header('If-Modified-Since')) >= $post->updated_at->timestamp; if ($noneMatch || $notModified) { return response()->noContent() ->setStatusCode(304) ->header('ETag', $etag) ->header('Last-Modified', $lastModified); } return response()->json(['data' => $post]) ->header('ETag', $etag) ->header('Last-Modified', $lastModified) ->header('Cache-Control', 'public, max-age=3600'); } } </div>

التخزين المؤقت على مستوى تطبيق Laravel

بالإضافة إلى التخزين المؤقت HTTP، توفر Laravel تخزينًا مؤقتًا قويًا على مستوى التطبيق لتخزين نتائج الاستعلام باهظة الثمن واستجابات API والبيانات المحسوبة:

<?php namespace App\Http\Controllers\Api; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use App\Models\User; use App\Models\Product; class CachedApiController extends Controller { /** * تخزين استعلامات قاعدة البيانات باهظة الثمن مؤقتًا */ public function dashboard(Request $request) { $userId = $request->user()->id; // التخزين المؤقت لمدة ساعة واحدة مع مفتاح خاص بالمستخدم $dashboardData = Cache::remember( "user.{$userId}.dashboard", 3600, function () use ($userId) { return [ 'stats' => $this->getUserStats($userId), 'recent_orders' => $this->getRecentOrders($userId), 'recommendations' => $this->getRecommendations($userId), ]; } ); return response()->json(['data' => $dashboardData]); } /** * التخزين المؤقت باستخدام العلامات لإبطال سهل */ public function products(Request $request) { $category = $request->get('category'); $page = $request->get('page', 1); $cacheKey = "products.category.{$category}.page.{$page}"; $products = Cache::tags(['products', "category:{$category}"]) ->remember($cacheKey, 1800, function () use ($category, $page) { return Product::where('category_id', $category) ->with('images', 'reviews') ->paginate(20, ['*'], 'page', $page); }); return response()->json($products) ->header('Cache-Control', 'public, max-age=1800'); } /** * إبطال ذاكرة التخزين المؤقت عند تغيير البيانات */ public function updateProduct(Request $request, $id) { $product = Product::findOrFail($id); $product->update($request->validated()); // إبطال جميع ذاكرات التخزين المؤقت للمنتج باستخدام العلامات Cache::tags(['products', "category:{$product->category_id}"]) ->flush(); return response()->json([ 'message' => 'تم تحديث المنتج بنجاح', 'data' => $product ]); } /** * التخزين المؤقت إلى الأبد مع الإبطال الصريح */ public function settings() { $settings = Cache::rememberForever('app.settings', function () { return [ 'site_name' => config('app.name'), 'features' => Feature::enabled()->get(), 'maintenance' => $this->getMaintenanceStatus(), ]; }); return response()->json(['data' => $settings]) ->header('Cache-Control', 'public, max-age=86400'); } } </div>
تحدي إبطال ذاكرة التخزين المؤقت: واحدة من أصعب المشاكل في علوم الكمبيوتر هي إبطال ذاكرة التخزين المؤقت. قم دائمًا بتطبيق استراتيجيات واضحة لمتى وكيف يتم إبطال البيانات المخزنة مؤقتًا لتجنب تقديم معلومات قديمة.

رأس Vary للتفاوض على المحتوى

يخبر رأس Vary ذاكرات التخزين المؤقت بأن إصدارات مختلفة من المورد موجودة بناءً على رؤوس طلب معينة:

<?php namespace App\Http\Controllers\Api; use Illuminate\Http\Request; use App\Models\Content; class ContentController extends Controller { /** * المحتوى يختلف حسب Accept-Language */ public function show(Request $request, $id) { $locale = $request->header('Accept-Language', 'en'); $content = Content::findOrFail($id); $localizedContent = $content->translate($locale); return response()->json([ 'data' => $localizedContent ]) ->header('Cache-Control', 'public, max-age=3600') ->header('Vary', 'Accept-Language') ->header('Content-Language', $locale); } /** * الاستجابة تختلف حسب رؤوس متعددة */ public function data(Request $request) { $format = $request->header('Accept'); $encoding = $request->header('Accept-Encoding'); $language = $request->header('Accept-Language'); $data = $this->getData(); return response()->json($data) ->header('Cache-Control', 'public, max-age=1800') ->header('Vary', 'Accept, Accept-Encoding, Accept-Language'); } } </div>

تنفيذ Middleware للتخزين المؤقت

إنشاء middleware قابلة لإعادة الاستخدام لتطبيق سياسات تخزين مؤقت متسقة عبر الواجهة البرمجية:

<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; class ApiCacheHeaders { /** * إضافة رؤوس ذاكرة التخزين المؤقت إلى استجابات API */ public function handle(Request $request, Closure $next, ...$params) { $response = $next($request); // التطبيق فقط على الاستجابات الناجحة if ($response->status() !== 200) { return $response; } // تحليل معاملات middleware $cacheType = $params[0] ?? 'public'; $maxAge = $params[1] ?? 3600; // تطبيق رؤوس ذاكرة التخزين المؤقت $response->header('Cache-Control', "{$cacheType}, max-age={$maxAge}"); // إضافة ETag إذا لم يكن موجودًا if (!$response->headers->has('ETag')) { $etag = md5($response->getContent()); $response->header('ETag', $etag); // معالجة الطلبات الشرطية if ($request->header('If-None-Match') === $etag) { return response()->noContent() ->setStatusCode(304) ->header('ETag', $etag) ->header('Cache-Control', "{$cacheType}, max-age={$maxAge}"); } } return $response; } } </div>

تسجيل واستخدام middleware في المسارات:

// app/Http/Kernel.php protected $middlewareAliases = [ 'cache.headers' => \App\Http\Middleware\ApiCacheHeaders::class, ]; // routes/api.php Route::get('/products', [ProductController::class, 'index']) ->middleware('cache.headers:public,3600'); Route::get('/user/profile', [UserController::class, 'profile']) ->middleware('cache.headers:private,600'); </div>

استراتيجيات التخزين المؤقت لأنواع الموارد المختلفة

<?php namespace App\Services; class CacheStrategyService { /** * محتوى ثابت (نادرًا ما يتغير) * مثال: إعدادات الموقع، قوائم الدول */ public function staticContent() { return [ 'Cache-Control' => 'public, max-age=86400, immutable', 'max_age' => 86400, // 24 ساعة ]; } /** * محتوى شبه ثابت (يتغير أحيانًا) * مثال: منشورات المدونة، كتالوجات المنتجات */ public function semiStaticContent() { return [ 'Cache-Control' => 'public, max-age=3600, must-revalidate', 'max_age' => 3600, // ساعة واحدة ]; } /** * محتوى ديناميكي (يتغير بشكل متكرر) * مثال: المنتجات الرائجة، موجز الأخبار */ public function dynamicContent() { return [ 'Cache-Control' => 'public, max-age=300, must-revalidate', 'max_age' => 300, // 5 دقائق ]; } /** * محتوى خاص بالمستخدم * مثال: ملف تعريف المستخدم، سلة التسوق */ public function privateContent() { return [ 'Cache-Control' => 'private, max-age=600', 'max_age' => 600, // 10 دقائق ]; } /** * محتوى في الوقت الفعلي (لا ينبغي تخزينه مؤقتًا) * مثال: أسعار الأسهم المباشرة، رموز المصادقة */ public function realTimeContent() { return [ 'Cache-Control' => 'no-store, no-cache, must-revalidate', 'Pragma' => 'no-cache', 'max_age' => 0, ]; } } </div>
تمرين عملي:
  1. إنشاء نقطة نهاية API مخزنة مؤقتًا لقوائم المنتجات مع رؤوس Cache-Control المناسبة
  2. تنفيذ دعم ETag الذي ينشئ علامات بناءً على بيانات المنتج والطابع الزمني updated_at
  3. إضافة middleware تتعامل تلقائيًا مع الطلبات الشرطية If-None-Match
  4. إنشاء استراتيجية إبطال ذاكرة التخزين المؤقت التي تفرغ ذاكرة التخزين المؤقت للمنتج عند تحديث المنتجات
  5. تنفيذ دعم رأس Vary لقيم Accept-Language المختلفة
  6. إضافة التخزين المؤقت على مستوى التطبيق باستخدام Cache::remember() مع TTL لمدة 30 دقيقة
  7. اختبار التنفيذ عن طريق إجراء طلبات مع رأس If-None-Match والتحقق من استجابات 304

ملخص أفضل الممارسات

  • اختيار قيم max-age المناسبة: موازنة النضارة مع كفاءة ذاكرة التخزين المؤقت
  • استخدام ETags للموارد التي يتم تحديثها بشكل متكرر: يسمح بإعادة التحقق الفعالة
  • تنفيذ كل من التخزين المؤقت HTTP والتطبيق: استراتيجية تخزين مؤقت متعددة الطبقات
  • تضمين رؤوس Vary دائمًا: ضمان التخزين المؤقت الصحيح للتفاوض على المحتوى
  • استخدام private للبيانات الخاصة بالمستخدم: منع مشاكل ذاكرة التخزين المشتركة
  • عدم تخزين البيانات الحساسة مؤقتًا أبدًا: استخدام no-store لرموز المصادقة
  • تنفيذ إبطال ذاكرة التخزين المؤقت: مسح ذاكرة التخزين المؤقت عند تغيير البيانات الأساسية
  • مراقبة معدلات إصابة ذاكرة التخزين المؤقت: تتبع فعالية استراتيجية التخزين المؤقت
  • اختبار الطلبات الشرطية: التحقق من أن استجابات 304 تعمل بشكل صحيح
  • توثيق سلوك التخزين المؤقت: مساعدة مستهلكي API على فهم سياسات التخزين المؤقت
مراقبة الأداء: استخدم أدوات مثل Laravel Telescope أو المقاييس المخصصة لتتبع معدلات إصابة/فقدان ذاكرة التخزين المؤقت. يجب أن تحقق استراتيجية التخزين المؤقت المنفذة بشكل جيد معدلات إصابة ذاكرة تخزين مؤقت تتراوح بين 60-80٪ لمعظم الواجهات البرمجية التي تركز على القراءة.