Database Engineering

إتقان مجموعات Redis المرتبة (ZSET): بناء لوحات متصدرين قابلة للتوسع في الوقت الفعلي

في عالم الألعاب التنافسية، وتفاعل وسائل التواصل الاجتماعي، والمراهنات الرياضية المباشرة، لا يعد عرض التصنيفات الدقيقة والفورية مجرد ميزة، بل هو مطلب أساسي للمنتج. بينما تواجه قواعد البيانات العلائقية التقليدية مثل PostgreSQL أو MySQL صعوبة في التعامل مع الحمل الحسابي لحساب وترتيب التصنيفات لملايين السجلات، تتفوق Redis في هذا المجال بفضل بنيتها القائمة على الذاكرة (In-memory) وهياكل البيانات المتخصصة. وتحديداً، تُعد المجموعة المرتبة (Sorted Set أو ZSET) المعيار الذهبي لتنفيذ لوحات المتصدرين. يستكشف هذا المنشور كيفية هندسة أنظمة تصنيف عالية الأداء باستخدام Redis، مع التركيز على تحسين عمليات ZSET للتحديثات عالية التكرار.

لماذا يُعد ZSET محرك التصنيف في الوقت الفعلي

في جوهره، تتكون مجموعة Redis ZSET من مجموعة من الأعضاء (Members) النصيين الفريدين، يرتبط كل منها بنقطة عائمة (Floating-point score). يكمن السحر في كيفية الحفاظ على هذا الهيكل داخلياً. تستخدم Redis داخلياً مزيجاً من جداول التجزئة (Hash tables) لإجراء عمليات البحث بسرعة O(1) بناءً على العضو، وقوائم التخطي (Skip lists) للتكرار المرتب بناءً على النقاط. يسمح هذا النهج ذو الهيكل المزدوج بما يلي:

  • تعقيد O(log(N)) لإضافة الأعضاء وتحديث النقاط.
  • تعقيد O(log(N)) للعثور على ترتيب العضو.
  • تعقيد O(M + log(N)) لاسترداد نطاق من الأعضاء (حيث M هو عدد العناصر المُرجعة).

بالنسبة إلى لوحة متصدرين تحتوي على 10 ملايين مستخدم، تضمن هذه التعقيدات أن إضافة نقطة جديدة أو جلب أفضل 100 لاعب يظل فورياً، بغض النظر عن حجم قاعدة المستخدمين الإجمالي.

العمليات الأساسية والتنفيذ البرمجي

يتضمن تنفيذ لوحة المتصدرين ثلاث عمليات رئيسية: تحديث نقطة المستخدم، واسترداد ترتيب المستخدم، وجلب أفضل N لاعباً. فيما يلي مثال عملي باستخدام لغة Python ومكتبة العميل redis-py. نفترض سيناريو نحتاج فيه إلى تحديث نقطة لاعب في لعبة تسمى "SpaceInvaders".

import redis

# الاتصال بنسخة Redis
client = redis.Redis(host='localhost', port=6379, decode_responses=True)

# 1. تحديث النقاط
# تضيف ZADD عضواً إلى المجموعة المرتبة بنقطة محددة.
# إذا كان العضو موجوداً بالفعل، تقوم بتحديث النقطة.
def update_leaderboard(player_id, score):
    # استخدام علم NX للإضافة فقط إذا لم يكن موجوداً، أو XX للتحديث إذا كان موجوداً
    # هنا نقوم ببساطة بتحديث/إدخال النقطة
    client.zadd("space_invaders_rankings", {player_id: score})
    print(f"تم تحديث {player_id} إلى النقطة {score}")

# 2. الحصول على الترتيب العالمي
# يعيد RANK ترتيب العضو، حيث يكون 0 هو الأول.
# يعيد WITHSCORES النقطة أيضاً.
def get_player_rank(player_id):
    rank = client.zrank("space_invaders_rankings", player_id)
    score = client.zscore("space_invaders_rankings", player_id)
    if rank is not None:
        # تحويل الفهرس من 0 إلى ترتيب يبدأ من 1
        return {"rank": rank + 1, "score": score}
    return {"rank": None, "score": None}

