Redis ile sıralama tablosu (leaderboard), her tablo için bir sorted set tutularak kurulur: üye olarak oyuncu veya ürün kimliği, puan olarak sıralamak istediğiniz değer saklanır. Puan güncellemek için ZADD veya ZINCRBY, ilk N kaydı almak için ZRANGE ... REV WITHSCORES, bir kullanıcının yukarıdan kaçıncı olduğunu bulmak için ZREVRANK kullanılır. Sorted set üyeleri her zaman sıralı tuttuğu için bu işlemler tablonun boyutundan bağımsız olarak O(log N) karmaşıklıkta kalır; kullanıcı adı, avatar gibi bilgiler ise sorted set'i hafif tutmak için kimliğe bağlı ayrı bir hash'te durur.
Bu yazı Redis belgelerindeki "Redis leaderboard" kullanım senaryosunu ve redis-py, node-redis, PHP örneklerini temel alır. Sorted set komutlarının seçeneklerinin tamamı (NX, GT, BYSCORE, BYLEX vb.) sorted set yazısında anlatılır; burada bu komutların bir sıralama tablosunda nasıl bir araya geldiğine, sayfalamaya, eşit puanlara ve zaman pencereli tablolara odaklanıyoruz.
Sıralama tablosu neden zor bir problemdir?

Belge sorunu iyi özetler: ilişkisel bir veritabanında tek bir kaydın sırasını hesaplamak tüm tablo üzerinde bir ORDER BY gerektirir, bu en iyi ihtimalle O(N)'dir ve milyonlarca satırda sorgu süresini saniyelere taşır. Sonucu önbelleğe almak da işe yaramaz, çünkü puanlar her kullanıcı eyleminde değişir; süreye dayalı bir önbellek ya hemen bayatlar ya da süresi dolduğunda aynı anda yenileme yağmuruna yol açar. Önbellek kavramlarının genel anlatımı için önbellek yazısına bakabilirsiniz.
Süreç içi veri yapıları da yük dengeleyicinin arkasında bozulur, çünkü hiçbir örnek sıralamanın tamamını tutmaz. Ayrı bir analitik veya leaderboard servisi kurmak ise temelde tek bir sıralı veri yapısı olan bir iş için operasyon yükü getirir. Belge bu senaryoyu iki benzer Redis yapısından da ayırır: Stream'ler olay geçmişini kaydeder ama sıralı durum tutmaz; olasılıksal Top-K ise en sık görülenleri yaklaşık olarak bulur, kesin sıra veya "çevremdekiler" sorgusu veremez. Top-K için olasılıksal yapılar yazısına bakın.
Veri modeli: sorted set artı hash
Belgedeki redis-py örneği iki yapı kullanır. Puanlar leaderboard:demo adlı bir sorted set'tedir; her kullanıcının ayrıntıları ise leaderboard:demo:user:player-1 gibi aynı kimliği taşıyan bir hash'tedir:
leaderboard:demo
player-1 => 1225
player-2 => 1180
player-3 => 1105
leaderboard:demo:user:player-1
name = Ada
description = Solves production incidents before breakfast.Bu ayrımın nedeni, sıralama görünümü ile profil görünümünün farklı veri biçimleri istemesidir. Sorted set yalnızca kimlik ve puan tutarak küçük ve hızlı kalır; görünen ad, kısa açıklama, takım veya ülke, avatar adresi gibi hafif alanlar hash'te durur. Belge büyük profil belgelerinin veya nadiren kullanılan özniteliklerin başka bir depoda kalmasını, Redis'te yalnızca tabloyu hızla çizmek için gereken alanların tutulmasını önerir. Hash komutları için hash yazısına bakın.
Puan yazmak ve artırmak: ZADD ile ZINCRBY
Bir sıralama tablosunu güncellemenin iki yolu vardır. Yeni puanı biliyorsanız ZADD ile doğrudan yazarsınız; mevcut puana ekleme yapmak istiyorsanız ZINCRBY kullanırsınız. Sorted set belgesindeki örnek farkı gösterir:
> ZADD racer_scores 100 "Wood"
(integer) 1
> ZADD racer_scores 100 "Henshaw"
(integer) 1
> ZADD racer_scores 150 "Henshaw"
(integer) 0
> ZINCRBY racer_scores 50 "Wood"
"150"
> ZINCRBY racer_scores 50 "Henshaw"
"200"ZADD, üye zaten varsa 0 döndürür ve puanı önceki değere bakmadan değiştirir; Henshaw'ın puanı 100'den 150'ye bu şekilde çıkar. ZINCRBY ise yeni puanı döndürür ve 150 olan puanı 200'e taşır. Belgenin vurguladığı nokta, ZINCRBY'nin puanı yerinde ve atomik olarak güncellemesidir: önce okuyup sonra yazan bir döngü veya önbellek geçersizleştirme gerekmez. Aynı oyuncu için iki sunucu aynı anda puan eklese bile iki artış da kaybolmadan uygulanır.
İlk N kayıt ve kullanıcının sırası

