Redis والتخزين المؤقت المتقدم

أنواع بيانات Redis: القوائم والمجموعات

20 دقيقة الدرس 4 من 30

قوائم Redis

قوائم Redis هي مجموعات مرتبة من السلاسل، تم تنفيذها كقوائم مرتبطة. هذا يعني أن إضافة عناصر إلى الرأس أو الذيل سريعة للغاية (O(1))، لكن الوصول إلى العناصر بواسطة الفهرس أبطأ (O(N)). القوائم مثالية لقوائم الانتظار وموجزات النشاط وتتبع العناصر الحديثة.

التنفيذ: يتم تنفيذ قوائم Redis كقوائم مرتبطة مزدوجة، وليست مصفوفات. هذا يجعل عمليات الدفع/السحب عند كلا الطرفين فورية، لكن الوصول العشوائي أبطأ من المصفوفات.

LPUSH و RPUSH - إضافة عناصر

إضافة عناصر إلى اليسار (الرأس) أو اليمين (الذيل) من القائمة:

// LPUSH - الدفع إلى اليسار (الرأس)\n127.0.0.1:6379> LPUSH notifications "New message"\n(integer) 1\n\n127.0.0.1:6379> LPUSH notifications "Friend request"\n(integer) 2\n\n// القائمة الآن: ["Friend request", "New message"]\n\n// RPUSH - الدفع إلى اليمين (الذيل)\n127.0.0.1:6379> RPUSH tasks "Task 1"\n(integer) 1\n\n127.0.0.1:6379> RPUSH tasks "Task 2" "Task 3"\n(integer) 3\n\n// القائمة الآن: ["Task 1", "Task 2", "Task 3"]\n\n// دفع قيم متعددة في وقت واحد\n127.0.0.1:6379> LPUSH queue "item1" "item2" "item3"\n(integer) 3
// أمثلة Laravel\n// إضافة إلى البداية (الأحدث أولاً)\nRedis::lpush('user:1000:notifications', 'New comment on your post');\n\n// إضافة إلى النهاية (قائمة انتظار FIFO)\nRedis::rpush('email:queue', json_encode([\n 'to' => 'user@example.com',\n 'subject' => 'Welcome!',\n 'body' => 'Thanks for signing up'\n]));

LPOP و RPOP - إزالة عناصر

إزالة وإرجاع عناصر من اليسار (الرأس) أو اليمين (الذيل):

// LPOP - السحب من اليسار (الرأس)\n127.0.0.1:6379> RPUSH queue "first" "second" "third"\n(integer) 3\n\n127.0.0.1:6379> LPOP queue\n"first"\n\n127.0.0.1:6379> LPOP queue\n"second"\n\n// RPOP - السحب من اليمين (الذيل)\n127.0.0.1:6379> RPOP queue\n"third"\n\n// السحب من قائمة فارغة يرجع nil\n127.0.0.1:6379> LPOP queue\n(nil)\n\n// سحب عناصر متعددة في وقت واحد (Redis 6.2+)\n127.0.0.1:6379> LPOP queue 3\n1) "item1"\n2) "item2"\n3) "item3"
// معالجة قائمة انتظار Laravel\nwhile ($job = Redis::lpop('jobs:pending')) {\n $data = json_decode($job, true);\n $this->processJob($data);\n}
أنماط قائمة الانتظار:
  • قائمة انتظار FIFO: RPUSH (إضافة) + LPOP (معالجة)
  • مكدس LIFO: LPUSH (إضافة) + LPOP (معالجة)
  • العناصر الحديثة: LPUSH (إضافة) + LRANGE 0 9 (الحصول على آخر 10)

LRANGE - استرجاع عناصر القائمة

الحصول على نطاق من العناصر من قائمة. الفهارس تبدأ من الصفر، والفهارس السالبة تعد من النهاية:

// إنشاء قائمة\n127.0.0.1:6379> RPUSH fruits "apple" "banana" "cherry" "date" "elderberry"\n(integer) 5\n\n// LRANGE - الحصول على نطاق من العناصر\n127.0.0.1:6379> LRANGE fruits 0 2\n1) "apple"\n2) "banana"\n3) "cherry"\n\n// الحصول على جميع العناصر (من 0 إلى -1)\n127.0.0.1:6379> LRANGE fruits 0 -1\n1) "apple"\n2) "banana"\n3) "cherry"\n4) "date"\n5) "elderberry"\n\n// الحصول على آخر عنصرين\n127.0.0.1:6379> LRANGE fruits -2 -1\n1) "date"\n2) "elderberry"\n\n// الحصول على العنصر الأول فقط\n127.0.0.1:6379> LRANGE fruits 0 0\n1) "apple"
// Laravel - الحصول على موجز النشاط الأخير (آخر 20 عنصراً)\n$activities = Redis::lrange('user:1000:activity', 0, 19);\n\nforeach ($activities as $activity) {\n $data = json_decode($activity, true);\n echo "{$data['action']} at {$data['timestamp']}<br>";\n}\n\n// ترقيم الصفحات مع القوائم\n$page = 2;\n$perPage = 10;\n$start = ($page - 1) * $perPage;\n$end = $start + $perPage - 1;\n\n$items = Redis::lrange('products:featured', $start, $end);

LLEN - طول القائمة

الحصول على عدد العناصر في قائمة:

127.0.0.1:6379> RPUSH mylist "a" "b" "c"\n(integer) 3\n\n127.0.0.1:6379> LLEN mylist\n(integer) 3\n\n// قائمة فارغة أو غير موجودة ترجع 0\n127.0.0.1:6379> LLEN nonexistent\n(integer) 0
// Laravel - التحقق من حجم قائمة الانتظار\n$queueSize = Redis::llen('jobs:pending');\n\nif ($queueSize > 1000) {\n // تنبيه: قائمة الانتظار تنمو بشكل كبير جداً\n Log::warning("تجاوز حجم قائمة الانتظار العتبة: {$queueSize}");\n}

تقليم القائمة والصيانة

احتفظ بالقوائم في حجم يمكن التحكم فيه عن طريق تقليم العناصر القديمة:

// LTRIM - تقليم القائمة إلى النطاق المحدد\n127.0.0.1:6379> RPUSH logs "log1" "log2" "log3" "log4" "log5"\n(integer) 5\n\n// الاحتفاظ بأول 3 عناصر فقط\n127.0.0.1:6379> LTRIM logs 0 2\nOK\n\n127.0.0.1:6379> LRANGE logs 0 -1\n1) "log1"\n2) "log2"\n3) "log3"
// Laravel - الاحتفاظ بآخر 100 إشعار فقط\nRedis::lpush('user:1000:notifications', $newNotification);\nRedis::ltrim('user:1000:notifications', 0, 99); // الاحتفاظ بـ 0-99 (100 عنصر)\n\n// بديل: الإزالة بعد الإضافة\n$maxSize = 50;\nRedis::lpush('activity_feed', $activity);\n\nif (Redis::llen('activity_feed') > $maxSize) {\n Redis::rpop('activity_feed'); // إزالة الأقدم\n}

مجموعات Redis

مجموعات Redis هي مجموعات غير مرتبة من السلاسل الفريدة. المجموعات مثالية للعلامات وتتبع الزوار الفريدين وعمليات المجموعة مثل الاتحادات والتقاطعات.

التفرد: المجموعات تتعامل تلقائياً مع التفرد - إضافة نفس العنصر عدة مرات ليس له تأثير. تستخدم المجموعات جداول تجزئة داخلياً، مما يوفر O(1) للإضافة والإزالة والتحقق من العضوية.

SADD و SMEMBERS - عمليات المجموعة الأساسية

// SADD - إضافة أعضاء إلى المجموعة\n127.0.0.1:6379> SADD tags "php" "laravel" "redis"\n(integer) 3\n\n// إضافة مكرر ليس له تأثير\n127.0.0.1:6379> SADD tags "php"\n(integer) 0 // 0 = لم تتم إضافة عناصر جديدة\n\n// SMEMBERS - الحصول على جميع الأعضاء (غير مرتبة!)\n127.0.0.1:6379> SMEMBERS tags\n1) "redis"\n2) "php"\n3) "laravel"\n\n// SCARD - الحصول على حجم المجموعة\n127.0.0.1:6379> SCARD tags\n(integer) 3
// أمثلة Laravel\n// تتبع الزوار الفريدين\nRedis::sadd('page:home:visitors:' . date('Y-m-d'), $userId);\n\n// الحصول على عدد الزوار الفريدين\n$uniqueVisitors = Redis::scard('page:home:visitors:' . date('Y-m-d'));\n\n// إضافة علامات متعددة إلى المنشور\nRedis::sadd('post:1000:tags', 'php', 'tutorial', 'beginner');

