Manipulando Conjuntos Ordenados (ZSet) do Redis com Spring Data Redis

O Redis disponibiliza a estrutura de dados de Conjuntos Ordenados (ZSet), que, assim como os conjuntos não ordenados, armazena uma coleção de strings sem permitir membros duplicados. A principal diferença reside no fato de que cada elemento possui uma pontuação (score) associada do tipo double. Embora os membros sejam estritamente únicos, os scores podem se repetir. O Redis utiliza esses scores para classificar os elementos do conjunto em ordem crescente.

A interface ZSetOperations do Spring Data Redis disponibiliza uma vasta gama de métodos para interagir com essa estrutura. Abaixo, demonstramos a configuração inicial para obter o RedisTemplate e as operações específicas para ZSet, utilizando JUnit 5 para os testes.

Configuração Inicial

private RedisTemplate<String, Object> redisClient;
private ZSetOperations<String, Object> zSetOps;

@BeforeEach
public void setupEnvironment() {
    ApplicationContext appContext = new ClassPathXmlApplicationContext("spring-redis-config.xml");
    redisClient = (RedisTemplate<String, Object>) appContext.getBean("redisTemplate");
    zSetOps = redisClient.opsForZSet();
}

Adição de Elementos e Consulta por Índice

O método add insere um membro com sua respectiva pontuação. Se o membro já existir, o score é atualizado. Podemos adicionar múltiplos elementos de uma vez utilizando um Set de TypedTuple. O método range retorna os elementos dentro de um intervalo de índices, ordenados por score.

@Test
public void verifyAddAndRangeOperations() {
    String key = "leaderboard:game_alpha";
    redisClient.delete(key);
    
    // Adicionando um único elemento
    Boolean added = zSetOps.add(key, "player_alice", 100.5);
    System.out.println("Alice added: " + added); 
    
    // Adicionando múltiplos elementos via TypedTuple
    ZSetOperations.TypedTuple<Object> tupleBob = new DefaultTypedTuple<>("player_bob", 250.0);
    ZSetOperations.TypedTuple<Object> tupleCharlie = new DefaultTypedTuple<>("player_charlie", 150.75);
    
    Set<ZSetOperations.TypedTuple<Object>> batch = new HashSet<>();
    batch.add(tupleBob);
    batch.add(tupleCharlie);
    
    Long addedCount = zSetOps.add(key, batch);
    System.out.println("Batch added count: " + addedCount);
    
    // Recuperando todos os elementos (índice 0 até -1) ordenados por score
    Set<Object> allPlayers = zSetOps.range(key, 0, -1);
    System.out.println("Ordered players: " + allPlayers);
}

Remoção de Membros

A operação remove exclui um ou mais membros específicos do conjunto ordenado, retornando a quantidade de elementos efetivamente removidos.

@Test
public void verifyRemoveOperation() {
    String key = "inventory:items";
    redisClient.delete(key);
    
    zSetOps.add(key, "item_sword", 10);
    zSetOps.add(key, "item_shield", 20);
    
    System.out.println("Before remove: " + zSetOps.range(key, 0, -1));
    
    Long removed = zSetOps.remove(key, "item_sword");
    System.out.println("Removed count: " + removed);
    System.out.println("After remove: " + zSetOps.range(key, 0, -1));
}

Incremento de Pontuação

Permite alterar o score de um elemento existente (ou criar um novo com score 0 + delta) adicionando um valor incremental.

@Test
public void verifyIncrementScore() {
    String key = "metrics:page_views";
    redisClient.delete(key);
    
    // Incrementa o score. Se o membro não existir, o score inicial é 0.
    Double newScore = zSetOps.incrementScore(key, "homepage", 5.5);
    System.out.println("New score for homepage: " + newScore);
    
    System.out.println("Keys matching pattern: " + redisClient.keys("metrics:*"));
}

Obtenção de Ranks (Índices)