ZRANGE varsayılan olarak düşükten yükseğe sıralar; sıralama tablosunda en yüksek puan başta olmalıdır, bu yüzden REV eklenir. Puanları da istiyorsanız WITHSCORES kullanırsınız. İlk 10 kaydı almak için ZRANGE tablo 0 9 REV WITHSCORES yeterlidir; sıralar sıfırdan başladığı için 0 birinci, 9 onuncu sıradır. Kullanıcının sırası için ZREVRANK kullanılır. Komut sayfasının örneği hem düz kullanımı hem de WITHSCORE seçeneğini gösterir:
> ZADD myzset 1 "one"
(integer) 1
> ZADD myzset 2 "two"
(integer) 1
> ZADD myzset 3 "three"
(integer) 1
> ZREVRANK myzset "one"
(integer) 2
> ZREVRANK myzset "four"
(nil)
> ZREVRANK myzset "three" WITHSCORE
1) (integer) 0
2) "3"
> ZREVRANK myzset "four" WITHSCORE
(nil)İki ayrıntı önemlidir. Birincisi, sıra sıfır tabanlıdır: en yüksek puanlı üye 0 döndürür, kullanıcıya gösterirken bir eklemeniz gerekir. İkincisi, üye veya anahtar yoksa yanıt nil'dir; uygulamanız bunu "listede yok" olarak ele almalıdır. WITHSCORE sayesinde sıra ve puanı tek komutla alırsınız, böylece "1225 puanla 1. sıradasınız" gibi bir satır için iki ayrı istek gerekmez. Artan sıra gerekiyorsa aynı işi ZRANK yapar.
"Çevremdekiler" sorgusu ve sayfalama
Belgenin vurguladığı yeteneklerden biri, herhangi bir kullanıcıya kesin genel sırasını ve hemen üstündeki ile altındaki komşularını göstermektir. Bunun için önce kullanıcının sırası bulunur, sonra bu sıranın etrafında bir pencere hesaplanıp ters sıralı bir aralık okunur. redis-py örneğindeki yöntem şöyledir:
def get_around_rank(self, rank: int, count: int) -> list[dict[str, object]]:
normalized_rank = max(1, int(rank))
normalized_count = max(1, int(count))
half_window = normalized_count // 2
start = max(0, normalized_rank - 1 - half_window)
end = start + normalized_count - 1
entries = self.redis.zrange(
self.key,
start,
end,
desc=True,
withscores=True,
)
return self._hydrate_entries(entries, start_rank=start + 1)Küçük bir hesapla izleyelim. Kullanıcı 10. sırada (bir tabanlı) olsun ve beş kişilik bir pencere istensin. Yarım pencere 5 // 2 = 2'dir. Başlangıç indisi 10 - 1 - 2 = 7, bitiş indisi 7 + 5 - 1 = 11 olur. Sıfır tabanlı 7 ile 11 arası, bir tabanlı 8 ile 12. sıralara karşılık gelir; yani kullanıcının üstünde iki, altında iki kişi görünür. Kullanıcı 1. sıradaysa başlangıç negatif çıkacağı için max(0, ...) onu 0'a sabitler ve pencere en üstten başlar. _hydrate_entries ise sıradan sonra her kimlik için hash'teki meta veriyi okuyup sonuca ekleyen yardımcı yöntemdir.
Sayfalı bir tablo için aynı mantık daha basittir: sayfa numarası p (sıfırdan) ve sayfa boyu k ise indisler p*k ile p*k+k-1 arasıdır. Puan aralığına göre sayfalama yapmak isterseniz ZRANGE komutunun LIMIT offset count seçeneği vardır, ancak komut sayfasına göre bu seçenek BYSCORE veya BYLEX gerektirir ve büyük bir offset, o kadar öğenin dolaşılmasını gerektirdiği için O(N)'e varan maliyet getirebilir. Sorted set belgesi de on binlerce veya daha fazla öğe döndüren ZRANGE çağrılarında dikkatli olunmasını, karmaşıklığın döndürülen öğe sayısıyla büyüdüğünü hatırlatır.
Tabloyu sabit boyutta tutmak
Örnekteki yardımcı sınıf her güncellemeden sonra tabloyu yapılandırılmış en fazla kayıt sayısına kırpar ve kırpılan kullanıcıların meta veri hash'lerini DEL ile siler. Kırpma için ZREMRANGEBYRANK kullanılır. Bu komutta indisler düşükten yükseğe sayılır (0 en düşük puan), negatif indisler ise en yüksekten geriye doğru sayar (-1 en yüksek puan). Komut sayfasındaki örnek en düşük iki üyeyi siler:
> ZADD myzset 1 "one"
(integer) 1
> ZADD myzset 2 "two"
(integer) 1
> ZADD myzset 3 "three"
(integer) 1
> ZREMRANGEBYRANK myzset 0 1
(integer) 2
> ZRANGE myzset 0 -1 WITHSCORES
1) "three"
2) "3"Yalnızca en iyi 100 kaydı tutmak istiyorsanız en düşük sıradan, en yüksek 100 üyenin hemen altına kadar olan aralığı silersiniz: ZREMRANGEBYRANK tablo 0 -101. Burada -101, en yüksekten geriye sayıldığında 101. üyedir; 0 ile -101 arası, en iyi 100'ün dışında kalan herkestir. Tabloda 100 veya daha az üye varsa aralık boştur ve komut 0 döndürür. Komut silinen üye sayısını döndürdüğü için kırpma olup olmadığını da öğrenmiş olursunuz.
Belge bir uyarı da ekler: yazma ve kırpma işlemleri ayrı komutlar olduğu için çok yoğun eşzamanlı yazmalarda daha güçlü garanti istiyorsanız güncelle ve kırp davranışını bir Lua betiğine veya daha sıkı bir transaction akışına taşıyabilirsiniz. Küçük bir uygulama için örnekteki pipeline yaklaşımı yeterlidir. Seçenekler için Lua script ve MULTI/EXEC yazılarına bakın.
Eşit puanlar nasıl sıralanır?
Sorted set belgesine göre iki üyenin puanı aynıysa üyeler sözlük sırasına göre dizilir. ZRANGE sayfası, REV kullanıldığında eşit puanların ters sözlük sırasıyla çözüldüğünü belirtir. Belgedeki küçük örnekte Norem ve Royce aynı puanda (10) olduğu için ZRANK racer_scores "Norem" 0, ZREVRANK racer_scores "Norem" ise 2 döndürür: artan sırada Norem, Royce'tan önce; azalan sırada ise sonra gelir. Bu davranış belirlenimlidir, ama oyuncu kimliklerinin alfabetik sırasına bağlı olması çoğu oyun için adil bir kural değildir.
Leaderboard sayfalarının "eşitlikler nasıl davranmalı" başlığı bu yüzden şunu önerir: uygulamanız farklı bir eşitlik bozucu istiyorsa onu puanın içine kodlayabilir veya ek durum saklayabilirsiniz. Örneğin "aynı puana önce ulaşan üstte olsun" kuralı için puanın ondalık kısmına zamanla azalan küçük bir değer eklemek bir yaklaşımdır; ancak bu durumda kullanıcıya gösterilen puanı tam sayıya yuvarlamanız ve sorted set puanının bir kayan nokta sayısı olduğunu hesaba katmanız gerekir. Hangi yolu seçerseniz seçin, kuralı belgeleyin ve testlerle sabitleyin.
Günlük, haftalık ve sezonluk tablolar
Bazı tablolar kalıcıdır, bazıları her gün, her hafta veya her sezon sıfırlanır. Belgenin önerdiği yaklaşım anahtarları sezona veya etkinliğe göre ad alanlarına ayırmak, geçmiş sonuçların anlık görüntüsünü başka bir yere almak ve gerekirse güncel tabloyu kaynak veriden yeniden kurmaktır. Pencere başına ayrı bir anahtar kullanır ve bu anahtara EXPIRE koyarsanız günlük, haftalık ve aylık tablolar kendiliğinden temizlenir. Süre yönetimi için EXPIRE ve TTL yazısına bakın.
Pencereleri birleştirmek için ZUNIONSTORE kullanılır: birden çok sorted set'in birleşimini hesaplayıp sonucu yeni bir anahtara yazar. Varsayılan olarak bir üyenin sonuç puanı, bulunduğu kümelerdeki puanların toplamıdır. WEIGHTS her kümenin puanlarını bir katsayıyla çarpar; AGGREGATE ise toplama yerine en küçük (MIN) veya en büyük (MAX) değeri seçer. Komut sayfasındaki örnek ağırlıkların etkisini gösterir:
> ZADD zset1 1 "one"
(integer) 1
> ZADD zset1 2 "two"
(integer) 1
> ZADD zset2 1 "one"
(integer) 1
> ZADD zset2 2 "two"
(integer) 1
> ZADD zset2 3 "three"
(integer) 1
> ZUNIONSTORE out 2 zset1 zset2 WEIGHTS 2 3
(integer) 3
> ZRANGE out 0 -1 WITHSCORES
1) "one"
2) "5"
3) "three"
4) "9"
5) "two"
6) "10"Sonuç kolayca doğrulanabilir: "one" iki kümede de 1 puanlıdır, 1x2 + 1x3 = 5 olur; "two" için 2x2 + 2x3 = 10; yalnızca ikinci kümede bulunan "three" için 3x3 = 9. Bu, haftalık tabloyu yedi günlük tablodan üretmek veya bu haftanın puanlarını geçen haftanınkinden daha ağır saymak için kullanılabilir. Hedef anahtar zaten varsa üzerine yazılır. Redis 8.8 sürüm notlarına göre ZUNIONSTORE ve benzerlerine yeni bir COUNT toplayıcısı geldi; bu seçenek puanları yok sayıp bir üyenin kaç kümede bulunduğunu sayar ve yalnızca 8.8 veya sonrasında kullanılabilir. Cluster kullanıyorsanız çok anahtarlı bu komutun anahtarlarının aynı hash slot'ta olması gerektiğini unutmayın; ayrıntılar cluster yazısında.
Sık yapılan hatalar

- REV unutmak:
ZRANGEvarsayılan olarak en düşük puandan başlar; sıralama tablosundaREVgerekir. - Sırayı bir tabanlı sanmak:
ZREVRANKen yüksek puan için 0 döndürür. - Oku-değiştir-yaz ile puan artırmak: Puanı okuyup istemcide toplayıp
ZADDile yazmak eşzamanlı güncellemeleri kaybettirir;ZINCRBYkullanın. - Profil verisini üye adına gömmek: Üye olarak JSON veya uzun metin saklamak sorted set'i şişirir ve güncellemeyi zorlaştırır; kimlik kullanıp ayrıntıyı hash'te tutun.
- Kırpılan kullanıcıların hash'lerini unutmak: Tablodan düşen kullanıcının meta veri hash'i silinmezse bellekte birikir.
- Büyük offset ile sayfalama: Çok derin sayfalar maliyetlidir; derin sayfalar yerine "çevremdekiler" görünümü sunmayı düşünün.
Puanların sık değiştiği bir tablo istemci tarafı önbellek için de iyi bir aday değildir; belge sık güncellenen sayaç ve skor tablosu verileri için önbelleksiz ayrı bir bağlantı kullanmayı önerir. Ayrıntılar client-side caching yazısında.
Sık Sorulan Sorular
Milyonlarca kullanıcıda performans düşer mi?
Belgeye göre ZADD, ZRANGE ve ZREVRANK küme boyutundan bağımsız olarak O(log N) çalışır; ZRANGE için buna döndürülen öğe sayısı eklenir. Belge milyonlarca üyenin yaklaşık 500 MB RAM ile desteklenebileceğini de belirtir; kendi veriniz için MEMORY USAGE ile ölçüm yapın.
Kullanıcı tabloda yoksa ne olur?
ZREVRANK ve ZSCORE nil döndürür. Uygulamanız bu durumda "henüz sıralamada değilsiniz" gibi bir mesaj göstermelidir.
Ayrı bir leaderboard servisi gerekir mi?
Belgenin görüşü, Redis zaten kullanılıyorsa sıralama tablosunun ek altyapı gerektirmediği yönündedir. Okuma ve yazmaların milisaniyenin altında olması tabloyu doğrudan istek yolunda kullanmayı mümkün kılar.
Hangi istemci kütüphanesiyle kullanabilirim?
Belgede redis-py, node-redis, go-redis, Jedis, Lettuce, StackExchange.Redis, Predis, redis-rb ve redis-rs için çalışan örnekler bulunur. Komutlar her kütüphanede aynıdır; yalnızca yöntem adları değişir (örneğin node-redis'te zAdd, redis-py'de zadd).
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Redis öğrenme rehberi · Sorted set: ZADD ve ZRANGE · Hash komutları · EXPIRE ve TTL · Redis Lua script · Bloom filter ve Top-K