تنفيذ التخزين المؤقت باستخدام Redis لتطبيقات Next.js 15
تنفيذ التخزين المؤقت باستخدام Redis لتطبيقات Next.js 15
يقدم Next.js 15 استراتيجيات تخزين مؤقت (caching) قوية ومباشرة، ولكن الاعتماد فقط على التخزين المؤقت المدمج في نظام الملفات أو التخزين المؤقت في الذاكرة (memory cache) لتطبيقات المؤسسات الكبيرة يعد وصفة للحصول على بيانات قديمة وأداء غير متوقع ومشاكل مستمرة أثناء النشر. فعندما تتوسع عبر وظائف serverless متعددة أو حاويات (containers)، ينهار التخزين المؤقت المحلي. أنت بحاجة إلى تخزين مؤقت موزع (distributed cache)؛ أنت بحاجة إلى Redis.
يغطي هذا الدليل كيفية تنفيذ التخزين المؤقت باستخدام Redis لتطبيقات Next.js 15 بدقة. وسنقوم ببناء بنية تحتية قوية وقابلة للتوسع باستخدام Upstash Redis (أو أي موفر لخدمة Redis) وواجهة برمجة التطبيقات الأصلية لـ Next.js وهي unstable_cache، مع تجاوز الإعدادات الافتراضية الهشة والتحكم الكامل في طبقة البيانات الخاصة بك.
المشكلة
يعتمد Next.js 15 بشكل كبير على التخزين المؤقت لـ Fetch API وتخزين قطاعات المسارات (route segment caching). بشكل افتراضي، يقوم بتخزين البيانات المؤقتة على نظام الملفات (file system). ويعمل هذا بشكل جيد مع المواقع الثابتة المنشورة على مثيل (instance) واحد من Node.js.
ولكن عند النشر على منصات serverless مثل Vercel أو AWS Lambda، يتوسع تطبيقك عن طريق تشغيل مثيلات متعددة ومستقلة. لا يملك المثيل أ (Instance A) إمكانية الوصول إلى التخزين المؤقت لنظام الملفات الخاص بالمثيل ب (Instance B). إذا زار المستخدم المثيل أ، فقد يحصل على بيانات جديدة، وإذا زار المثيل ب، فقد يحصل على بيانات قديمة أو يتسبب في تشغيل استعلام قاعدة بيانات مكرر وغير ضروري.
علاوة على ذلك، فإن التخزين المؤقت لنظام الملفات يعد متقلباً ومؤقتاً؛ فنشر إصدار جديد من تطبيقك غالباً ما يمسح التخزين المؤقت بالكامل، مما يؤدي إلى زيادة مفاجئة وضخمة في حركة المرور على قاعدة البيانات الخاصة بك مباشرة بعد عملية النشر (وهو ما يُعرف بـ cache stampede).
لذا، نحن بحاجة إلى تخزين مؤقت موزع ومستمر يستمر خارج كود التطبيق ويبقى فعالاً بعد عمليات النشر.
لماذا يعد هذا الأمر صعباً؟
إن دمج Redis في Next.js لا يقتصر فقط على استدعاء redis.get() و redis.set(). يمتلك Next.js 15 بنية معمارية محددة لـ React Server Components (RSC)، حيث يفضل إطار العمل أن يمتلك طبقة التخزين المؤقت الخاصة به.
إذا قمت ببساطة بتغليف استدعاءات قاعدة البيانات بأوامر Redis، فسوف تواجه تعارضاً مع إطار العمل. وستفقد مزايا إعادة التحقق عند الطلب (revalidateTag و revalidatePath)، وتخاطر بتقديم بيانات غير متطابقة للعميل.
التحدي يكمن في حقن Redis داخل دورة حياة التخزين المؤقت لـ Next.js. يجب أن نعترض عمليات القراءة والكتابة للتخزين المؤقت الخاصة بإطار العمل، ونستبدل التخزين المؤقت المتقلب لنظام الملفات بمخزن Redis الموزع الخاص بنا، مع الحفاظ على التوافق مع أساسيات إعادة التحقق في Next.js.
يتطلب هذا معالجات تخزين مؤقت مخصصة (custom cache handlers)، وهي ميزة مرت بمراحل تجريبية مختلفة في Next.js. وفي Next.js 15، يعد تكوين معالج تخزين مؤقت مخصص هو المسار الوحيد المقبول للتطبيقات ذات حركة المرور الكثيفة.
البنية المعمارية
تتكون بنيتنا المعمارية من ثلاث طبقات:
- طبقة التطبيق (Next.js 15): وتتكون من React Server Components التي تنفذ منطق العمل وتعرض واجهة المستخدم.
- معترض التخزين المؤقت (Cache Interceptor): معالج تخزين مؤقت مخصص لـ Next.js يعترض طلبات
unstable_cacheوfetch. - طبقة التخزين المؤقت الموزع (Redis): مخزن سريع في الذاكرة (in-memory key-value store) يحتوي على الاستجابات المحولة إلى نصوص (serialized responses).
عندما يطلب Server Component بيانات:
- يستدعي إطار العمل معالج التخزين المؤقت المخصص لدينا.
- يتحقق المعالج من وجود المفتاح في Redis.
- إذا عثر على البيانات (cache hit)، نقوم بفك ترميز JSON وإرجاعها، ويعرض إطار العمل واجهة المستخدم على الفور.
- إذا لم يعثر على البيانات (cache miss)، ينفذ إطار العمل منطق جلب البيانات (على سبيل المثال، الاستعلام من PostgreSQL)، ويمرر النتيجة إلى المعالج الخاص بنا، والذي يكتبها في Redis قبل إرجاعها إلى المكون (component).
يضمن هذا أن تشترك جميع مثيلات serverless في مصدر واحد وموحد للبيانات الصحيحة.
التنفيذ
سنستخدم حزمة @neshca/cache-handler. توفر هذه الحزمة أساساً قوياً وجاهزاً للإنتاج لمعالجات التخزين المؤقت المخصصة في Next.js، وهي مصممة خصيصاً للربط مع Redis. وسنستخدم ioredis كعميل لـ Redis.
1. تثبيت التبعيات
npm install @neshca/cache-handler ioredis
2. تكوين عميل Redis
قم بإنشاء مثيل عميل Redis قوي. لا تقم بتهيئة اتصالات متعددة لكل استدعاء serverless.
// lib/redis.ts
import { Redis } from 'ioredis';
const redisUrl = process.env.REDIS_URL;
if (!redisUrl) {
throw new Error('REDIS_URL environment variable is not defined');
}
// Ensure a single instance in development to prevent connection leaks during HMR
const globalForRedis = global as unknown as { redis: Redis };
export const redis = globalForRedis.redis || new Redis(redisUrl, {
maxRetriesPerRequest: 3,
enableReadyCheck: false,
});
if (process.env.NODE_ENV !== 'production') globalForRedis.redis = redis;
3. إنشاء معالج التخزين المؤقت (Cache Handler)
يوضح هذا الملف لـ Next.js كيفية التواصل مع Redis. ويقوم بربط عمليات التخزين المؤقت لـ Next.js (مثل get و set و revalidateTag) بأوامر Redis.
// cache-handler.mjs
import { CacheHandler } from '@neshca/cache-handler';
import createRedisHandler from '@neshca/cache-handler/redis-strings';
import { Redis } from 'ioredis';
CacheHandler.onCreation(async () => {
let client;
try {
// We instantiate the client here.
// In production, ensure REDIS_URL is set.
client = new Redis(process.env.REDIS_URL, {
maxRetriesPerRequest: 3,
lazyConnect: true, // Don't block startup
});
// Test the connection
client.on('error', (error) => {
console.error('Redis connection error:', error);
});
} catch (error) {
console.warn('Failed to initialize Redis client for cache handler', error);
}
return {
handlers: [
createRedisHandler({
client,
keyPrefix: 'next-cache:',
timeoutMs: 1000, // Fail fast if Redis is slow
}),
],
};
});
export default CacheHandler;
4. ربطه بـ Next.js
أخبر Next.js باستخدام معالج التخزين المؤقت المخصص في ملف next.config.js.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
// Note: experimental features change, but this is the current pattern for Next 15
cacheHandler: require.resolve('./cache-handler.mjs'),
cacheLife: {
default: {
stale: 3600, // 1 hour
revalidate: 86400, // 1 day
},
},
},
};
module.exports = nextConfig;
5. جلب البيانات
الآن، استخدم unstable_cache أو fetch الأصلي كما تفعل عادةً. سيقوم إطار العمل بتوجيه عمليات التخزين المؤقت بشفافية عبر Redis.
// app/products/[id]/page.tsx
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
const getProduct = unstable_cache(
async (id: string) => {
console.log('Cache miss: Fetching product from DB', id);
return await db.product.findUnique({ where: { id } });
},
['product-details'], // Cache key segments
{ tags: ['products'], revalidate: 3600 }
);
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
if (!product) return <div>Product not found</div>;
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
</main>
);
}
عندما تحتاج إلى إبطال هذه البيانات (على سبيل المثال، عبر لوحة تحكم admin webhook)، ما عليك سوى استدعاء revalidateTag('products'). سيقوم معالج التخزين المؤقت بترجمة ذلك إلى أمر Redis DEL أو إبطال قائم على الوسم (tag-based invalidation)، مما يضمن أن الطلب التالي سيتوجه لقاعدة البيانات مباشرة.
الأخطاء الشائعة
يعرضك تنفيذ التخزين المؤقت باستخدام Redis في Next.js 15 لعدة فخاخ معمارية. تجنب هذه الحالات:
1. وقت الاستجابة لـ Redis وعواصف انقطاع الخدمة (Timeout Storms)
يتميز Redis بالسرعة، ولكن وقت استجابة الشبكة لا يمكن التنبؤ به. إذا توقف مثيل Redis الخاص بك، أو ارتفع وقت استجابة الاتصال إلى 2000ms، فسيتوقف تطبيق Next.js بالكامل بانتظار التخزين المؤقت.
الحل: افرض أوقات انتظار صارمة في معالج التخزين المؤقت الخاص بك (مثل timeoutMs: 500). إذا لم يستجب Redis خلال 500ms، يجب على معالج التخزين المؤقت أن يفشل بسلاسة، ويعامل الطلب كحالة عدم عثور على البيانات (cache miss) ويتراجع للاستعلام من قاعدة البيانات مباشرة. توافر الخدمة أهم من الأداء.
2. تسلسل الكائنات الكبيرة (Serialization)
يخزن Redis النصوص البرمجية. ويقوم Next.js بتحويل بياناتك إلى JSON قبل إرسالها إلى معالج التخزين المؤقت. إن تخزين كتل JSON ضخمة بحجم 5 ميجابايت تمثل جدولاً كاملاً غير مقسم إلى صفحات سيخنق عمليات الإدخال والإخراج لشبكة Redis ودورات المعالج (CPU) المستخدمة للتسلسل وفك التسلسل. الحل: قم بتخزين ما تحتاجه فقط. حدد استعلامات قاعدة البيانات الخاصة بك بدقة ولا تجلب الحقول غير المستخدمة، واجلب فقط البيانات المطلوبة للمكون لتقليل حجم الحمولة.
3. الازدحام المفاجئ على التخزين المؤقت (Cache Stampedes)
عندما تنتهي صلاحية مفتاح تخزين مؤقت شائع أو يتم إبطاله، قد تصل مئات الطلبات المتزامنة إلى الخادم في نفس الوقت. ستشهد جميع هذه الطلبات حالة عدم عثور على البيانات (cache miss) وتضغط على قاعدة البيانات للحصول على نفس الاستعلام بالضبط، مما قد يؤدي إلى تعطيل قاعدة البيانات الرئيسية.
الحل: يوفر unstable_cache في Next.js إلغاء تكرار الطلبات (request deduping) بشكل أساسي لكل مثيل. لإلغاء التكرار الموزع، تحتاج إلى آلية قفل مدعومة بـ Redis، أو الاعتماد على أنماط stale-while-revalidate (SWR) بحيث يحصل المستخدم على البيانات القديمة أثناء حدوث إعادة التحقق في الخلفية.
4. تسريبات الاتصال في بيئات Serverless
تتميز بيئات serverless (مثل وظائف Vercel) بتجميد واستعادة سياق التنفيذ. إذا قمت بفتح اتصال Redis جديد مع كل طلب، فسوف تستهلك حد اتصال Redis الخاص بك على الفور.
الحل: قم بتهيئة عميل Redis خارج نطاق معالج الطلبات. واستخدم عملاء Redis المعتمدين على HTTP (مثل Upstash REST API) إذا لم تتمكن من الحفاظ على اتصالات TCP مستمرة، على الرغم من أن حزمة @neshca/cache-handler تدعم العملاء المستمرين بكفاءة إذا تم تكوينها بشكل صحيح.
النتيجة
من خلال تنفيذ معالج تخزين مؤقت مخصص لـ Redis، فإنك تتخلص من التخزين المؤقت غير المتوقع والمؤقت لنظام الملفات وتستبدله بمخزن بيانات مركزي قوي.
لن تؤدي عمليات النشر الجديدة إلى مسح التخزين المؤقت لديك. وستشترك مثيلات serverless في مصدر موحد وصحيح للمعلومات، وينخفض الضغط على قاعدة البيانات الخاصة بك بشكل كبير، وتستقر أوقات الاستجابة عبر مجموعة الخوادم.
يريد Next.js 15 التحكم في التخزين المؤقت. دعه يتحكم في واجهة برمجة التطبيقات (API)، ولكن يجب أن تمتلك أنت البنية التحتية. يمنحك Redis التحكم المطلوب لتشغيل Next.js على نطاق واسع. لا ترضَ بالإعدادات الافتراضية.
خدمة سفن لابس
تطوير برمجيات SaaS - Next.js و MERN
