Redis 有序集合(Sorted Set)
以下从基础命令、内部编码和使用场景三个维度对 Redis 有序集合进行详细解析:
一、基础命令
命令 | 时间复杂度 | 命令含义 |
---|---|---|
zadd key score member [score member …] | O ( k × l o g ( n ) ) O(k×log(n)) O(k×log(n)),k是添加成员的个数,n是当前有序集合成员个数 | 向有序集合(Sorted Set)中添加一个或多个成员,同时为每个成员关联一个分数(score),根据分数对成员进行排序 。 |
zcard key | O(1) | 获取有序集合中成员的数量 。 |
zscore key member | O(1) | 获取有序集合中指定成员的分数 。 |
zrank key member | O(log(n)),n是当前有序集合成员个数 | 获取有序集合中指定成员的排名(从0开始,按分数从小到大排序) 。 |
zrevrank key member | O(log(n)),n是当前有序集合成员个数 | 获取有序集合中指定成员的排名(从0开始,按分数从大到小排序) 。 |
zrem key member [member …] | O(k*log(n)),k是删除成员的个数,n是当前有序集合成员个数 | 从有序集合中删除一个或多个成员 。 |
zincrby key increment member | O(log(n)),n是当前有序集合成员个数 | 为有序集合中指定成员的分数加上指定的增量 。 |
zrange key start end [withscores] | O(log(n)+k),k是要获取的成员个数,n是当前有序集合成员个数 | 按照分数从小到大的顺序,获取有序集合中指定区间内的成员,可选择是否返回成员的分数 。 |
zrevrange key start end [withscores] | O(log(n)+k),k是要获取的成员个数,n是当前有序集合成员个数 | 按照分数从大到小的顺序,获取有序集合中指定区间内的成员,可选择是否返回成员的分数 。 |
zrangebyscore key min max [withscores] [limit offset count] | O(log(n)+k),k是要获取的成员个数,n是当前有序集合成员个数 | 按照分数范围,获取有序集合中分数在指定区间内的成员,可选择是否返回成员的分数,还可通过limit指定返回结果的偏移量和数量 。 |
zrevrangebyscore key max min [withscores] [limit offset count] | O(log(n)+k),k是要获取的成员个数,n是当前有序集合成员个数 | 按照分数范围,从大到小获取有序集合中分数在指定区间内的成员,可选择是否返回成员的分数,还可通过limit指定返回结果的偏移量和数量 。 |
zcount key min max | O(log(n)),n是当前有序集合成员个数 | 计算有序集合中分数在指定区间内的成员数量 。 |
zremrangebyrank key start end | O(log(n)+k),k是要删除的成员个数,n是当前有序集合成员个数 | 根据成员排名范围,从有序集合中删除指定区间内的成员 。 |
zremrangebyscore key min max | O(log(n)+k),k是要删除的成员个数,n是当前有序集合成员个数 | 根据分数范围,从有序集合中删除分数在指定区间内的成员 。 |
有序集合的核心命令围绕元素分数(score)的增删查改、排序和集合运算展开,主要分为以下几类:
1. 元素操作
-
ZADD:添加或更新元素及其分数(支持
NX
、XX
、INCR
等选项)。ZADD leaderboard 100 "player1" # 添加玩家1,分数100
-
ZREM:移除指定元素。
-
ZINCRBY:增减元素的分数(如积分变动)。
ZINCRBY leaderboard 50 "player1" # 玩家1分数+50
-
ZSCORE:获取元素分数。
2. 范围查询与排序
- ZRANGE / ZREVRANGE:按分数升序/降序返回指定区间元素。
- ZRANGEBYSCORE:按分数区间筛选元素(支持开闭区间)。
- ZRANK / ZREVRANK:获取元素的正序/逆序排名。
3. 集合运算
-
ZINTERSTORE / ZUNIONSTORE:计算多个有序集合的交集/并集,结果存储到新键。
ZINTERSTORE result 2 key1 key2 # 计算key1和key2的交集
4. 统计与删除
- ZCARD:获取集合元素总数。
- ZCOUNT:统计指定分数区间内的元素数量。
- ZREMRANGEBYRANK / ZREMRANGEBYSCORE:按排名或分数区间批量删除元素。
5. 阻塞操作
- BZPOPMAX / BZPOPMIN:阻塞弹出最高/最低分元素,适用于优先级队列。
二、内部编码
Redis 根据数据规模动态选择编码方式,以平衡内存和性能:
1. ziplist(压缩列表)
-
触发条件:
- 元素数量 ≤
zset-max-ziplist-entries
(默认 128)。 - 每个元素的成员(member)长度 ≤
zset-max-ziplist-value
(默认 64 字节)。
- 元素数量 ≤
-
特点:
- 内存紧凑,连续存储成员和分数,无指针开销。
- 增删操作时间复杂度为 O(n),适用于小规模数据。
2. skiplist(跳表)
-
触发条件:超出 ziplist 容量限制时自动转换。
-
特点:
- 跳表+哈希表组合实现:跳表维护有序结构(O(logN) 查询),哈希表存储成员到分数的映射(O(1) 查询)。
- 支持高效范围查询和动态数据插入。
3. 编码转换规则
- 只允许从 ziplist 转为 skiplist,不可逆。
- 可通过
OBJECT ENCODING key
查看当前编码类型。
三、使用场景
有序集合凭借唯一性和有序性,适用于以下典型场景:
1. 排行榜系统
-
代码案例: 音乐排行榜 统计音乐播放量以及相关排名信息
package com.example.redis.sortedset; import redis.clients.jedis.Jedis; import redis.clients.jedis.Tuple; import java.util.Set; /** * 描述: 音乐排行榜系统 * 使用 Redis 有序集合(Sorted Set)存储歌曲排行榜,key 为 RANKING_KEY(例如:"music:ranking")。 * 每个元素的 score 用于表示歌播放量、点赞数等,可以根据具体业务情况计算。 * 使用 zadd 命令添加或更新歌曲得分,zadd 如果已有成员会更新其得分,保证同一首歌曲不会重复添加。 * clickPlay 方法,每次点击播放时使用 jedis.zincrby(RANKING_KEY, 1, songId) 将歌曲的播放量加 1。 * 使用 zrevrange 按分数降序查询排行榜,从而获取热门歌曲;同时可使用 zrevrank、zscore 获取歌曲排名和得分。 * 示例中 main 函数模拟了添加/更新操作,并展示了如何获取排行榜、查询具体歌曲信息等操作 * @author ZHOUXIAOYUE * @date 2025/4/21 15:19 */ public class MusicRankingSystem { // 定义排行榜对应的 Redis key private static final String RANKING_KEY = "music:ranking"; private Jedis jedis; public MusicRankingSystem() { // 连接 Redis 服务,默认地址 localhost:6379 jedis = new Jedis("localhost", 6379); } /** * 添加歌曲到排行榜 * 如果歌曲已存在,则不会重复添加,初始播放量可为 0(或指定值) * * @param songId 歌曲 ID * @param playCount 初始播放量 */ public void addSong(String songId, double playCount) { // 判断歌曲是否已存在 if (jedis.zscore(RANKING_KEY, songId) == null) { jedis.zadd(RANKING_KEY, playCount, songId); System.out.println("添加歌曲 " + songId + ",初始播放量为:" + playCount); } else { System.out.println("歌曲 " + songId + " 已存在,当前播放量为:" + jedis.zscore(RANKING_KEY, songId)); } } /** * 模拟点击播放歌曲,每点击一次歌曲播放量自动+1 * * @param songId 歌曲 ID */ public void clickPlay(String songId) { // 使用 zincrby 实现播放量自动加 1 double newPlayCount = jedis.zincrby(RANKING_KEY, 1, songId); System.out.println("歌曲 " + songId + " 播放一次,当前播放量为:" + newPlayCount); } /** * 获取排行榜前 N 名的歌曲(播放量从高到低排序) * * @param top 前 N 名 * @return 歌曲 ID 集合 */ public Set<String> getTopSongs(int top) { // 使用 zrevrange 按播放量从高到低排序 Set<String> topSongs = jedis.zrevrange(RANKING_KEY, 0, top - 1); System.out.println("排行榜前 " + top + " 名:" + topSongs); return topSongs; } /** * 获取某个歌曲的排名(排名从 1 开始,播放量越高排名越靠前) * * @param songId 歌曲 ID * @return 歌曲排名,歌曲不存在返回 -1 */ public long getSongRank(String songId) { Long rank = jedis.zrevrank(RANKING_KEY, songId); if (rank == null) { System.out.println("歌曲 " + songId + " 不在排行榜中。"); return -1; } else { System.out.println("歌曲 " + songId + " 的排名是:" + (rank + 1)); return rank + 1; } } /** * 获取某个歌曲的播放量 * * @param songId 歌曲 ID * @return 播放量 */ public double getSongPlayCount(String songId) { Double playCount = jedis.zscore(RANKING_KEY, songId); System.out.println("歌曲 " + songId + " 的播放量为:" + playCount); return playCount == null ? 0 : playCount; } /** * 获取排行榜中指定区间的歌曲及播放量 * * @param start 区间开始索引(从 0 开始) * @param end 区间结束索引 */ public void getSongsWithPlayCount(int start, int end) { Set<Tuple> tuples = jedis.zrevrangeWithScores(RANKING_KEY, start, end); System.out.println("排行榜区间 [" + start + ", " + end + "] 内的歌曲信息:"); for (Tuple tuple : tuples) { System.out.println("歌曲 ID:" + tuple.getElement() + ",播放量:" + tuple.getScore()); } } /** * 关闭 Jedis 连接 */ public void close() { if (jedis != null) { jedis.close(); } } public static void main(String[] args) { MusicRankingSystem rankingSystem = new MusicRankingSystem(); // 添加几首歌曲到排行榜,初始播放量设置为 0 rankingSystem.addSong("song:101", 0); rankingSystem.addSong("song:102", 0); rankingSystem.addSong("song:103", 0); rankingSystem.addSong("song:104", 0); rankingSystem.addSong("song:105", 0); // 模拟点击播放,不同歌曲点击次数不同 rankingSystem.clickPlay("song:101"); rankingSystem.clickPlay("song:102"); rankingSystem.clickPlay("song:101"); rankingSystem.clickPlay("song:103"); rankingSystem.clickPlay("song:102"); rankingSystem.clickPlay("song:101"); rankingSystem.clickPlay("song:104"); rankingSystem.clickPlay("song:105"); rankingSystem.clickPlay("song:103"); rankingSystem.clickPlay("song:105"); // 获取排行榜前 3 名的歌曲 rankingSystem.getTopSongs(3); // 获取指定歌曲的排名与播放量 rankingSystem.getSongRank("song:101"); rankingSystem.getSongPlayCount("song:101"); // 获取排行榜中索引 0~4 区间内的歌曲信息 rankingSystem.getSongsWithPlayCount(0, 4); // 关闭连接 rankingSystem.close(); } }
-
实现:以用户积分作为分数,利用
ZREVRANGE
获取排名前 N 的用户。ZREVRANGE leaderboard 0 9 WITHSCORES # 获取Top10玩家及分数
2. 优先级队列
- 实现:任务分数表示优先级,通过
BZPOPMAX
优先处理高优先级任务。
3. 时间轴/事件排序
-
实现:以时间戳为分数,存储用户行为日志,按时间范围查询。
ZADD user:1001:actions 1620000000 "login" ZRANGEBYSCORE user:1001:actions 1620000000 1620003600 # 查询某时间段行为
4. 动态统计与过滤
-
实现:统计分数区间内元素(如商品价格区间)。
ZCOUNT products 100 500 # 统计价格在100-500的商品数量
5. 数据去重与聚合
- 实现:通过
ZINTERSTORE
计算用户兴趣标签的交集。
四、调优建议
-
内存优化:
- 小规模数据优先使用 ziplist,调整
zset-max-ziplist-entries
和zset-max-ziplist-value
参数。
- 小规模数据优先使用 ziplist,调整
-
性能优化:
- 高频写入场景建议增大 ziplist 阈值,减少跳表转换开销。
-
查询优化:
- 避免全量遍历(如
ZRANGE 0 -1
),改用分页或迭代器(ZSCAN
)。
- 避免全量遍历(如