ملخّص المقال: شرح عملي لكيفية استخدام 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-server

macOS

brew install redis
brew services start redis

Docker

خيار عملي ومناسب لبيئات التطوير لأنه يعزل نسخة 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 100

RedisInsight

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 بالكامل في سطر واحد، والفكرة:

  1. يبحث Laravel عن المفتاح في الكاش.
  2. إذا وجد البيانات، يعيدها مباشرة.
  3. إذا لم يجدها، ينفّذ الـ Closure.
  4. يخزّن النتيجة في الكاش.
  5. يعيد النتيجة.
$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 SendWelcomeEmail
namespace 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');   // PONG

EXISTS — التحقق من وجود مفتاح

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 بهذه الطريقة، فإنه يخفّف الضغط عن قاعدة البيانات ويحسّن زمن الاستجابة بشكل ملموس — خصوصًا في التطبيقات ذات البيانات المتكررة أو العمليات المكلفة التي تتكرر بلا داعٍ.