4.5 sorted set
4.5.1 概述
- 核心基础
有序集合(sorted set)是在普通集合(set)的基础上做了扩展,为集合中的每一个元素都关联了一个对应的分数(score)。
基于这个特性,它不仅支持普通集合的核心操作(插入、删除、判断元素是否存在),还额外支持一系列与分数相关的专属操作(如获取分数最高 / 最低的前 N 个元素、获取指定分数范围的元素等)。 - 与列表(list)类型的异同
(一)相同点(二者均具备有序性、范围查询能力)
二者存储的元素都是有序的;
二者都支持获取指定范围的元素。
(二)核心区别(底层实现与功能特性差异)
|--------|--------------------------------|-----------------------------|
| 对比维度 | 列表(list) | 有序集合(sorted set) |
| 底层实现 | 双向链表 | 散列表 |
| 访问性能 | 靠近两端的数据访问速度极快,元素增多后,访问中间数据速度变慢 | 无论首尾还是中间数据,访问速度都很快(高效) |
| 元素位置调整 | 无法简单调整单个元素的位置 | 可通过 修改元素关联的分数 ,灵活调整元素位置 |
| 内存消耗 | 内存占用相对较低 | 内存消耗更高(因需额外存储分数及维护有序结构) |
- 总结
- sorted set 基于普通 set 扩展,核心特性是「元素唯一 + 关联分数 + 有序排列」;
- 与 list 同为有序且支持范围查询,但底层实现、访问性能、功能灵活性、内存消耗均有显著差异;
- sorted set 更适合需要基于分数排序、高效查询中间数据的场景,代价是更高的内存占用。
4.5.2 命令
-
增加元素
向有序集合中加入一个元素和该元素的分数,如果该元素已经存在则会用新的分数替换原有的分数。返回值是新加入到集合中的元素个数,不包含之前已经存在的元素。ZADD key score member [score member ...]

2.获取元素的分数
ZSCORE key member

- 获取排名在某个范围的元素列表
-
按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素(包含两端的元素)。
ZRANGE key start stop [WITHSCORES]
先将该有序集合的所有元素,按照分数(score)从小到大(升序)进行排序,形成一个有序的 "虚拟列表";
这个排序后的 "虚拟列表"下标从 0 开始(和 Redis List、Java 数组下标规则一致);
按照 start 和 stop 指定的下标范围,返回该范围内的所有元素(包含 start 和 stop 对应的元素);
可选参数 WITHSCORES:加上该参数后,返回结果会同时携带元素的值(member)和对应的分数(score),不加则只返回元素值。

-
照元素分数从大到小的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
ZREVRANGE key start stop [WITHSCORES]

-
获得指定分数范围的元素
ZRANGEBYSCORE key min max [WITHSCORES][LIMIT offset count]