SISMEMBER - التحقق من العضوية

التحقق مما إذا كان العنصر موجوداً في مجموعة - عملية سريعة للغاية O(1):

127.0.0.1:6379> SADD premium_users "1000" "2000" "3000"\n(integer) 3\n\n// SISMEMBER - التحقق مما إذا كان العضو موجوداً\n127.0.0.1:6379> SISMEMBER premium_users "1000"\n(integer) 1 // 1 = موجود\n\n127.0.0.1:6379> SISMEMBER premium_users "9999"\n(integer) 0 // 0 = غير موجود
// Laravel - التحقق مما إذا كان المستخدم مميزاً\nif (Redis::sismember('premium_users', $userId)) {\n // منح الوصول إلى الميزات المميزة\n return view('dashboard.premium');\n}\n\n// التحقق مما إذا كان عنوان IP محظوراً\nif (Redis::sismember('blocked_ips', $request->ip())) {\n abort(403, 'تم رفض الوصول');\n}

SREM - إزالة أعضاء

// SREM - إزالة أعضاء من المجموعة\n127.0.0.1:6379> SADD colors "red" "green" "blue" "yellow"\n(integer) 4\n\n127.0.0.1:6379> SREM colors "green"\n(integer) 1 // 1 = تمت إزالة العضو\n\n127.0.0.1:6379> SREM colors "green"\n(integer) 0 // 0 = العضو لم يكن موجوداً\n\n// إزالة أعضاء متعددين\n127.0.0.1:6379> SREM colors "red" "blue"\n(integer) 2
// Laravel - إزالة مستخدم من المجموعة\nRedis::srem('online_users', $userId);\n\n// إزالة الجلسات المنتهية الصلاحية\n$expiredSessions = Session::where('expires_at', '<', now())->pluck('id');\nRedis::srem('active_sessions', ...$expiredSessions);

SUNION - اتحاد المجموعات

دمج مجموعات متعددة وإرجاع جميع العناصر الفريدة:

// إنشاء مجموعات\n127.0.0.1:6379> SADD skills:user1 "php" "javascript" "mysql"\n(integer) 3\n\n127.0.0.1:6379> SADD skills:user2 "javascript" "python" "redis"\n(integer) 3\n\n// SUNION - الحصول على اتحاد المجموعات\n127.0.0.1:6379> SUNION skills:user1 skills:user2\n1) "php"\n2) "javascript"\n3) "mysql"\n4) "python"\n5) "redis"\n\n// SUNIONSTORE - تخزين النتيجة في مجموعة جديدة\n127.0.0.1:6379> SUNIONSTORE all_skills skills:user1 skills:user2\n(integer) 5
// Laravel - العثور على جميع المستخدمين الذين أعجبوا بأي من هذه المنشورات\n$postIds = [100, 101, 102];\n$likeKeys = array_map(fn($id) => "post:{$id}:likes", $postIds);\n\n$allLikers = Redis::sunion(...$likeKeys);\necho "إجمالي المستخدمين الفريدين الذين أعجبوا: " . count($allLikers);

SINTER - تقاطع المجموعات

العثور على العناصر المشتركة عبر مجموعات متعددة:

// SINTER - الحصول على التقاطع (العناصر المشتركة)\n127.0.0.1:6379> SADD languages:project1 "php" "javascript" "html"\n(integer) 3\n\n127.0.0.1:6379> SADD languages:project2 "javascript" "python" "html"\n(integer) 3\n\n127.0.0.1:6379> SINTER languages:project1 languages:project2\n1) "javascript"\n2) "html"\n\n// SINTERSTORE - تخزين النتيجة\n127.0.0.1:6379> SINTERSTORE common_languages languages:project1 languages:project2\n(integer) 2
// Laravel - العثور على المستخدمين في مجموعات متعددة\n$group1Members = Redis::smembers('group:admins');\n$group2Members = Redis::smembers('group:moderators');\n\n// المستخدمون الذين هم مسؤولون ومشرفون\n$superUsers = Redis::sinter('group:admins', 'group:moderators');\n\n// العثور على منتجات بعلامات متعددة\n$phpProducts = Redis::sinter('tag:php', 'tag:tutorial', 'tag:beginner');

