Database Engineering

تسلط بر ZSET ردیس: ساخت جدول‌های امتیازدهی مقیاس‌پذیر و بلادرنگ

در دنیای بازی‌های رقابتی، تعاملات شبکه‌های اجتماعی و شرط‌بندی ورزشی زنده، توانایی نمایش رتبه‌بندی‌های دقیق و بلادرنگ تنها یک ویژگی نیست، بلکه یک نیاز اساسی محصول است. در حالی که پایگاه‌های داده رابطه‌ای سنتی مانند PostgreSQL یا MySQL در مدیریت بار محاسباتی مربوط به محاسبه و مرتب‌سازی رتبه‌ها برای میلیون‌ها رکورد با مشکل مواجه‌اند، ردیس به دلیل معماری حافظه‌محور (in-memory) و ساختارهای داده تخصصی خود در این حوزه درخشان عمل می‌کند. به طور خاص، مجموعه مرتب (ZSET) استاندارد طلایی برای پیاده‌سازی جدول‌های امتیازدهی است. این پست به بررسی نحوه معماری سیستم‌های رتبه‌بندی با عملکرد بالا با استفاده از ردیس می‌پردازد و بر بهینه‌سازی عملیات ZSET برای به‌روزرسانی‌های با فرکانس بالا تمرکز دارد.

چرا ZSET موتور رتبه‌بندی بلادرنگ است

در هسته خود، یک ZSET ردیس مجموعه‌ای از اعضای رشته‌ای یکتا است که هر کدام با یک امتیاز اعشاری (floating-point) مرتبط هستند. جادوی کار در نحوه نگهداری این ساختار توسط ردیس نهفته است. در سطح داخلی، ردیس از ترکیبی از جداول هش برای جستجوی O(1) بر اساس عضو و لیست‌های پرش‌دار (skip lists) برای تکرار مرتب‌شده بر اساس امتیاز استفاده می‌کند. این رویکرد دو ساختاری اجازه می‌دهد:

  • پیچیدگی O(log(N)) برای افزودن اعضا و به‌روزرسانی امتیازات.
  • پیچیدگی O(log(N)) برای یافتن رتبه یک عضو.
  • پیچیدگی O(M + log(N)) برای بازیابی یک محدوده از اعضا (که در آن M تعداد آیتم‌های برگشتی است).

برای یک جدول امتیازدهی با ۱۰ میلیون کاربر، این پیچیدگی‌ها تضمین می‌کنند که افزودن یک امتیاز جدید یا دریافت ۱۰۰ بازیکن برتر، صرف‌نظر از اندازه کل پایگاه کاربران، آنی انجام می‌شود.

عملیات‌های اصلی و پیاده‌سازی کد

پیاده‌سازی یک جدول امتیازدهی شامل سه عملیات اصلی است: به‌روزرسانی امتیاز کاربر، دریافت رتبه کاربر و دریافت N بازیکن برتر. در زیر یک مثال عملی با استفاده از پایتون و کتابخانه کلاینت redis-py آورده شده است. فرض می‌کنیم سناریویی داریم که نیاز به به‌روزرسانی امتیاز یک بازیکن در بازی «SpaceInvaders» داریم.

import 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"Updated {player_id} to score {score}")

# 2. دریافت رتبه جهانی
# RANK رتبه عضو را با صفر به عنوان اولین رتبه برمی‌گرداند.
# 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:
        # تبدیل ایندکس ۰-پایه به رتبه ۱-پایه
        return {"rank": rank + 1, "score": score}
    return {"rank": None, "score": None}

# 3. دریافت ۱۰ بازیکن برتر
# 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

تأخیر شبکه اغلب گلوگاه در سیستم‌های توزیع‌شده است. به جای ارسال دستورات ZADD جداگانه برای به‌روزرسانی هر بازیکن، از 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("Batch update completed successfully")

2. انتخاب نوع امتیاز مناسب

به طور پیش‌فرض، ردیس از اعداد اعشاری با دقت دوگانه استفاده می‌کند. با این حال، اگر امتیازهای شما اعداد صحیح هستند، گاهی اوقات می‌توانید با نرمال‌سازی آن‌ها بهره‌وری عملکرد و حافظه کمی کسب کنید. مهم‌تر از آن، مطمئن شوید که نحوه مدیریت تساوی‌ها را درک می‌کنید. ردیس تساوی‌ها را به صورت لغوی بر اساس نام عضو حل می‌کند. اگر نیاز به شکستن تساوی دقیق بر اساس زمان دارید، در نظر بگیرید که یک مهره زمانی یا یک شماره دنباله یکتا را به امتیاز اضافه کنید (مثلاً base_score * 1000000 + timestamp). این تضمین می‌کند که اگر امتیازها برابر باشند، اقدامات جدیدتر رتبه بالاتری دریافت کنند.

3. مدیریت حافظه و حذف

جدول‌های امتیازدهی می‌توانند به طور نامحدود رشد کنند. اگر مکانیسمی برای کوتاه کردن لیست نداشته باشید، استفاده از حافظه ردیس شما به صورت خطی با تعداد بازیکنان یکتا رشد خواهد کرد. یک استراتژی پاکسازی را با استفاده از ZREMRANGEBYRANK پیاده‌سازی کنید تا بازیکنانی که مدت‌هاست امتیازات خود را به‌روز نکرده‌اند را حذف کنید یا اندازه مجموعه را در یک حد قابل مدیریت نگه دارید (مثلاً نگه داشتن ۱۰,۰۰۰ بازیکن برتر و حذف بقیه).

نتیجه‌گیری

ZSETهای ردیس پایه‌ای قوی و با عملکرد بالا برای ساخت جدول‌های امتیازدهی بلادرنگ فراهم می‌کنند. با بهره‌گیری از کارایی ساختارهای داده حافظه‌محور و بهینه‌سازی الگوهای نوشتن از طریق دسته‌بندی، توسعه‌دهندگان می‌توانند با تأخیر حداقل از میلیون‌ها کاربر همزمان پشتیبانی کنند. هنگام طراحی سیستم‌های رتبه‌بندی خود، همیشه مورد استفاده خاص خود را پروفایل کنید و عواملی مانند قوانین شکستن تساوی و محدودیت‌های حافظه را در نظر بگیرید تا اطمینان حاصل کنید که راه‌حل شما تحت بار کاری به خوبی مقیاس‌پذیر است.

Share: