Pular para conteúdo

Sorted Set: o ranking do servidor

Lição 101-05 · Lab · 8 min

Um placar em que cada membro carrega um score e o Redis mantém a ordem a cada escrita: buscar a posição custa O(log N); retornar o top 10 ou uma faixa custa O(log N + M), em que M é o número de resultados. A ordem já está no índice, sem ORDER BY a cada consulta. É o Sorted Set, e o lab constrói o ranking de XP do seed com ele.

O que o lab faz

  • Ler o top 10 do ranking rank:xp com ZREVRANGE ... WITHSCORES
  • Descobrir a posição da Vesper com ZREVRANK e o XP dela com ZSCORE
  • Dar 5000 XP para ela com ZINCRBY e ver o ranking se ajustar sozinho
  • Montar um grupo por faixa de XP com ZCOUNT e ZRANGEBYSCORE
  • Conferir com ./quest verify 101-05

Faça agora

./quest run 101-05 jedis
./quest run 101-05 lettuce    # opcional: mesmo lab, outro client
./quest verify 101-05

O código

String rank = ctx.k("rank", "xp");
try (RedisClient jedis = Clients.jedis()) {
    jedis.zadd(rank, 900, "vesper");                           // back to the seed value

    List<Tuple> top = jedis.zrevrangeWithScores(rank, 0, 9);   // top 10, highest first
    for (Tuple t : top) System.out.println(t.getElement() + " " + (long) t.getScore());

    Long before = jedis.zrevrank(rank, "vesper");              // 11 -> 12th place (zero based)
    Double xp = jedis.zscore(rank, "vesper");                  // 900.0

    double newXp = jedis.zincrby(rank, 5000, "vesper");        // 5900.0, atomic
    Long after = jedis.zrevrank(rank, "vesper");
    Tuple rival = jedis.zrevrangeWithScores(rank, after - 1, after - 1).get(0);   // who is right above

    long inBracket = jedis.zcount(rank, 100_000, 600_000);
    List<Tuple> bracket = jedis.zrangeByScoreWithScores(rank, 100_000, 600_000);
}
try (StatefulRedisConnection<String, String> connection = Clients.lettuceConnection()) {
    RedisCommands<String, String> redis = connection.sync();
    redis.zadd(rank, 900, "vesper");

    List<ScoredValue<String>> top = redis.zrevrangeWithScores(rank, 0, 9);
    for (ScoredValue<String> sv : top) System.out.println(sv.getValue() + " " + (long) sv.getScore());

    Long before = redis.zrevrank(rank, "vesper");
    Double xp = redis.zscore(rank, "vesper");

    Double newXp = redis.zincrby(rank, 5000, "vesper");
    Long after = redis.zrevrank(rank, "vesper");
    ScoredValue<String> rival = redis.zrevrangeWithScores(rank, after - 1, after - 1).get(0);

    Range<Double> range = Range.create(100_000.0, 600_000.0);
    Long inBracket = redis.zcount(rank, range);
    List<ScoredValue<String>> bracket = redis.zrangebyscoreWithScores(rank, range);
}

No Redis Insight

No Browser, abra seu-prefixo:rank:xp. O Insight mostra os membros já ordenados por score; Vesper aparece com 5900 no fim da lista, logo abaixo de Marisol (6400). Rode a lição de novo e o valor continua 5900: o lab reaplica o XP do seed antes de incrementar.

No Workbench, experimente ZRANGE seu-prefixo:rank:xp 0 9 REV WITHSCORES (a forma moderna do ZREVRANGE) e ZRANGEBYSCORE seu-prefixo:rank:xp 100000 600000 WITHSCORES.

No Profiler, veja o ZINCRBY passar como um comando só: não há GET, soma em Java e SET de volta, por isso ele é atômico.

Por dentro
Comando O que faz
ZADD key score membro Insere ou atualiza o score de um membro. NX, XX, GT e LT controlam quando gravar
ZINCRBY key incr membro Soma ao score de forma atômica e devolve o novo valor. Cria o membro se não existe
ZSCORE key membro Score de um membro (ou nil)
ZCARD key Quantos membros
ZREVRANGE key start stop [WITHSCORES] Fatia por posição, do maior para o menor. 0 9 é o top 10
ZRANGE key start stop REV WITHSCORES A mesma coisa na sintaxe unificada do Redis 6.2+
ZREVRANK key membro Posição do maior para o menor, base zero: 11 significa 12º lugar
ZRANK key membro Posição do menor para o maior
ZCOUNT key min max Quantos membros têm score na faixa
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT off n] Membros por faixa de score. (100 exclui a borda; -inf e +inf abrem a faixa

Empates: membros com o mesmo score ficam em ordem lexicográfica por bytes, invertida em consultas com REV. O Redis não guarda automaticamente a ordem de chegada para desempatar.

Em produção
  • O score é um double de 64 bits: inteiros até 2^53 são exatos. Um score composto com horário exige planejar escala e precisão; não acrescente um timestamp fracionário sem validar perda de precisão, limites e ordem desejada.
  • Placar por temporada: uma chave por semana (rank:xp:2026-w37) com EXPIRE, em vez de zerar o ranking global. ZUNIONSTORE junta temporadas quando você precisar do acumulado.
  • Se só o topo interessa, não deixe o ranking crescer sem limite: ZREMRANGEBYRANK key 0 -1001 mantém os 1000 melhores. Leituras são O(log N + M), mas ZRANGE 0 -1 em milhões de membros é um comando lento como qualquer *RANGE sem limite.
Desafio

Vesper ganhou 5000 XP e continuou em 12º: Marisol tem 6400. Troque BONUS_XP para 6000 no lab e rode de novo: ZREVRANK passa a devolver 10 (11º lugar) e o "logo acima" vira Dorian. Depois volte para 5000, porque o verify espera exatamente 5900.