(min / (max:给 min 或 max 加上括号,代表不包含该边界值(默认是包含边界值的)。
offset:是在符合分数范围的结果列表里的起始下标(从 0 开始)。
-
增加某个元素的分数
返回值是更改后的分数ZINCRBY key increment member

6.获得集合中元素的数量
ZCARD key

7.获得指定分数范围内的元素个数
ZCOUNT key min max

8.按照排名范围删除元素
按分数从小到大(升序)排序
ZREMRANGEBYRANK key start stop

-
按照分数范围删除元素
ZREMRANGEBYSCORE key min max

10.获取元素的排名
#从小到大
ZRANK key member
#从大到小
ZREVRANK key member


4.5.3 方法
- sorted set类型核心操作
|--------------------------|-----------------------------------------------------|-------------------|
| Redis 命令 | StringRedisTemplate 核心方法 | 功能说明 |
| zadd | opsForZSet().add(key, member, score) | 添加 / 更新有序集合元素 |
| zscore | opsForZSet().score(key, member) | 获取元素对应的分数 |
| zrange | opsForZSet().range(key, start, stop) | 升序获取指定排名范围元素 |
| zrange withscores | opsForZSet().rangeWithScores(key, start, stop) | 升序获取元素并携带分数 |
| zrevrange | opsForZSet().reverseRange(key, start, stop) | 降序获取指定排名范围元素 |
| zrangebyscore | opsForZSet().rangeByScore(key, min, max) | 按分数范围获取元素 |
| zrangebyscore withscores | opsForZSet().rangeByScoreWithScores(key, min, max) | 按分数范围获取元素并携带分数 |
| zincrby | opsForZSet().incrementScore(key, member, increment) | 给元素增加指定分数 |
| zcard | opsForZSet().size(key) | 获取有序集合元素总数 |
| zcount | opsForZSet().count(key, min, max) | 获取指定分数范围的元素个数 |
| zrem | opsForZSet().remove(key, member) | 删除有序集合中的指定元素 |
| zremrangebyrank | opsForZSet().removeRange(key, start, stop) | 按排名范围删除元素 |
| zremrangebyscore | opsForZSet().removeRangeByScore(key, min, max) | 按分数范围删除元素 |
| zrank | opsForZSet().rank(key, member) | 升序获取元素的排名(从 0 开始) |
| zrevrank | opsForZSet().reverseRank(key, member) | 降序获取元素的排名(从 0 开始) |
- 总结
SpringBoot 中操作 Redis Sorted Set 的核心套路是 stringRedisTemplate.opsForZSet().xxx()。 - 代码

package com.qcby.springbootredis;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import java.util.Set;
import java.util.stream.Collectors;
@SpringBootTest
public class SortedSetTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testSortedSetCoreCommands() {
String scoreboardKey = "scoreboard";
String hackersKey = "hackers";
String zsetKey = "zset";
try {
// 10.2.1 增加元素(ZADD)+ 获取元素分数(ZSCORE)
Boolean add1 = stringRedisTemplate.opsForZSet().add(scoreboardKey, "zhangsan", 80);
stringRedisTemplate.opsForZSet().add(scoreboardKey, "lisi", 89);
stringRedisTemplate.opsForZSet().add(scoreboardKey, "wangwu", 94);
System.out.println("新增 zhangsan:" + add1);
Boolean add2 = stringRedisTemplate.opsForZSet().add(scoreboardKey, "lisi", 97);
System.out.println("更新 lisi 分数:" + add2);
Double lisiScore = stringRedisTemplate.opsForZSet().score(scoreboardKey, "lisi");
System.out.println("lisi 的分数:" + lisiScore);
// 10.2.2 获得排名范围的元素列表(ZRANGE / ZREVRANGE)
Set<String> zrangeResult = stringRedisTemplate.opsForZSet().range(scoreboardKey, 0, 2);
System.out.println("升序排名 0-2 元素:" + zrangeResult);
Set<ZSetOperations.TypedTuple<String>> zrangeWithScores = stringRedisTemplate.opsForZSet().rangeWithScores(scoreboardKey, 0, 1);
Set<String> withScoresStr = zrangeWithScores.stream()
.map(t -> t.getValue() + "(" + t.getScore() + ")")
.collect(Collectors.toSet());
System.out.println("升序带分数:" + withScoresStr);
Set<String> zrevrangeResult = stringRedisTemplate.opsForZSet().reverseRange(scoreboardKey, 0, 2);
System.out.println("降序排名 0-2 元素:" + zrevrangeResult);
// 10.2.3 获得指定分数范围的元素(ZRANGEBYSCORE)
Set<ZSetOperations.TypedTuple<String>> zrangeByScore1 = stringRedisTemplate.opsForZSet().rangeByScoreWithScores(scoreboardKey, 90, 97);
Set<String> scoreWithStr1 = zrangeByScore1.stream()
.map(t -> t.getValue() + "(" + t.getScore() + ")")
.collect(Collectors.toSet());
System.out.println("分数 90-97 带分数:" + scoreWithStr1);
// 半开区间 90 <= score < 97
Set<ZSetOperations.TypedTuple<String>> zrangeByScore2 = stringRedisTemplate.opsForZSet().rangeByScoreWithScores(scoreboardKey, 90, 96.999);
Set<String> scoreWithStr2 = zrangeByScore2.stream()
.map(t -> t.getValue() + "(" + t.getScore() + ")")
.collect(Collectors.toSet());
System.out.println("分数 90-(97) 带分数:" + scoreWithStr2);
// 分页 LIMIT 1 2
Set<String> zrangeByScore3 = stringRedisTemplate.opsForZSet().rangeByScore(scoreboardKey, 70, 100, 1, 2);
System.out.println("分数 70-100 分页:" + zrangeByScore3);
// 10.2.4 增加元素分数(ZINCRBY)+ 集合元素数量(ZCARD)+ 分数范围元素个数(ZCOUNT)
Double newLisiScore = stringRedisTemplate.opsForZSet().incrementScore(scoreboardKey, "lisi", 4);
System.out.println("lisi 加4分后:" + newLisiScore);
Long zcardCount = stringRedisTemplate.opsForZSet().size(scoreboardKey);
System.out.println("集合元素个数:" + zcardCount);
Long zcount = stringRedisTemplate.opsForZSet().count(scoreboardKey, 80, 90);
System.out.println("分数 80-90 元素个数:" + zcount);
stringRedisTemplate.opsForZSet().add(hackersKey, "Alan Kay", 1940);
stringRedisTemplate.opsForZSet().add(hackersKey, "Richard Stallman", 1953);
stringRedisTemplate.opsForZSet().add(hackersKey, "Yukihiro Matsumoto", 1965);
stringRedisTemplate.opsForZSet().add(hackersKey, "Claude Shannon", 1916);
stringRedisTemplate.opsForZSet().add(hackersKey, "Linus Torvalds", 1969);
stringRedisTemplate.opsForZSet().add(hackersKey, "Alan Turing", 1912);
Set<String> hackersZrange = stringRedisTemplate.opsForZSet().range(hackersKey, 0, -1);
System.out.println("hackers 升序:" + hackersZrange);
Set<String> hackersZrevrange = stringRedisTemplate.opsForZSet().reverseRange(hackersKey, 0, -1);
System.out.println("hackers 降序:" + hackersZrevrange);
// 清空当前数据库
stringRedisTemplate.getConnectionFactory().getConnection().flushDb();
System.out.println("清空数据库成功");
// zset 示例
stringRedisTemplate.opsForZSet().add(zsetKey, "hello", 10.1);
stringRedisTemplate.opsForZSet().add(zsetKey, ":", 10.0);
stringRedisTemplate.opsForZSet().add(zsetKey, "zset", 9.0);
stringRedisTemplate.opsForZSet().add(zsetKey, "zset!", 11.0);
Long zsetCard = stringRedisTemplate.opsForZSet().size(zsetKey);
System.out.println("zset 元素个数:" + zsetCard);
Double zsetScore = stringRedisTemplate.opsForZSet().score(zsetKey, "zset");
System.out.println("zset 的分数:" + zsetScore);
Set<String> zsetZrange = stringRedisTemplate.opsForZSet().range(zsetKey, 0, -1);
System.out.println("zset 升序:" + zsetZrange);
Long zremCount = stringRedisTemplate.opsForZSet().remove(zsetKey, "zset!");
System.out.println("删除 zset!:" + zremCount);
Long zsetZcount = stringRedisTemplate.opsForZSet().count(zsetKey, 9.5, 10.5);
System.out.println("分数 9.5-10.5 元素个数:" + zsetZcount);
Set<String> zsetFinal = stringRedisTemplate.opsForZSet().range(zsetKey, 0, -1);
System.out.println("zset 删除后:" + zsetFinal);
// 10.2.5 其他命令(ZREMRANGEBYRANK / ZREMRANGEBYSCORE / ZRANK / ZREVRANK)
stringRedisTemplate.opsForZSet().add(scoreboardKey, "zhangsan", 80);
stringRedisTemplate.opsForZSet().add(scoreboardKey, "lisi", 97);
stringRedisTemplate.opsForZSet().add(scoreboardKey, "wangwu", 94);
Long zremByRankCount = stringRedisTemplate.opsForZSet().removeRange(scoreboardKey, 0, 1);
System.out.println("按排名删除 0-1:" + zremByRankCount);
Set<String> scoreboardAfterRankDel = stringRedisTemplate.opsForZSet().range(scoreboardKey, 0, -1);
System.out.println("删除后元素:" + scoreboardAfterRankDel);
stringRedisTemplate.opsForZSet().add(scoreboardKey, "zhangsan", 84);
Long zremByScoreCount = stringRedisTemplate.opsForZSet().removeRangeByScore(scoreboardKey, 80, 100);
System.out.println("按分数删除 80-100:" + zremByScoreCount);
stringRedisTemplate.opsForZSet().add(scoreboardKey, "zhangsan", 80);
stringRedisTemplate.opsForZSet().add(scoreboardKey, "lisi", 97);
Long lisiRank = stringRedisTemplate.opsForZSet().rank(scoreboardKey, "lisi");
System.out.println("lisi 升序排名:" + lisiRank);
Long zhangsanRevRank = stringRedisTemplate.opsForZSet().reverseRank(scoreboardKey, "zhangsan");
System.out.println("zhangsan 降序排名:" + zhangsanRevRank);
} catch (Exception e) {
e.printStackTrace();
}
}
}