بعضی وقتا زمانی که در یک جای تاریک هستید فکر میکنید دفن شدهاید اما در واقع شما کاشته شدهاید
طراحی مکانیزم امتیازدهی بازی با Redis (Scoring)
قبل از هر چیز یک اعتراف صادقانه:
این سناریو لزوماً از دل یک بازی واقعی نیامده. بیشتر یک مدل ذهنی است برای اینکه بفهمیم Redis چطور فکر میکند و چطور میشود از آن برای ساخت مکانیزمهای بازی استفاده کرد.
فرض کنیم یک بازی داریم به اسم Super Redis Brothers؛
یک بازی پلتفرمر آنلاین که به یک سرور مرکزی وصل است. شخصیت اصلی بازی، Redisman، باید سکه جمع کند، با سکهها power-up بگیرد و زنده بماند.
تمرکز ما در این مقاله فقط روی امتیازدهی (جمعکردن سکه) است.
سادهترین حالت: شمارنده با String
اگر فقط بخواهیم تعداد سکههای یک بازیکن را نگه داریم، سادهترین راه استفاده از یک counter است.
مثلاً:
INCR pID123یا اگر چند سکه با هم جمع شود:
INCRBY pID123 10کمکردن امتیاز هم با:
DECRBY pID123 5این روش:
سریع است (O(1))
ساده است
ولی بهشدت محدود
مشکل کجاست؟
به محض اینکه بخواهی:
لیدربورد بسازی
بازیکنها را با هم مقایسه کنی
رتبه بدهی
دیگر string جواب نمیدهد.
چرا Sorted Set انتخاب بهتری است؟
اینجاست که Sorted Set وارد بازی میشود.
ایده خیلی ساده است:
یک کلید داریم برای همهی امتیازها
member = شناسهی بازیکن
score = تعداد سکهها
با این مدل:
هم امتیاز هر بازیکن را داری
هم میتوانی رتبهبندی بگیری
هم لیدربورد بسازی
مدلسازی جمعکردن سکه با Sorted Set
فرض کنیم همهی امتیازها زیر این کلید ذخیره میشوند:
scoresوقتی بازیکن سکه جمع میکند:
ZINCRBY scores 10 pID1234
چند نکتهی مهم اینجا وجود دارد:
اگر بازیکن قبلاً وجود نداشته باشد، Redis از صفر شروع میکند
score صفر یک مقدار معتبر است
score منفی هم مجاز است (که بعداً به دردسرش میخوریم!)

پیچیدگی این عملیات:
O(log n)
برای بازی و آپدیتهای پرتعداد کاملاً قابل قبول
نمایش سکهها در HUD بازی
برای نمایش تعداد سکهها روی صفحهی بازی، فقط کافی است امتیاز فعلی را بگیریم:
ZSCORE scores pID1234نکتهی جالب:
ZINCRBYخودش امتیاز جدید را برمیگرداندیعنی همان پاسخ Redis میتواند مستقیم وارد HUD شود
بدون کوئری اضافه
خرجکردن سکهها (Power-Up)
فرض کنیم یک power-up داریم که:
۱۰ سکه میگیرد
دشمنها را فریز میکند
از نظر Redis:
ZINCRBY scores -10 pID1234کاملاً منطقی.
یا اگر بخواهیم سکهها را صفر کنیم (مثل Sonic وقتی ضربه میخورد):
ZADD scores 0 pID1234اینجا شاید ZADD عجیب به نظر برسد،
ولی در واقع یک upsert است:
اگر عضو باشد → امتیازش آپدیت میشود
اگر نباشد → با امتیاز صفر اضافه میشود
مشکل واقعی: امتیاز منفی!
اینجا به یک سادهسازی خطرناک میرسیم.
اگر Redisman صفر سکه داشته باشد و power-up بخورد:
ZINCRBY scores -10 pID1234نتیجه:
-10که احتمالاً در منطق بازی غلط است.
پس قبل از کمکردن امتیاز، باید چک کنیم:
آیا بازیکن بهاندازهی کافی سکه دارد یا نه؟
حل مسئله با Lua Script (اتمیک و تمیز)
منطق به زبان ساده:
امتیاز فعلی را بگیر
اگر کافی بود، کم کن
اگر نه، هیچ کاری نکن
بهصورت شبهکد:
if currentScore >= neededCoins:
decrementدر Redis، بهترین جا برای این منطق یک Lua Script است؛
چون:
اتمیک اجرا میشود
بین
ZSCOREوZINCRBYهیچ عملیات دیگری نمیپرد
اسکریپت نمونه:
EVAL "
if tonumber(redis.call('zscore',KEYS[1],ARGV[2])) >= (ARGV[1]*-1)
then
return tonumber(redis.call('zincrby',KEYS[1],ARGV[1],ARGV[2]))
end
" 1 scores -10 pID1234این اسکریپت:
اول امتیاز را میگیرد
چک میکند به زیر صفر نرود
فقط در صورت مجاز بودن، امتیاز را کم میکند
در محیط production:
معمولاً از
SCRIPT LOADوEVALSHAاستفاده میشودولی منطق دقیقاً همین است
ساخت لیدربورد با Sorted Set
اگر تا اینجا همراه بوده باشی، حالا یک نکته باید کاملاً روشن باشد:
Sorted Set تقریباً برای لیدربورد ساخته شده است.
ما همین حالا هم امتیاز هر بازیکن را بهعنوان score داخل یک Sorted Set نگه میداریم.
پس بخش بزرگی از منطق لیدربورد از قبل آماده است.
نمایش نفرات برتر (Top-N)
در بیشتر بازیها، لیدربورد از بیشترین امتیاز به کمترین نمایش داده میشود.
برای همین از ZREVRANGE استفاده میکنیم.
ZREVRANGE scores 0 9 WITHSCORESاین دستور:
۱۰ بازیکن اول را برمیگرداند
بالاترین امتیاز در ابتدای لیست است
امتیازها هم همراه اسم بازیکنها برمیگردد
نکتهی مهم:
Redis تساوی امتیازها را هم درست مدیریت میکند
ممکن است ۱۰ نفر اول، همگی امتیاز یکسان داشته باشند
ترتیب همچنان پایدار و قابل پیشبینی است
لیدربورد «اطراف من» (نه فقط Top 10)
نمایش ۱۰ نفر اول برای همه مفید نیست.
اگر کاربر تازهوارد باشد و رتبهاش مثلاً ۴۸٬۳۲۱ باشد،
دیدن Top 10 هیچ انگیزهای ایجاد نمیکند.
آنچه کاربر واقعاً میخواهد ببیند:
چند نفر بالاتر از خودش
چند نفر پایینتر از خودش
گرفتن رتبهی کاربر
اول باید بفهمیم بازیکن چندم است:
ZREVRANK scores pID1234این دستور:
رتبهی بازیکن را از بالا برمیگرداند
دقیقاً همان چیزی که برای لیدربورد لازم داریم
نمایش بازهای اطراف کاربر
حالا که رتبه را داریم، میتوانیم مثلاً:
۵ نفر بالاتر
خود کاربر
۵ نفر پایینتر
را با یک ZREVRANGE بگیریم.
ایده ساده است:
start = rank - 5
end = rank + 5و بعد:
ZREVRANGE scores start end WITHSCORESاین مدل لیدربورد:
بسیار شخصیتر است
انگیزهی رقابت ایجاد میکند
در بازیهای واقعی بسیار مؤثرتر از Top-N است
اما یک مشکل جدی در مقیاس بالا
تا اینجا همهچیز عالی به نظر میرسد،
اما این مدل یک نقطهی ضعف معماری دارد.
در تمام مثالها، ما فقط با یک کلید کار میکنیم:
scoresدر مقیاس کوچک مشکلی نیست.
اما در یک بازی واقعی با ترافیک بالا چه اتفاقی میافتد؟
مشکل Hot Key
در Redis (بهخصوص در حالت cluster):
هر کلید روی یک shard مشخص قرار میگیرد
تمام عملیات روی آن کلید، روی همان نود انجام میشود
نتیجه:
همهی
ZINCRBYهمهی
ZREVRANGEهمهی
ZREVRANK
روی یک ماشین میافتد.
حتی اگر:
دهها shard
یا چندین نود داشته باشی
تا وقتی کلید یکی است،
گلوگاه همان یک shard خواهد بود.
به این حالت میگویند: Hot Key
در یک بازی با:
جمعکردن سکهی زیاد
خرجکردن power-up
کاربران همزمان زیاد
این محدودیت واقعاً خودش را نشان میدهد.
راهحل ساده؟ نه کاملاً
یک راه این است که:
امتیاز هر بازیکن را در یک key جداگانه با
INCRنگه داریمهر چند وقت یکبار با
SCAN، یک Sorted Set بسازیم
اما این کار:
real-time بودن لیدربورد را از بین میبرد
سیستمهای جانبی و cron job میخواهد
پیچیدگی را بالا میبرد
یعنی مشکل را حل نمیکند، فقط جابجا میکند.
یک راهحل مقیاسپذیرتر (ولی خاص)
در معماریهای پیشرفتهتر، میشود از Redis بهصورت توزیعشدهتر استفاده کرد.
مثلاً در Redis Enterprise Active-Active:
چند کلاستر همرده داریم
نوشتنها بین آنها پخش میشود
دادهها بعداً با مدل CRDT همگام میشوند
در این حالت:
فشار نوشتن روی یک کلید تقسیم میشود
برای workloadهای bursty (افزایش ناگهانی ترافیک) مناسبتر است
real-time کامل نیست، ولی بسیار مقیاسپذیرتر است
این راهحل:
عمومی نیست
برای همه پروژهها هم لازم نیست
ولی دانستنش برای طراحی سیستمهای بزرگ حیاتی است
سخن پایانی
در بازیها و سامانههای بلادرنگ، امتیازدهی و رتبهبندی فقط یک قابلیت ظاهری نیست؛ بخشی از هستهٔ طراحی سیستم است. Redis و بهویژه Sorted Set امکان پیادهسازی سریع و کارآمد مکانیزمهایی مثل امتیازدهی، لیدربورد و رتبهٔ شخصی کاربران را فراهم میکنند، اما در مقیاس بالا، تصمیمهای معماری—مانند مدیریت hot key و الگوی توزیع داده—نقش تعیینکنندهای در پایداری سامانه دارند.
اگر در حال طراحی یا توسعهٔ بازیها و سیستمهای real-time با بار بالا هستید، تیم معماری و توسعهٔ نرمافزار شرکت راهکار نگار هوشمند (آرکان) میتواند از تحلیل مسئله تا طراحی معماری مقیاسپذیر و پیادهسازی عملیاتی، همراه شما باشد.
این مقاله با هدف انتقال تجربه و آگاهیبخشی فنی، توسط تیم توسعهٔ نرمافزار شرکت راهکار نگار هوشمند (آرکان) تهیه شده است.
#Redis
#SortedSet
#Leaderboard
#Game_Mechanics
#RealTime_Systems
#Scalable_Architecture
#Backend_Engineering
#System_Design
#High_Performance
#Distributed_Systems
#Software_Architecture
#شرکت_راهکار_نگار_هوشمند
#arcanco
مطلبی دیگر از این انتشارات
فرایندکاوی بهعنوان سرویس
مطلبی دیگر از این انتشارات
نظرتون درباره تست نویسی چیه؟ بیاید یه چالش راه بندازیم و این هیولای دوستداشتنی رو اهلی کنیم!
مطلبی دیگر از این انتشارات
تحلیل و مستندسازی معماری ابزار HSC بر اساس arc42