rank retorna a posição (índice) de um membro na ordem crescente (do menor para o maior score). reverseRank faz o inverso, retornando a posição na ordem decrescente. Ambos os índices começam em zero.

@Test
public void verifyRankOperations() {
    String key = "scores:exam_results";
    redisClient.delete(key);
    
    zSetOps.add(key, "student_a", 85);
    zSetOps.add(key, "student_b", 95);
    zSetOps.add(key, "student_c", 70);
    
    // Ordem crescente: student_c (70), student_a (85), student_b (95)
    Long rankAscending = zSetOps.rank(key, "student_b");
    System.out.println("Rank of student_b (ascending): " + rankAscending); // 2
    
    Long rankDescending = zSetOps.reverseRank(key, "student_b");
    System.out.println("Rank of student_b (descending): " + rankDescending); // 0
}

Consultando Elementos com seus Scores

Quando é necessário recuperar não apenas os valores, mas também suas pontuações, utilizamos métodos que retornam TypedTuple. rangeByScore filtra os elementos com base em um intervalo mínimo e máximo de scores, permitindo também paginação com offset e count.

@Test
public void verifyRangeWithScoresAndByScore() {
    String key = "leaderboard:weekly";
    redisClient.delete(key);
    
    zSetOps.add(key, "user_1", 10);
    zSetOps.add(key, "user_2", 30);
    zSetOps.add(key, "user_3", 20);
    zSetOps.add(key, "user_4", -5);
    
    // Recuperando todos com scores
    Set<ZSetOperations.TypedTuple<Object>> tuples = zSetOps.rangeWithScores(key, 0, -1);
    for (ZSetOperations.TypedTuple<Object> t : tuples) {
        System.out.printf("Value: %s | Score: %.1f%n", t.getValue(), t.getScore());
    }
    
    // Filtrando por score (entre 10 e 20)
    Set<Object> filtered = zSetOps.rangeByScore(key, 10, 20);
    System.out.println("Filtered by score [10-20]: " + filtered);
    
    // Filtrando por score com paginação (offset 0, count 2)
    Set<Object> paginated = zSetOps.rangeByScore(key, -10, 100, 0, 2);
    System.out.println("Paginated by score: " + paginated);
}

Operações de Ordem Inversa

As variantes reverseRange, reverseRangeWithScores, reverseRangeByScore e suas combinações com paginação funcionam de maneira análoga às suas contrapartes, mas retornam os dados ordenados do maior para o menor score.

@Test
public void verifyReverseRangeOperations() {
    String key = "leaderboard:monthly";
    redisClient.delete(key);
    
    zSetOps.add(key, "dev_a", 10);
    zSetOps.add(key, "dev_b", 30);
    zSetOps.add(key, "dev_c", 20);
    
    // Ordem decrescente
    Set<Object> descValues = zSetOps.reverseRange(key, 0, -1);
    System.out.println("Descending values: " + descValues);
    
    Set<ZSetOperations.TypedTuple<Object>> descTuples = zSetOps.reverseRangeWithScores(key, 0, -1);
    for (ZSetOperations.TypedTuple<Object> t : descTuples) {
        System.out.printf("Desc Value: %s | Score: %.1f%n", t.getValue(), t.getScore());
    }
    
    // Filtro por score em ordem decrescente
    Set<Object> descFiltered = zSetOps.reverseRangeByScore(key, 15, 35);
    System.out.println("Desc filtered by score: " + descFiltered);
}

Contagem, Tamanho e Consulta de Score

count calcula quantos elementos possuem scores dentro de um intervalo. size e zCard retornam o número total de membros no conjunto. O método score recupera a pontuação de um membro específico.

@Test
public void verifyCountSizeAndScore() {
    String key = "cache:temp_data";
    redisClient.delete(key);
    
    zSetOps.add(key, "k1", 5);
    zSetOps.add(key, "k2", 15);
    zSetOps.add(key, "k3", 25);
    
    Long countInRange = zSetOps.count(key, 10, 30);
    System.out.println("Count in range [10-30]: " + countInRange); // 2
    
    Long totalSize = zSetOps.size(key);
    Long zCardSize = zSetOps.zCard(key);
    System.out.println("Size: " + totalSize + ", ZCard: " + zCardSize); // 3, 3
    
    Double specificScore = zSetOps.score(key, "k2");
    System.out.println("Score of k2: " + specificScore); // 15.0
}

Remoção por Intervalo de Índice ou Score

removeRange remove elementos com base em seus índices (rank), enquanto removeRangeByScore remove elementos cujos scores estejam entre um valor mínimo e máximo.

@Test
public void verifyRemoveRangeOperations() {
    String key = "queue:tasks";
    redisClient.delete(key);
    
    zSetOps.add(key, "task_1", 1);
    zSetOps.add(key, "task_2", 2);
    zSetOps.add(key, "task_3", 3);
    zSetOps.add(key, "task_4", 4);
    
    // Remove por índice (remove o 2º e 3º elementos)
    Long removedByIndex = zSetOps.removeRange(key, 1, 2);
    System.out.println("Removed by index: " + removedByIndex);
    
    // Remove por score
    Long removedByScore = zSetOps.removeRangeByScore(key, 3, 10);
    System.out.println("Removed by score: " + removedByScore);
}

Operações de União e Interseção

É possível realizar operações matemáticas entre conjuntos. unionAndStore calcula a união de dois ou mais ZSets, somando os scores de membros em comum, e salva o resultado em uma nova chave. intersectAndStore faz o mesmo, mas considerando apenas a interseção (elementos presentes em todos os conjuntos de origem).

@Test
public void verifyUnionAndIntersect() {
    String keyA = "sets:groupA";
    String keyB = "sets:groupB";
    String destUnion = "sets:unionResult";
    String destIntersect = "sets:intersectResult";
    
    redisClient.delete(keyA);
    redisClient.delete(keyB);
    redisClient.delete(destUnion);
    redisClient.delete(destIntersect);
    
    zSetOps.add(keyA, "item_x", 10);
    zSetOps.add(keyA, "item_y", 20);
    
    zSetOps.add(keyB, "item_y", 5);
    zSetOps.add(keyB, "item_z", 15);
    
    // União: item_x(10), item_y(20+5=25), item_z(15)
    Long unionCount = zSetOps.unionAndStore(keyA, keyB, destUnion);
    System.out.println("Union size: " + unionCount);
    
    Set<ZSetOperations.TypedTuple<Object>> unionTuples = zSetOps.rangeWithScores(destUnion, 0, -1);
    for (ZSetOperations.TypedTuple<Object> t : unionTuples) {
        System.out.printf("Union -> Value: %s | Score: %.1f%n", t.getValue(), t.getScore());
    }
    
    // Interseção: apenas item_y (20+5=25)
    Long intersectCount = zSetOps.intersectAndStore(keyA, keyB, destIntersect);
    System.out.println("Intersect size: " + intersectCount);
}

Iteração com Scan

Para iterar sobre grandes conjuntos sem bloquear o servidor Redis (diferente de iterações completas em memória), o método scan utiliza um cursor, sendo ideal para conjuntos de dados massivos.

@Test
public void verifyScanOperation() {
    String key = "logs:events";
    redisClient.delete(key);
    
    zSetOps.add(key, "event_1", 1);
    zSetOps.add(key, "event_2", 2);
    zSetOps.add(key, "event_3", 3);
    
    // Iterando utilizando o cursor
    Cursor<ZSetOperations.TypedTuple<Object>> cursor = zSetOps.scan(key, ScanOptions.NONE);
    while (cursor.hasNext()) {
        ZSetOperations.TypedTuple<Object> item = cursor.next();
        System.out.printf("Scanned -> Value: %s | Score: %.1f%n", item.getValue(), item.getScore());
    }
}

Tags: Redis spring-data-redis Zset java nosql

Publicado em 7-20 20:15