ملخّص المقال: شرح عملي لكيفية استخدام Redis داخل تطبيقات Laravel — من التثبيت والإعداد، مرورًا بالتخزين المؤقت (Caching) وإبطال الكاش (Cache Invalidation) والطوابير (Queues) والعدادات وهياكل البيانات، وصولًا إلى إدارة الذاكرة وسياسات الحفظ (Persistence) والأخطاء الشائعة التي يقع فيها المطورون في بيئة الإنتاج.
مقدمة
تُعد سرعة الاستجابة أحد أهم العوامل التي تؤثر في نجاح أي تطبيق ويب، خصوصًا عندما يتعامل التطبيق مع حجم بيانات كبير أو عدد مرتفع من الطلبات المتزامنة.
الاعتماد على قاعدة البيانات وحدها لتنفيذ كل عملية — بما فيها العمليات المتكررة التي لا تتغيّر نتيجتها كثيرًا — يؤدي مع الوقت إلى زيادة الضغط عليها وارتفاع زمن الاستجابة. وهنا يأتي دور Redis، وهو مخزن بيانات يعمل داخل الذاكرة (In-Memory Data Store)، يمكن استخدامه في سيناريوهات متعددة مثل Caching وQueues وCounters وRate Limiting وغيرها.
في هذا المقال سنتناول Redis بشكل عملي، ثم نربطه مع Laravel ونستعرض أهم الاستخدامات والأوامر والممارسات التي تساعد فعليًا على تحسين الأداء دون الوقوع في مشكلات جديدة.
ما هو Redis؟
Redis هو نظام مفتوح المصدر لتخزين البيانات يعتمد نموذج Key-Value، لكنه لا يقتصر على تخزين النصوص والقيم البسيطة، بل يدعم مجموعة متنوعة من هياكل البيانات:
Strings— القيم النصية والرقمية.Lists— قوائم مرتبة تسمح بالتكرار.Sets— مجموعات من القيم الفريدة بلا ترتيب.Sorted Sets— مجموعات فريدة مرتبة حسب Score.Hashes— كائنات تحتوي على حقول وقيم.
ولأن Redis يخزّن البيانات في الذاكرة (RAM)، فإن عمليات القراءة والكتابة فيه تتم بزمن استجابة منخفض جدًا، ما يجعله مناسبًا بشكل خاص للبيانات التي يتم الوصول إليها بشكل متكرر.
لماذا نستخدم Redis؟
1. السرعة
تخزين البيانات في الذاكرة يجعل عمليات القراءة والكتابة أسرع بكثير من الرجوع إلى قاعدة البيانات في كل طلب، خصوصًا في الاستعلامات المعقّدة أو التجميعية.
2. تنوّع هياكل البيانات
Redis لا يقتصر على String، بل يوفّر هياكل مختلفة تسمح ببناء حلول متنوعة (ترتيب، عدّ، مجموعات فريدة) دون تنفيذ كل شيء عبر قاعدة البيانات.
3. تعدد الاستخدامات
- التخزين المؤقت Caching
- قوائم الانتظار Queues
- عدادات الزيارات والأحداث Counters
- لوحات الصدارة Leaderboards
- تحديد معدّل الطلبات Rate Limiting
- الأقفال الذرّية Atomic Locks
- معالجة البيانات بشكل متسلسل أو على دفعات
متى لا يكون Redis هو الحل؟
من المهم أن نضع الأمور في نصابها: Redis ليس بديلًا عن قاعدة البيانات، ولا هو حلٌّ لكل مشكلة أداء.
- عندما تكون المشكلة في الاستعلام نفسه: استعلام بطيء بسبب غياب Index أو بسبب مشكلة N+1 يجب إصلاحه أولًا، لا إخفاؤه خلف طبقة كاش.
- عندما تكون البيانات ضخمة: Redis محدود بحجم الذاكرة المتاحة، وتخزين ملايين السجلات فيه مكلف.
- عندما تكون البيانات حسّاسة للتحديث اللحظي: أي كاش يعني احتمال قراءة نسخة قديمة؛ يجب أن يكون ذلك مقبولًا في سياق التطبيق.
تثبيت Redis
Linux — Ubuntu / Debian
sudo apt update
sudo apt install redis-serverللتأكد من حالة الخدمة:
sudo systemctl status redis-servermacOS
brew install redis
brew services start redisDocker
خيار عملي ومناسب لبيئات التطوير لأنه يعزل نسخة Redis عن النظام:
docker run -d --name redis -p 6379:6379 redis:7-alpineالتحقق من التشغيل
redis-cli ping
# PONGالتعامل مع Redis عبر redis-cli
قبل الانتقال إلى Laravel، من المفيد التعرّف على الأوامر الأساسية مباشرة من الطرفية:
redis-cliتخزين قيمة — SET
SET myRedisKey "Hello Redis"
# OKقراءة قيمة — GET
GET myRedisKey
# "Hello Redis"تحديد مدة انتهاء الصلاحية
SETEX anotherKey 10 "Hello" # يُنشئ المفتاح مع مدة 10 ثوانٍ
EXPIRE myRedisKey 60 # يضيف مدة انتهاء لمفتاح موجود
TTL myRedisKey # الوقت المتبقي بالثوانيالقيمة -1 في نتيجة TTL تعني أن المفتاح بلا مدة انتهاء، بينما -2 تعني أن المفتاح غير موجود أصلًا.
حذف المفاتيح — DEL
DEL myKey
DEL key1 key2 key3استعراض المفاتيح — SCAN وليس KEYS
الأمر KEYS * شائع في الأمثلة التعليمية، لكنه يمرّ على كامل مساحة المفاتيح ويحجب الخادم أثناء التنفيذ.
تحذير: تجنّب
KEYSفي بيئة الإنتاج، واستخدمSCANالذي يعمل على دفعات ولا يحجب الخادم.
SCAN 0 MATCH "product:views:*" COUNT 100RedisInsight
RedisInsight واجهة رسومية رسمية تساعد على إدارة قواعد بيانات Redis ومراقبتها وتنفيذ الأوامر بطريقة أسهل من redis-cli. تتيح استعراض المفاتيح وأنواع البيانات، وفحص استهلاك الذاكرة، وتحليل المفاتيح الأكبر حجمًا (Memory Analysis) — وهي معلومة مهمة جدًا عند تشخيص مشكلات الذاكرة.
إعداد Redis مع Laravel
اختيار العميل: phpredis أم predis؟
يدعم Laravel عميلين للاتصال بـ Redis:
- phpredis: إضافة مكتوبة بلغة C تُثبّت على مستوى PHP، وهي الخيار الافتراضي في الإصدارات الحديثة من Laravel والأفضل من ناحية الأداء واستهلاك الذاكرة.
- predis: مكتبة مكتوبة بالكامل بـ PHP، أسهل في التثبيت (عبر Composer فقط) ومفيدة عندما لا تستطيع تثبيت إضافات PHP على الخادم.
لتثبيت predis:
composer require predis/predisولتثبيت phpredis على Ubuntu:
sudo apt install php-redisملف الإعداد config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env(
'REDIS_PREFIX',
Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'
),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],متغيرات البيئة
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
CACHE_STORE=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redisملاحظة على الإصدارات: في Laravel 11 فما فوق يُستخدم المتغير
CACHE_STORE، بينما كان الاسمCACHE_DRIVERفي الإصدارات السابقة.
قواعد البيانات المرقّمة في Redis
على عكس MySQL أو PostgreSQL، لا تحمل قواعد Redis أسماء مثل users أو products، بل هي قواعد مرقّمة (0, 1, 2 ... افتراضيًا حتى 15).
فعندما نكتب:
'database' => env('REDIS_DB', '0'),فهذا يعني أن الاتصال default سيستخدم قاعدة Redis رقم 0. ويمكن الانتقال بينها يدويًا من الطرفية:
SELECT 1فصل الكاش عن باقي الاستخدامات (مثل الطوابير والجلسات) في قاعدة مستقلة ليس ترفًا تنظيميًا فقط؛ فهو يجعل أمر php artisan cache:clear آمنًا، ولا يمسح مهام الطوابير التي لم تُعالَج بعد.
انتبه إلى البادئة Prefix
يضيف Laravel بادئة تلقائية لكل مفتاح (مثل laravel_database_). لذلك عندما تكتب في التطبيق:
Cache::put('greeting', 'Hello', 60);فإن المفتاح الفعلي داخل Redis سيكون شيئًا مثل:
laravel_database_greetingوهذا سبب شائع للحيرة عند البحث عن المفاتيح عبر redis-cli وعدم العثور عليها بالاسم المتوقَّع.
استخدام Redis للتخزين المؤقت في Laravel
يوفّر Laravel واجهة Cache موحّدة تخفي تفاصيل المحرّك المستخدَم:
use Illuminate\Support\Facades\Cache;تخزين قيمة مؤقتة
Cache::put('greeting', 'Hello from Redis Cache!', 60);
Cache::put('user_settings', [
'theme' => 'dark',
'notifications' => true,
], 30);مهم: المعامل الثالث يُحسب بـ الثواني منذ Laravel 5.8 (كان بالدقائق قبل ذلك). ويمكن تمرير كائن
DateTimeأوnow()->addMinutes(10)لجعل النية أوضح.
القراءة من الكاش
$greeting = Cache::get('greeting');
// قيمة افتراضية عند عدم وجود المفتاح
$value = Cache::get('some_key', 'Default Value');
// التحقق من الوجود
if (Cache::has('some_key')) {
// ...
}الحذف والتخزين الدائم
Cache::forget('greeting');
Cache::forever('important_data', ['value' => 123]);
// حذف واسترجاع القيمة في عملية واحدة
$value = Cache::pull('greeting');الطريقة الأهم: Cache::remember()
تختصر remember() نمط Cache-Aside بالكامل في سطر واحد، والفكرة:
- يبحث Laravel عن المفتاح في الكاش.
- إذا وجد البيانات، يعيدها مباشرة.
- إذا لم يجدها، ينفّذ الـ Closure.
- يخزّن النتيجة في الكاش.
- يعيد النتيجة.
$users = Cache::remember('users:all', 60, function () {
return User::all();
});
// نسخة بلا مدة انتهاء
$settings = Cache::rememberForever('settings:global', function () {
return Setting::pluck('value', 'key');
});مثال عملي: تخزين منتج في Redis
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ProductController extends Controller
{
public function show(int $id)
{
$product = Cache::remember(
"product:{$id}",
now()->addMinutes(10),
fn () => Product::find($id)
);
abort_if(! $product, 404);
return view('product.show', compact('product'));
}
public function update(Request $request, int $id)
{
$product = Product::findOrFail($id);
$product->update($request->validated());
Cache::forget("product:{$id}");
return back()->with('success', 'Product updated successfully!');
}
}لاحظ السطر الحاسم في دالة التحديث:
Cache::forget("product:{$id}");بدونه سيستمر التطبيق في عرض نسخة قديمة من المنتج حتى انتهاء مدة الكاش.
تنبيه: تجنّب تخزين نتيجة
nullفي الكاش دون قصد. إذا كان المنتج غير موجود، فإنremember()ستعيد تنفيذ الاستعلام في كل طلب (لأن القيمة تُعامَل كغياب للكاش)، وقد يفتح ذلك بابًا لضغط غير متوقع على قاعدة البيانات.
تخزين نماذج Eloquent والعلاقات
$post = Post::find(1);
Cache::put('post:1', $post, 120);
$cachedPost = Cache::get('post:1');ويمكن تخزين Collection كاملة:
$latestPosts = Cache::remember('posts:latest', 60, function () {
return Post::query()
->latest()
->take(10)
->get();
});مثال أوسع مع العلاقات:
public function getUserProfile(int $userId)
{
$user = Cache::remember(
"user:{$userId}:profile",
now()->addDay(),
fn () => User::with(['posts', 'comments'])->find($userId)
);
abort_if(! $user, 404, 'User not found.');
return view('profile.show', compact('user'));
}لكن انتبه إلى ثلاث نقاط عند تخزين نماذج Eloquent:
- يتم Serialize للنموذج بالكامل مع علاقاته المُحمَّلة، ما يعني حجمًا أكبر مما تتوقع.
- عند التغيير في بنية النموذج أو الجدول، قد تصبح النسخ المخزَّنة غير متوافقة.
- في كثير من الحالات يكون تخزين مصفوفة أو DTO يحتوي على الحقول المطلوبة فقط أخفّ وأكثر استقرارًا من تخزين النموذج كاملًا.
Cache Tags
يدعم محرّك Redis وسوم الكاش، وهي مفيدة لإبطال مجموعة مفاتيح مترابطة دفعة واحدة:
Cache::tags(['products'])->put("product:{$id}", $product, 600);
// إبطال كل ما يتعلق بالمنتجات
Cache::tags(['products'])->flush();ملاحظة: الوسوم غير مدعومة في محرّكات
fileوdatabase، لذا اعتمادها يربط شيفرتك بـ Redis أو Memcached.
Cache Invalidation: التحدي الحقيقي
أشهر مقولة في هذا المجال أن أصعب مشكلتين في علوم الحاسوب هما تسمية الأشياء وإبطال الكاش. والمثال التالي يوضّح السبب:
Product #1 → Price = 100 (تم تخزينه في Redis)
تحديث في قاعدة البيانات → Price = 150
التطبيق ما زال يقرأ → Price = 100لذلك يجب أن تكون لديك استراتيجية واضحة لمسح أو تحديث الكاش عند الإنشاء والتعديل والحذف.
الاستراتيجيات الشائعة
- TTL قصير: أبسط حل — اقبل بيانات قديمة لفترة محدودة ومعروفة.
- الإبطال الصريح: استدعاء
Cache::forget()عند كل تعديل. - Observers: ربط الإبطال بأحداث النموذج تلقائيًا (الأفضل للصيانة).
- المفاتيح الإصدارية: إدراج
updated_atضمن المفتاح، فيتولّد مفتاح جديد تلقائيًا عند كل تعديل.
الإبطال التلقائي عبر Observer
namespace App\Observers;
use App\Models\Product;
use Illuminate\Support\Facades\Cache;
class ProductObserver
{
public function saved(Product $product): void
{
$this->flush($product);
}
public function deleted(Product $product): void
{
$this->flush($product);
}
private function flush(Product $product): void
{
Cache::forget("product:{$product->id}");
Cache::forget('products:total_value');
}
}وربطه بالنموذج في Laravel 11 فما فوق:
use App\Observers\ProductObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy(ProductObserver::class)]
class Product extends Model
{
// ...
}المفاتيح الإصدارية
$key = "product:{$product->id}:v{$product->updated_at->timestamp}";
$data = Cache::remember($key, 3600, fn () => $product->load('reviews'));ميزة هذه الطريقة أنها لا تحتاج إلى حذف صريح؛ إذ يصبح المفتاح القديم يتيمًا ويُزال لاحقًا بانتهاء مدته أو بسياسة الإزالة (Eviction).
مثال تطبيقي: كاش إحصائيات لوحة التحكم
لنفترض جدول products يحتوي على عدد كبير من السجلات، ونريد حساب القيمة الإجمالية للمخزون، وقيمة المنتجات المباعة، والقيمة المجمّعة.
النسخة غير المُحسَّنة
public function calculateTotalValues()
{
$totals = [
'totalValue' => 0,
'totalSoldValue' => 0,
'totalCombinedValue' => 0,
];
Product::chunk(1000, function ($products) use (&$totals) {
foreach ($products as $product) {
$totals['totalValue'] += $product->price * $product->quantity;
$totals['totalSoldValue'] += $product->price * $product->sold_units;
$totals['totalCombinedValue'] +=
$product->price * ($product->quantity + $product->sold_units);
}
});
return view('dashboard', compact('totals'));
}المشكلة هنا مزدوجة: الحساب يتكرر مع كل تحميل للصفحة، كما أنه يتم في PHP بينما تستطيع قاعدة البيانات تنفيذه أسرع بكثير.
الخطوة الأولى: حسّن الاستعلام قبل الكاش
$totals = Product::query()
->selectRaw('SUM(price * quantity) as total_value')
->selectRaw('SUM(price * sold_units) as total_sold_value')
->selectRaw('SUM(price * (quantity + sold_units)) as total_combined_value')
->first();قاعدة عامة: الكاش يخفي التكلفة، لكنه لا يلغيها. حسّن الاستعلام أولًا، ثم خزّن نتيجته.
الخطوة الثانية: خزّن النتيجة في Redis
public function calculateTotalValues()
{
$totals = Cache::store('redis')->remember(
'products:totals',
now()->addMinutes(10),
function () {
return Product::query()
->selectRaw('SUM(price * quantity) as total_value')
->selectRaw('SUM(price * sold_units) as total_sold_value')
->selectRaw('SUM(price * (quantity + sold_units)) as total_combined_value')
->first()
->toArray();
}
);
return view('dashboard', compact('totals'));
}لاحظ فرقًا مهمًا عن الطريقة التقليدية: خزّنّا مفتاحًا واحدًا يحتوي كل القيم بدل ثلاثة مفاتيح ومفتاح رابع بوصفه عَلَمًا (flag). هذا يمنع حالة عدم الاتساق التي تحدث حين ينتهي أحد المفاتيح قبل غيره فتُعرض أرقام من لحظتين مختلفتين.
القيمة 600 ثانية (10 دقائق) مناسبة لبيانات لا تحتاج تحديثًا لحظيًا. أما لوحة تُحدَّث مرة كل 24 ساعة فيمكن رفع المدة كثيرًا.
Cache Stampede والأقفال الذرّية
تخيّل مفتاحًا مكلفًا انتهت صلاحيته في لحظة ذروة، فوصل 500 طلب متزامن ووجدوا الكاش فارغًا — سينفّذ الجميع الاستعلام الثقيل نفسه في الوقت نفسه. هذه المشكلة تُعرف بـ Cache Stampede.
الحل الأول: القفل الذرّي
use Illuminate\Support\Facades\Cache;
$totals = Cache::lock('products:totals:lock', 10)->block(5, function () {
return Cache::remember('products:totals', 600, function () {
return $this->computeTotals();
});
});هنا يعيد بناء الكاش طلبٌ واحد فقط، بينما ينتظر الباقون النتيجة.
الحل الثاني: Cache::flexible
في Laravel 11.23 فما فوق تتوفر flexible() التي تطبّق نمط stale-while-revalidate:
$totals = Cache::flexible('products:totals', [300, 900], function () {
return $this->computeTotals();
});خلال أول 300 ثانية تُعدّ البيانات طازجة. وبين 300 و900 ثانية تُعاد البيانات القديمة للمستخدم فورًا، بينما يُعاد بناء الكاش في الخلفية. بعد 900 ثانية تُحسب من جديد بشكل متزامن.
Redis كعدّاد للزيارات
العدّادات من أفضل استخدامات Redis، لأن عملية INCR ذرّية وسريعة جدًا، وتُغنينا عن تحديث صف في قاعدة البيانات مع كل زيارة:
use App\Models\Product;
use Illuminate\Support\Facades\Redis;
public function show(int $id)
{
$product = Product::findOrFail($id);
$viewsCount = Redis::incr("product:views:{$id}");
return view('product.show', compact('product', 'viewsCount'));
}الأمر INCR يعيد القيمة الجديدة مباشرة، فلا حاجة إلى استدعاء GET بعده. ويمكن أيضًا استخدام واجهة الكاش:
Cache::increment("product:views:{$id}");ممارسة مهمة: العدّادات بيانات قابلة للفقدان إن لم يكن Persistence مفعّلًا. إن كانت الأرقام مهمة تجاريًا، شغّل مهمة مجدولة تُرحّل القيم دوريًا إلى قاعدة البيانات.
تحديد معدّل الطلبات Rate Limiting
نفس الفكرة تُستخدم لحماية نقاط النهاية من الاستخدام المفرط:
use Illuminate\Support\Facades\RateLimiter;
$executed = RateLimiter::attempt(
"send-message:{$user->id}",
perMinute: 5,
callback: fn () => $this->sendMessage($request),
);
if (! $executed) {
return response('Too many messages sent.', 429);
}Redis مع Laravel Queues
Redis لا يقتصر على الكاش؛ فهو من أكثر محرّكات الطوابير استخدامًا في Laravel.
QUEUE_CONNECTION=redisمثال: إرسال البريد الإلكتروني في الخلفية
php artisan make:job SendWelcomeEmailnamespace App\Jobs;
use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public int $tries = 3;
public int $timeout = 30;
public function __construct(
public User $user
) {}
public function handle(): void
{
Mail::to($this->user->email)->send(new WelcomeMail($this->user));
}
}وبعد تسجيل المستخدم:
SendWelcomeEmail::dispatch($user);
// أو مع تأخير
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));تشغيل العامل Worker
php artisan queue:work redis --queue=high,default --tries=3بهذا لا ينتظر المستخدم اكتمال إرسال البريد قبل الحصول على استجابة التطبيق.
ملاحظات إنتاجية على الطوابير
- استخدم Supervisor (أو Laravel Horizon) لإبقاء العامل يعمل وإعادة تشغيله عند التوقف.
queue:workيحمّل التطبيق في الذاكرة مرة واحدة، لذا يجب تنفيذphp artisan queue:restartبعد كل نشر ليأخذ العامل بالشيفرة الجديدة.- جهّز جدول
failed_jobsوراقبه؛ المهام الفاشلة الصامتة من أخطر ما يمر دون ملاحظة. - لا تمرّر كائنات ثقيلة إلى المهمة؛
SerializesModelsتخزّن المعرّف فقط وتعيد جلب النموذج عند التنفيذ. - افصل قاعدة Redis الخاصة بالطوابير عن قاعدة الكاش كما ذكرنا سابقًا.
تسمية مفاتيح Redis
عندما يتوسّع استخدام Redis في المشروع، تصبح تسمية المفاتيح مسألة صيانة حقيقية. اتفق على اصطلاح واضح والتزم به.
استخدم النقطتين للفصل بين المستويات
productviews1 ✗
product:views:1 ✓التزم بالأحرف الصغيرة
PRODUCTS:TOTAL ✗
products:total ✓استخدم : للتسلسل و_ داخل الاسم المركّب
products:total_value
user:42:profile
order:2026:08:countهذا الاصطلاح يجعل البحث بـ SCAN والحذف الجماعي والمراقبة أسهل بكثير.
أنواع البيانات في Redis
1. Strings
أبسط الأنواع، ويستوعب نصًا أو رقمًا أو بيانات مُسلسَلة:
Cache::store('redis')->put('app:version', '2.1.0');
Cache::store('redis')->get('app:version');2. Lists
قوائم مرتبة تسمح بالتكرار، وتصلح للطوابير البسيطة وسجلات الأحداث الأخيرة:
use Illuminate\Support\Facades\Redis;
foreach ($products as $product) {
Redis::lpush('products', $product->id); // إضافة إلى البداية
}
Redis::rpush('products', $product->id); // إضافة إلى النهاية
$items = Redis::lrange('products', 0, 9); // أول عشرة عناصر
$first = Redis::lpop('products'); // إزالة عنصر من البداية
Redis::ltrim('products', 0, 99); // الإبقاء على آخر 100 عنصر فقطملاحظة: تمرير عدد إلى
LPOP(مثلLPOP products 10) متاح ابتداءً من Redis 6.2. وأمرLTRIMمفيد جدًا لمنع نمو القائمة بلا حدود.
3. Sets
مجموعات لا تسمح بالتكرار ولا تحفظ ترتيبًا:
Redis::sadd("post:{$postId}:likes", $userId);
$userIds = Redis::smembers("post:{$postId}:likes");
$hasLiked = Redis::sismember("post:{$postId}:likes", $userId);
$likesCount = Redis::scard("post:{$postId}:likes");تصلح Sets لتخزين معرّفات المستخدمين الذين أعجبوا بمنشور، أو لتتبّع الزوّار الفريدين، ولعمليات التقاطع والاتحاد بين المجموعات (SINTER وSUNION).
4. Sorted Sets
تشبه Sets لكنها تضيف Score رقميًا لكل عنصر، فتتيح الترتيب والاستعلام بالنطاقات:
foreach ($products as $product) {
Redis::zadd('products:by_price', $product->price, $product->id);
}
$cheapest = Redis::zrange('products:by_price', 0, 9); // تصاعديًا
$expensive = Redis::zrevrange('products:by_price', 0, 9); // تنازليًا
$rank = Redis::zrevrank('products:by_price', $product->id); // ترتيب عنصر محددالاستخدامات الشائعة: Leaderboards، الترتيب حسب النقاط أو السعر، والبيانات الزمنية البسيطة (Time Series) باستخدام الطابع الزمني كـ Score.
5. Hashes
مناسبة لتخزين كائن بحقوله بدل تسلسل الكائن كله في مفتاح واحد، إذ تتيح قراءة حقل واحد أو تحديثه دون لمس الباقي:
Redis::hset("user:{$userId}", 'name', $user->name);
Redis::hset("user:{$userId}", 'plan', 'pro');
$name = Redis::hget("user:{$userId}", 'name');
$all = Redis::hgetall("user:{$userId}");
Redis::hincrby("user:{$userId}", 'login_count', 1);أوامر Redis المهمة
PING — التحقق من الاتصال
Redis::command('PING'); // PONGEXISTS — التحقق من وجود مفتاح
Redis::exists('visitors');TTL — الوقت المتبقي قبل انتهاء الصلاحية
Redis::ttl('visitors');PERSIST — إزالة مدة الانتهاء
Redis::persist('visitors');COPY — نسخ قيمة مفتاح إلى آخر
Redis::command('COPY', ['visitors', 'visitors:snapshot']);INCR و DECR — الزيادة والإنقاص
Redis::incr('views:post:1');
Redis::incrby('views:post:1', 10);
Redis::decr('stock:product:5');SORT — ترتيب عناصر List أو Set
Redis::command('SORT', ['visitors', ['sort' => 'desc', 'alpha' => true]]);DEL — الحذف
Redis::del('visitors');FLUSHDB و FLUSHALL
Redis::command('FLUSHDB'); // يمسح القاعدة الحالية فقط
Redis::command('FLUSHALL'); // يمسح جميع قواعد Redis⚠ تحذير: لا تستخدم
FLUSHALLفي بيئة الإنتاج. فهو يمسح كل القواعد — بما فيها الطوابير والجلسات وليس الكاش وحده. إن أردت مسح الكاش فقط فاستخدمphp artisan cache:clearمع قاعدة كاش منفصلة.
Redis Pipelines
عند تنفيذ عدد كبير من الأوامر، تصبح تكلفة الذهاب والإياب الشبكي (Round Trip) لكل أمر ملحوظة. تحلّ Pipelines هذه المشكلة بتجميع الأوامر وإرسالها دفعة واحدة.
بدلًا من:
for ($i = 0; $i < 1000; $i++) {
Redis::set("key:{$i}", $i);
}استخدم:
Redis::pipeline(function ($pipe) {
for ($i = 0; $i < 1000; $i++) {
$pipe->set("key:{$i}", $i);
}
});الفرق في الأداء يكون كبيرًا جدًا عند التعامل مع آلاف الأوامر، خصوصًا إن كان خادم Redis على جهاز منفصل.
Redis Transactions
Redis::transaction(function ($tx) {
$tx->set('key', 'value');
$tx->incr('counter');
});تُنفَّذ مجموعة الأوامر ضمن MULTI/EXEC بشكل ذرّي، بمعنى أن Redis لن يُدخل أوامر عميل آخر بينها أثناء التنفيذ.
فرق جوهري: لا تخلط بين
Redis::transaction()وDB::transaction(). معاملات Redis لا تدعم Rollback؛ فإذا فشل أحد الأوامر أثناء التنفيذ، تستمر بقية الأوامر ولا يُتراجع عمّا نُفِّذ. كما لا يمكن قراءة نتيجة أمر داخل المعاملة لاتخاذ قرار بشأن الأمر التالي.
إذا احتجت منطقًا شرطيًا ذرّيًا حقيقيًا، فالخيار الصحيح هو سكربت Lua الذي يُنفَّذ كوحدة واحدة داخل الخادم:
Redis::eval(<<<'LUA'
local current = tonumber(redis.call('GET', KEYS[1]) or 0)
if current >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
end
return -1
LUA, 1, 'stock:product:5', 3);الحفظ الدائم Persistence
رغم أن Redis يعمل من الذاكرة، فإنه يدعم آليات لحفظ البيانات واستعادتها بعد إعادة التشغيل.
RDB — Snapshots
ينشئ Redis لقطات دورية للبيانات على القرص. سريع عند الاستعادة وخفيف على الأداء، لكنك قد تفقد ما كُتب بين لقطتين.
AOF — Append Only File
يسجّل كل عمليات الكتابة بحيث يعاد تنفيذها عند الإقلاع. أكثر أمانًا من ناحية فقدان البيانات، مقابل ملف أكبر واستعادة أبطأ.
بدون Persistence
خيار مقبول لكاش خالص يمكن إعادة بنائه بالكامل من مصدره الأصلي.
RDB + AOF
الجمع بين الاثنين هو التوصية الشائعة للبيانات التي لا يمكن التفريط بها.
القاعدة هنا بسيطة: بيانات الكاش القابلة لإعادة البناء تختلف عن بيانات لا يمكن فقدانها. حدّد لكل نوع سياسته، وفضّل فصلها في نسخ Redis مختلفة إن أمكن.
إدارة الذاكرة
Redis يعتمد على RAM، وهذا يعني أن الذاكرة مورد محدود ومكلف. تخزين 300,000 نموذج Eloquent في الكاش قد يستهلك مئات الميغابايتات وقد يؤدي إلى فشل الطلب أصلًا قبل اكتمال التخزين.
حدّد سقفًا وسياسة إزالة
# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru| السياسة | السلوك عند امتلاء الذاكرة |
|---|---|
noeviction | ترفض عمليات الكتابة الجديدة (الافتراضي) |
allkeys-lru | تزيل الأقل استخدامًا مؤخرًا من كل المفاتيح — الأنسب للكاش |
allkeys-lfu | تزيل الأقل تكرارًا في الاستخدام |
volatile-lru | تزيل الأقل استخدامًا من المفاتيح التي لها مدة انتهاء فقط |
volatile-ttl | تزيل المفاتيح الأقرب إلى الانتهاء أولًا |
مهم: إذا كنت تستخدم نسخة Redis واحدة للكاش والطوابير معًا، فإن سياسة
allkeys-lruقد تحذف مهام لم تُعالَج بعد. هذا سبب إضافي قوي لفصل الاستخدامين.
ممارسات تقلّل استهلاك الذاكرة
- خزّن ما يُطلب بشكل متكرر فقط، لا كل ما يمكن تخزينه.
- حدّد TTL لكل مفتاح ما لم يكن هناك سبب واضح لعكس ذلك.
- خزّن الحقول المطلوبة فقط بدل النموذج الكامل بعلاقاته.
- استخدم Pagination وChunking بدل تخزين مجموعات ضخمة.
- راقب
INFO memoryوحلّل المفاتيح الكبيرة عبر RedisInsight.
أخطاء شائعة يجب تجنّبها
- التخزين المؤقت لإخفاء استعلام سيّئ بدل إصلاحه بإضافة Index أو معالجة مشكلة N+1.
- مفاتيح بلا TTL تتراكم إلى أن تمتلئ الذاكرة.
- غياب استراتيجية إبطال، فتظهر بيانات قديمة للمستخدم دون تفسير.
- استخدام
KEYS *أوFLUSHALLفي الإنتاج. - خلط الكاش والطوابير والجلسات في قاعدة واحدة.
- تخزين بيانات خاصة بكل مستخدم بمفتاح عام، وهو خطأ أمني يسرّب بيانات مستخدم إلى آخر — أضف دائمًا معرّف المستخدم إلى المفتاح.
- تخزين مجموعات ضخمة لمجرد تجنّب استعلام واحد.
- افتراض أن Redis لن يتعطّل أبدًا؛ يجب أن يظل التطبيق قادرًا على العمل — ولو أبطأ — عند سقوطه.
قائمة تحقق قبل الإنتاج
- تفعيل
requirepassوعدم تعريض المنفذ6379للإنترنت. - ضبط
maxmemoryوmaxmemory-policyبما يناسب طبيعة الاستخدام. - فصل قواعد الكاش والطوابير والجلسات.
- اختيار سياسة Persistence المناسبة لكل نوع بيانات.
- مراقبة استهلاك الذاكرة ونسبة الإصابة (Hit Rate) وعدد الاتصالات.
- إدارة العمّال عبر Supervisor أو Horizon مع
queue:restartبعد كل نشر. - التعامل مع فشل الاتصال بـ Redis بشكل لا يُسقط التطبيق بالكامل.
الخلاصة
Redis ليس مجرد نظام كاش، بل أداة متعددة الأغراض يمكن أن تصبح جزءًا أساسيًا من بنية تطبيق Laravel:
| الاستخدام | الفائدة |
|---|---|
| Cache | تقليل الاستعلامات على قاعدة البيانات وزمن الاستجابة |
| Queues | معالجة المهام الثقيلة في الخلفية |
| Counters | عدّادات الزيارات والأحداث بعمليات ذرّية سريعة |
| Lists | تخزين ومعالجة البيانات بترتيب محدد |
| Sets | تخزين القيم الفريدة وعمليات التقاطع والاتحاد |
| Sorted Sets | الترتيب حسب Score ولوحات الصدارة |
| Hashes | تخزين الكائنات بحقولها وتحديث حقل واحد بكفاءة |
| Pipelines | تقليل تكلفة تنفيذ عدد كبير من الأوامر |
| Transactions | تنفيذ مجموعة أوامر بشكل ذرّي |
| Locks & Rate Limiting | ضبط التزامن وحماية الموارد |
لكن الاستخدام الصحيح لا يعني تخزين أكبر قدر ممكن من البيانات، بل يعني اختيار البيانات المناسبة، وتحديد مدة تخزين مناسبة، وإدارة إبطال الكاش واستهلاك الذاكرة بعناية.
حين يُستخدم Redis بهذه الطريقة، فإنه يخفّف الضغط عن قاعدة البيانات ويحسّن زمن الاستجابة بشكل ملموس — خصوصًا في التطبيقات ذات البيانات المتكررة أو العمليات المكلفة التي تتكرر بلا داعٍ.
الموقع أكثر من رائع. في حال الإضافة، هل يمكن تحديث الكاش ؟ أم يجب مسح الكاش وإعادة تحميلها؟ مع الشكر
شكرًا لك على كلامك الطيب 🙏
سؤال ممتاز، والجواب أن الطريقتين صحيحتان، والاختيار بينهما يعتمد على نوع المفتاح.
1. التحديث المباشر — Write-Through
مناسب للمفاتيح المفردة التي تعرف قيمتها الجديدة فورًا، ويمنع حدوث فجوة يقرأ فيها المستخدمون من قاعدة البيانات:
2. المسح وإعادة البناء عند الطلب — Invalidation
أبسط وأأمن، خصوصًا للمفاتيح المشتقّة أو المجمّعة التي لا يمكن حساب قيمتها الجديدة بسهولة، مثل القوائم والعدادات والإحصائيات وصفحات الـ Pagination:
أما في حالة الإضافة تحديدًا
المنتج الجديد لا يوجد له مفتاح أصلًا، لذا لا شيء يُحدَّث. لكن يجب إبطال كل مفتاح تأثّر بوجوده، مثل
products:allوproducts:latestوعدد النتائج. وهنا تفيد الوسوم: