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());
}
}