# 3. الحصول على أفضل 10 لاعبين
# تسترجع ZREVRANGE الأعضاء بترتيب تنازلي للنقاط
def get_top_10():
    top_players = client.zrevrange("space_invaders_rankings", 0, 9, withscores=True)
    return top_players

# مثال على التنفيذ
update_leaderboard("user_123", 1500.5)
print(get_player_rank("user_123"))
print(get_top_10())

التحسين للتحديثات عالية التكرار

بينما تكون عمليات ZADD الفردية سريعة، يمكن أن تولد البيئات عالية التكرار (مثل عروض البيع المفاجئة أو نهائيات الألعاب الإلكترونية المباشرة) آلاف عمليات الكتابة في الثانية. لتحسين هذه السيناريوهات، ضع في الاعتبار الاستراتيجيات التالية:

1. التحديثات المجمعة باستخدام PIPELINE

غالباً ما تكون زمن انتقال الشبكة (Network latency) هو الاختناق في الأنظمة الموزعة. بدلاً من إرسال أوامر ZADD فردية لتحديث كل لاعب، استخدم تسلسل أوامر Redis (Pipelining). يسمح هذا للعميل بإرسال عدة أوامر إلى الخادم دون انتظار استجابة كل أمر منها، مما يزيد بشكل كبير من الإنتاجية.

def batch_update_scores(updates_dict):
    # updates_dict = {'user_A': 100, 'user_B': 200, ...}
    pipe = client.pipeline()
    pipe.zadd("space_invaders_rankings", updates_dict)
    pipe.execute()
    print("تم إكمال التحديث المجمّع بنجاح")

2. اختيار نوع النقطة المناسب

تستخدم Redis بشكل افتراضي أرقاماً عائمة بدقة مزدوجة. ومع ذلك، إذا كانت نقاطك أعداداً صحيحة، يمكنك أحياناً تحقيق كفاءة طفيفة في الأداء والذاكرة عن طريق تطبيعها. والأهم من ذلك، تأكد من فهم كيفية التعامل مع التعادل. تكسر Redis التعادل أبجدياً بناءً على اسم العضو. إذا كنت بحاجة إلى كسر تعادل دقيق يعتمد على الوقت، ففكر في إضافة طابع زمني أو رقم تسلسلي فريد إلى النقطة (على سبيل المثال، base_score * 1000000 + timestamp). يضمن ذلك أن تكون الإجراءات الأحدث مصنفة أعلى إذا كانت النقاط متساوية.

3. إدارة الذاكرة والإفراغ

يمكن أن تنمو لوحات المتصدرين إلى ما لا نهاية. إذا لم يكن لديك آلية لقص القائمة، فسيستمر استخدام ذاكرة Redis في النمو خطياً مع عدد اللاعبين الفريدين. نفذ استراتيجية تنظيف باستخدام ZREMRANGEBYRANK لإزالة اللاعبين الذين لم يقوموا بتحديث نقاطهم منذ فترة طويلة، أو للحفاظ على حجم المجموعة ضمن حد قابل للإدارة (على سبيل المثال، الاحتفاظ بأفضل 10,000 لاعب وإزالة البقية).

الخاتمة

توفر مجموعات Redis ZSET أساساً قوياً وعالي الأداء لبناء لوحات متصدرين في الوقت الفعلي. من خلال الاستفادة من كفاءة هياكل البيانات الموجودة في الذاكرة وتحسين أنماط الكتابة من خلال التجميع، يمكن للمطورين دعم ملايين المستخدمين المتزامنين بأقل زمن انتقال ممكن. أثناء تصميم أنظمة التصنيف الخاصة بك، قم دائماً بتقييم حالتك الاستخدامية المحددة، مع مراعاة عوامل مثل قواعد كسر التعادل وحدود الذاكرة، لضمان قابلية حلك للتوسع بسلاسة تحت الضغط.

Share: