Database Engineering

Redis ZSET'i Ustalıkla Kullanma: Ölçeklenebilir Gerçek Zamanlı Liderlik Tabloları Oluşturma

Rekabetçi oyunlar, sosyal medya etkileşimi ve canlı spor bahisleri dünyasında doğru, gerçek zamanlı sıralamaları görüntüleme yeteneği sadece bir özellik değil, temel bir ürün gereksinimidir. PostgreSQL veya MySQL gibi geleneksel ilişkisel veritabanları, milyonlarca kayıt için sıralama hesaplama yüküyle mücadele ederken, Redis in-memory (bellek içi) mimarisi ve özel veri yapıları sayesinde bu alanda öne çıkar. Özellikle Sıralı Küme (ZSET), liderlik tabloları uygulamak için altın standarttır. Bu yazı, Redis kullanarak yüksek performanslı sıralama sistemleri nasıl mimarileştirileceğini ve yüksek frekanslı güncellemeler için ZSET işlemlerinin nasıl optimize edileceğini incelemektedir.

Neden ZSET Gerçek Zamanlı Sıralamanın Motorudur?

Temel olarak, bir Redis ZSET, her biri kayan noktalı bir skora sahip benzersiz dize üyelerden oluşan bir koleksiyondur. Büyü, Redis'in bu yapıyı nasıl koruduğunda yatar. Dahili olarak Redis, üye adına göre O(1) aramalar için hash tabloları ve skora göre sıralı yineleme için atlamalı listelerin (skip lists) bir kombinasyonunu kullanır. Bu çift yapı yaklaşımı şunlara olanak tanır:

  • Üye ekleme ve skor güncelleme için O(log(N)) karmaşıklığı.
  • Bir üyenin sırasını bulma için O(log(N)) karmaşıklığı.
  • Üye aralığı alma (M döndürülen öğe sayısıdır) için O(M + log(N)) karmaşıklığı.

10 milyon kullanıcılı bir liderlik tablosu için bu karmaşıklıklar, toplam kullanıcı tabanının boyutundan bağımsız olarak, yeni bir skor ekleme veya ilk 100 oyuncuyu getirme işlemlerinin anında gerçekleşmesini sağlar.

Temel İşlemler ve Kod Uygulaması

Bir liderlik tablosu uygulamak üç temel işlemi içerir: bir kullanıcının skorunu güncelleme, bir kullanıcının sırasını getirme ve ilk N oyuncuyu getirme. Aşağıda, redis-py istemci kütüphanesi kullanılarak hazırlanmış pratik bir örnek bulunmaktadır. "SpaceInvaders" adlı bir oyunda bir oyuncunun skorunu güncellememiz gerektiği senaryosunu varsayıyoruz.

import redis

# Redis örneğine bağlan
client = redis.Redis(host='localhost', port=6379, decode_responses=True)

# 1. Skoru Güncelle
# ZADD, belirli bir skora sahip bir üyeyi sıralı kümeye ekler.
# Üye zaten varsa, skoru günceller.
def update_leaderboard(player_id, score):
    # NX bayrağını yalnızca mevcut değilse eklemek için, XX ise mevcut ise güncellemek için kullanın
    # Burada skoru basitçe güncelliyoruz/ekliyoruz
    client.zadd("space_invaders_rankings", {player_id: score})
    print(f"{player_id} skoru {score} olarak güncellendi")

# 2. Global Sırayı Al
# RANK, 0'ın ilk olduğunu belirterek üyenin sırasını döndürür.
# WITHSCORES ayrıca skoru da döndürür.
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 tabanlı dizini 1 tabanlı sıraya dönüştür
        return {"rank": rank + 1, "score": score}
    return {"rank": None, "score": None}

# 3. İlk 10 Oyuncuyu Al
# ZREVRANGE, skora göre azalan sırada üyeleri getirir
def get_top_10():
    top_players = client.zrevrange("space_invaders_rankings", 0, 9, withscores=True)
    return top_players

# Örnek Çalıştırma
update_leaderboard("user_123", 1500.5)
print(get_player_rank("user_123"))
print(get_top_10())

Yüksek Frekanslı Güncellemeler İçin Optimizasyon

Bireysel ZADD işlemleri hızlı olsa da, yüksek frekanslı ortamlar (bir flaş satış veya canlı bir espor finali gibi) saniyede binlerce yazma işlemi üretebilir. Bu senaryoları optimize etmek için aşağıdaki stratejileri göz önünde bulundurun:

1. PIPELINE ile Toplu Güncellemeler

Ağ gecikmesi, dağıtık sistemlerde genellikle darboğazdır. Her oyuncu güncellemesi için ayrı ayrı ZADD komutları göndermek yerine, Redis Pipelining (Hat) özelliğini kullanın. Bu, istemcinin her birinin yanıtını beklemek yerine sunucuya birden fazla komut göndermesine olanak tanır ve bu da verimliliği önemli ölçüde artırır.

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("Toplu güncelleme başarıyla tamamlandı")

2. Doğru Skor Türünü Seçme

Varsayılan olarak Redis çift hassasiyetli kayan noktalı sayılar kullanır. Ancak skorlarınız tamsayıysa, bunları normalize ederek bazen hafif performans ve bellek verimliliği elde edebilirsiniz. Daha da önemlisi, eşitliklerin nasıl ele alındığını anladığınızdan emin olun. Redis eşitlikleri üye adına göre leksikografik olarak çözer. Zaman bazında hassas eşitlik çözme gerekiyorsa, skora bir zaman damgası veya benzersiz bir sıra numarası eklemeyi düşünün (örneğin, base_score * 1000000 + timestamp). Bu, skorlar eşitse daha yeni işlemlerin daha yüksek sıralanmasını sağlar.

3. Bellek Yönetimi ve Eşzamanlılık (Eviction)

Liderlik tabloları sınırsız şekilde büyüyebilir. Listeyi kısaltmak için bir mekanizmanız yoksa, Redis bellek kullanımı benzersiz oyuncular sayısıyla doğrusal olarak artacaktır. ZREMRANGEBYRANK kullanarak bir temizleme stratejisi uygulayın; bu, skorlarını uzun süredir güncellemeyen oyuncuları kaldırmak veya küme boyutunu yönetilebilir bir sınırla tutmak (örneğin, ilk 10.000 oyuncuyu tutmak ve geri kalanı kaldırmak) için kullanılabilir.

Sonuç

Redis ZSET'leri, gerçek zamanlı liderlik tabloları oluşturmak için sağlam, yüksek performanslı bir temel sağlar. Bellek içi veri yapılarının verimliliğinden yararlanarak ve toplu işlemler yoluyla yazma desenlerini optimize ederek geliştiriciler, minimum gecikme ile milyonlarca eşzamanlı kullanıcıyı destekleyebilir. Sıralama sistemlerinizi tasarlarken, çözümün yük altında zarifçe ölçeklenmesini sağlamak için eşitlik çözme kuralları ve bellek sınırları gibi faktörleri göz önünde bulundurarak spesifik kullanım durumunuzu her zaman profilleyin.

Share: