Sorted Sets: рейтинги и лидерборды
Sorted set — самая мощная структура Redis. Уникальные элементы, каждый со своим числовым счётом, всегда отсортированы. Это готовый лидерборд.
Хотите топ-10 игроков по очкам, обновляемый в реальном времени миллионами запросов? Sorted set делает это одной командой и держит порядок автоматически.
Отсортированное множество (Sorted Set, ZSet) объединяет свойства множества (уникальность) и сортировки: каждый элемент имеет score (числовой счёт), и элементы всегда хранятся в порядке этого счёта. Это делает мгновенными запросы вроде «топ-N» и «ранг элемента».
Основные команды
127.0.0.1:6379> ZADD leaderboard 100 "alice" 250 "bob" 175 "carol"
(integer) 3
127.0.0.1:6379> ZADD leaderboard 300 "alice"
(integer) 0
127.0.0.1:6379> ZRANGE leaderboard 0 -1 WITHSCORES
1) "carol" 2) "175"
3) "bob" 4) "250"
5) "alice" 6) "300"
127.0.0.1:6379> ZREVRANGE leaderboard 0 2 WITHSCORES
1) "alice" 2) "300" (топ-3 по убыванию)
127.0.0.1:6379> ZRANK leaderboard "bob"
(integer) 1
127.0.0.1:6379> ZSCORE leaderboard "alice"
"300"
127.0.0.1:6379> ZINCRBY leaderboard 50 "carol"
"225"
ZADD добавляет элемент со счётом (повторное добавление обновляет счёт), ZRANGE — по возрастанию, ZREVRANGE — по убыванию (для топа), ZRANK — позиция элемента, ZSCORE — его счёт, ZINCRBY атомарно меняет счёт.
Лидерборд всегда отсортирован по score: ZADD 100 alice, 250 bob, 175 carol позиция: 0 1 2 элемент: carol bob alice score: 175 250 300 ZREVRANGE 0 2 = топ-3 (alice, bob, carol) ZRANK bob = 1 (он на 2-м месте с нуля)
Диапазонные запросы по счёту
ZRANGEBYSCORE leaderboard 200 300 # все со счётом 200..300
ZCOUNT leaderboard 200 300 # сколько таких
Это удобно для «игроки уровня X», «события за период» (если score — timestamp) и очередей с приоритетом.
Демонстрация: sorted set на Python — лидерборд с рангами
# Моделируем sorted set: словарь "элемент -> score"
scores = {}
def zadd(member, score):
scores[member] = score # повторное добавление обновляет score
def zrevrange(top):
# отсортировать по score по убыванию (как ZREVRANGE)
return sorted(scores.items(), key=lambda x: -x[1])[:top]
def zrank(member):
# позиция в порядке возрастания score (как ZRANK)
order = sorted(scores.items(), key=lambda x: x[1])
for i, (m, _) in enumerate(order):
if m == member:
return i
return None
zadd("alice", 100); zadd("bob", 250); zadd("carol", 175)
zadd("alice", 300) # alice улучшила результат
print("Топ-3 (ZREVRANGE):")
for rank, (name, sc) in enumerate(zrevrange(3), 1):
print(f" {rank}. {name} — {sc} очков")
print("\nРанг bob (ZRANK):", zrank("bob"))
print("Счёт alice (ZSCORE):", scores["alice"])
print("\nЭлементы уникальны, всегда отсортированы по score.")
Каждое обновление счёта мгновенно меняет позицию в рейтинге. В Redis это работает за O(log n) благодаря умной внутренней структуре.
Как работает под капотом
Большой sorted set хранится как пара структур: skip list (список с пропусками) для упорядоченного обхода и диапазонных запросов, и хеш-таблица для мгновенного поиска счёта по элементу (ZSCORE за O(1)). Skip list даёт операции добавления, удаления и поиска ранга за O(log n) — это и есть секрет масштабируемости лидербордов. Маленькие ZSet (≤128 элементов, короткие строки) Redis хранит компактно как listpack.
Частые ошибки
- Хранить нечисловые «счета». Score — это число с плавающей точкой; нельзя сортировать по произвольной строке.
- ZRANGE без WITHSCORES, когда нужны очки. Без флага вернутся только элементы.
- Путать ZRANGE и ZREVRANGE. Для топа (от большего к меньшему) нужен
ZREVRANGEилиZRANGE ... REV.
Best practices
- Используйте sorted set для лидербордов, рейтингов, очередей с приоритетом, временных рядов (score = timestamp).
- Для топ-N используйте
ZREVRANGE ... 0 N-1— это O(log n + N), очень быстро. - Меняйте счёт через
ZINCRBYатомарно, не пересоздавая элемент.
Итог: Sorted set — уникальные элементы со счётом, всегда отсортированные. Это готовый лидерборд с операциями за O(log n). Внутри — связка skip list + хеш-таблицы, дающая и быструю сортировку, и мгновенный поиск счёта.
Sorted set как очередь с приоритетом и временная шкала
Лидерборды — самое наглядное, но далеко не единственное применение sorted set. Поскольку score — это просто число, в него можно положить что угодно сортируемое, и структура раскрывается с новых сторон.
# Очередь с приоритетом: score = приоритет
ZADD jobs 1 "low-task" 10 "urgent-task"
ZPOPMIN jobs # забрать задачу с наименьшим score (или ZPOPMAX)
# Временная шкала: score = timestamp
ZADD timeline 1718900000 "event-A"
ZRANGEBYSCORE timeline 1718900000 1718903600 # события за час
Если положить в score метку времени, sorted set превращается в индекс по времени: «последние события», «всё за период», «удалить старше N» через ZREMRANGEBYSCORE. Это популярный приём для лент, расписаний и хранения недавней активности.
Команды ZPOPMIN/ZPOPMAX (и их блокирующие версии BZPOPMIN/BZPOPMAX) делают из sorted set полноценную очередь с приоритетом: всегда забирается элемент с наименьшим или наибольшим score. А ZRANGEBYLEX позволяет делать лексикографические диапазонные запросы, когда у всех элементов одинаковый score, — основа для автодополнения и словарей.