SDIFF - فرق المجموعات

العثور على العناصر في مجموعة واحدة غير موجودة في مجموعات أخرى:

// SDIFF - الحصول على الفرق (العناصر في الأولى وليست في الأخريات)\n127.0.0.1:6379> SADD set1 "a" "b" "c" "d"\n(integer) 4\n\n127.0.0.1:6379> SADD set2 "b" "d"\n(integer) 2\n\n127.0.0.1:6379> SDIFF set1 set2\n1) "a"\n2) "c"\n\n// SDIFFSTORE - تخزين النتيجة\n127.0.0.1:6379> SDIFFSTORE result set1 set2\n(integer) 2
// Laravel - العثور على المستخدمين الذين لم يكملوا البرنامج التعليمي\n$allUsers = Redis::smembers('users:registered');\n$completedUsers = Redis::smembers('users:completed_tutorial');\n\n// المستخدمون الذين يحتاجون إلى إكمال البرنامج التعليمي\n$pendingUsers = Redis::sdiff('users:registered', 'users:completed_tutorial');\n\n// العثور على المنشورات التي لم يقرأها المستخدم بعد\n$allPosts = Redis::smembers('posts:published');\n$readPosts = Redis::smembers('user:1000:read_posts');\n$unreadPosts = Redis::sdiff('posts:published', 'user:1000:read_posts');

حالات استخدام عملية

// 1. نظام علامات مع المجموعات\nclass PostTagService {\n public function addTags($postId, array $tags) {\n Redis::sadd("post:{$postId}:tags", ...$tags);\n \n foreach ($tags as $tag) {\n Redis::sadd("tag:{$tag}:posts", $postId);\n }\n }\n \n public function getPostsByTag($tag) {\n return Redis::smembers("tag:{$tag}:posts");\n }\n \n public function getCommonTags($postId1, $postId2) {\n return Redis::sinter("post:{$postId1}:tags", "post:{$postId2}:tags");\n }\n}\n\n// 2. موجز النشاط مع القوائم\nclass ActivityFeed {\n public function addActivity($userId, $activity) {\n $data = json_encode([\n 'action' => $activity,\n 'timestamp' => now()->toIso8601String()\n ]);\n \n Redis::lpush("user:{$userId}:feed", $data);\n Redis::ltrim("user:{$userId}:feed", 0, 49); // الاحتفاظ بآخر 50\n }\n \n public function getFeed($userId, $limit = 20) {\n $feed = Redis::lrange("user:{$userId}:feed", 0, $limit - 1);\n return array_map(fn($item) => json_decode($item, true), $feed);\n }\n}\n\n// 3. تتبع المستخدمين المتصلين مع المجموعات\nclass OnlineUsers {\n public function markOnline($userId) {\n Redis::sadd('users:online', $userId);\n Redis::setex("user:{$userId}:last_seen", 300, time());\n }\n \n public function getOnlineCount() {\n return Redis::scard('users:online');\n }\n \n public function isOnline($userId) {\n return Redis::sismember('users:online', $userId);\n }\n}
اعتبارات الأداء:
  • القوائم: O(N) لـ LRANGE - لا تحصل على نطاقات ضخمة
  • المجموعات: SMEMBERS هي O(N) - تجنب على مجموعات كبيرة جداً (>10K عضو)
  • عمليات المجموعة (SUNION، SINTER) يمكن أن تكون بطيئة على المجموعات الكبيرة
  • استخدم SSCAN بدلاً من SMEMBERS للمجموعات الكبيرة
تمرين: أنشئ نظام ميزات شبكة اجتماعية:
  1. نفذ "المتابعة" باستخدام المجموعات (user:X:following، user:X:followers)
  2. أنشئ دالة للمتابعة/إلغاء المتابعة للمستخدمين
  3. نفذ الأصدقاء المشتركين (المستخدمون الذين يتابعون بعضهم البعض)
  4. أنشئ موجز نشاط باستخدام القوائم يخزن آخر 50 نشاطاً
  5. أنشئ نظام علامات حيث يمكن أن يكون للمنشورات علامات متعددة
  6. نفذ "الأصدقاء المقترحين" (متابعو متابعيك)

في الدرس التالي، سنستكشف هاشات Redis والمجموعات المرتبة - بنيات بيانات متقدمة لحالات استخدام أكثر تعقيداً.