Redis 有序聚集(Sorted Set)

打印 上一主题 下一主题

主题 2012|帖子 2012|积分 6036

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
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 keyO(1)获取有序聚会合成员的数量 。zscore key memberO(1)获取有序聚会合指定成员的分数 。zrank key memberO(log(n)),n是当前有序聚集成员个数获取有序聚会合指定成员的排名(从0开始,按分数从小到大排序) 。zrevrank key memberO(log(n)),n是当前有序聚集成员个数获取有序聚会合指定成员的排名(从0开始,按分数从大到小排序) 。zrem key member [member …]O(k*log(n)),k是删除成员的个数,n是当前有序聚集成员个数从有序聚会合删除一个或多个成员 。zincrby key increment memberO(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 maxO(log(n)),n是当前有序聚集成员个数计算有序聚会合分数在指定区间内的成员数量 。zremrangebyrank key start endO(log(n)+k),k是要删除的成员个数,n是当前有序聚集成员个数根据成员排名范围,从有序聚会合删除指定区间内的成员 。zremrangebyscore key min maxO(log(n)+k),k是要删除的成员个数,n是当前有序聚集成员个数根据分数范围,从有序聚会合删除分数在指定区间内的成员 。 有序聚集的核心下令围绕元素分数(score)的增删查改、排序和聚集运算睁开,主要分为以下几类:
1. 元素操作



  • ZADD:添加或更新元素及其分数(支持 NX​、XX​、INCR​ 等选项)。
    1. ZADD leaderboard 100 "player1"  # 添加玩家1,分数100
    复制代码
  • ZREM:移除指定元素。
  • ZINCRBY:增减元素的分数(如积分变动)。
    1. ZINCRBY leaderboard 50 "player1"  # 玩家1分数+50
    复制代码
  • ZSCORE:获取元素分数。
2. 范围查询与排序



  • ZRANGE / ZREVRANGE:按分数升序/降序返回指定区间元素。
  • ZRANGEBYSCORE:按分数区间筛选元素(支持开闭区间)。
  • ZRANK / ZREVRANK:获取元素的正序/逆序排名。
3. 聚集运算



  • ZINTERSTORE / ZUNIONSTORE:计算多个有序聚集的交集/并集,结果存储到新键。
    1. 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. 排行榜系统



  • 代码案例: 音乐排行榜 统计音乐播放量以及干系排名信息
    1. package com.example.redis.sortedset;
    2. import redis.clients.jedis.Jedis;
    3. import redis.clients.jedis.Tuple;
    4. import java.util.Set;
    5. /**
    6. * 描述: 音乐排行榜系统
    7. * 使用 Redis 有序集合(Sorted Set)存储歌曲排行榜,key 为 RANKING_KEY(例如:"music:ranking")。
    8. * 每个元素的 score 用于表示歌播放量、点赞数等,可以根据具体业务情况计算。
    9. * 使用 zadd 命令添加或更新歌曲得分,zadd 如果已有成员会更新其得分,保证同一首歌曲不会重复添加。
    10. *  clickPlay 方法,每次点击播放时使用 jedis.zincrby(RANKING_KEY, 1, songId) 将歌曲的播放量加 1。
    11. * 使用 zrevrange 按分数降序查询排行榜,从而获取热门歌曲;同时可使用 zrevrank、zscore 获取歌曲排名和得分。
    12. * 示例中 main 函数模拟了添加/更新操作,并展示了如何获取排行榜、查询具体歌曲信息等操作
    13. * @author ZHOUXIAOYUE
    14. * @date 2025/4/21 15:19
    15. */
    16. public class MusicRankingSystem {
    17.     // 定义排行榜对应的 Redis key
    18.     private static final String RANKING_KEY = "music:ranking";
    19.     private Jedis jedis;
    20.     public MusicRankingSystem() {
    21.         // 连接 Redis 服务,默认地址 localhost:6379
    22.         jedis = new Jedis("localhost", 6379);
    23.     }
    24.     /**
    25.      * 添加歌曲到排行榜
    26.      * 如果歌曲已存在,则不会重复添加,初始播放量可为 0(或指定值)
    27.      *
    28.      * @param songId    歌曲 ID
    29.      * @param playCount 初始播放量
    30.      */
    31.     public void addSong(String songId, double playCount) {
    32.         // 判断歌曲是否已存在
    33.         if (jedis.zscore(RANKING_KEY, songId) == null) {
    34.             jedis.zadd(RANKING_KEY, playCount, songId);
    35.             System.out.println("添加歌曲 " + songId + ",初始播放量为:" + playCount);
    36.         } else {
    37.             System.out.println("歌曲 " + songId + " 已存在,当前播放量为:" + jedis.zscore(RANKING_KEY, songId));
    38.         }
    39.     }
    40.     /**
    41.      * 模拟点击播放歌曲,每点击一次歌曲播放量自动+1
    42.      *
    43.      * @param songId 歌曲 ID
    44.      */
    45.     public void clickPlay(String songId) {
    46.         // 使用 zincrby 实现播放量自动加 1
    47.         double newPlayCount = jedis.zincrby(RANKING_KEY, 1, songId);
    48.         System.out.println("歌曲 " + songId + " 播放一次,当前播放量为:" + newPlayCount);
    49.     }
    50.     /**
    51.      * 获取排行榜前 N 名的歌曲(播放量从高到低排序)
    52.      *
    53.      * @param top 前 N 名
    54.      * @return 歌曲 ID 集合
    55.      */
    56.     public Set<String> getTopSongs(int top) {
    57.         // 使用 zrevrange 按播放量从高到低排序
    58.         Set<String> topSongs = jedis.zrevrange(RANKING_KEY, 0, top - 1);
    59.         System.out.println("排行榜前 " + top + " 名:" + topSongs);
    60.         return topSongs;
    61.     }
    62.     /**
    63.      * 获取某个歌曲的排名(排名从 1 开始,播放量越高排名越靠前)
    64.      *
    65.      * @param songId 歌曲 ID
    66.      * @return 歌曲排名,歌曲不存在返回 -1
    67.      */
    68.     public long getSongRank(String songId) {
    69.         Long rank = jedis.zrevrank(RANKING_KEY, songId);
    70.         if (rank == null) {
    71.             System.out.println("歌曲 " + songId + " 不在排行榜中。");
    72.             return -1;
    73.         } else {
    74.             System.out.println("歌曲 " + songId + " 的排名是:" + (rank + 1));
    75.             return rank + 1;
    76.         }
    77.     }
    78.     /**
    79.      * 获取某个歌曲的播放量
    80.      *
    81.      * @param songId 歌曲 ID
    82.      * @return 播放量
    83.      */
    84.     public double getSongPlayCount(String songId) {
    85.         Double playCount = jedis.zscore(RANKING_KEY, songId);
    86.         System.out.println("歌曲 " + songId + " 的播放量为:" + playCount);
    87.         return playCount == null ? 0 : playCount;
    88.     }
    89.     /**
    90.      * 获取排行榜中指定区间的歌曲及播放量
    91.      *
    92.      * @param start 区间开始索引(从 0 开始)
    93.      * @param end   区间结束索引
    94.      */
    95.     public void getSongsWithPlayCount(int start, int end) {
    96.         Set<Tuple> tuples = jedis.zrevrangeWithScores(RANKING_KEY, start, end);
    97.         System.out.println("排行榜区间 [" + start + ", " + end + "] 内的歌曲信息:");
    98.         for (Tuple tuple : tuples) {
    99.             System.out.println("歌曲 ID:" + tuple.getElement() + ",播放量:" + tuple.getScore());
    100.         }
    101.     }
    102.     /**
    103.      * 关闭 Jedis 连接
    104.      */
    105.     public void close() {
    106.         if (jedis != null) {
    107.             jedis.close();
    108.         }
    109.     }
    110.     public static void main(String[] args) {
    111.         MusicRankingSystem rankingSystem = new MusicRankingSystem();
    112.         // 添加几首歌曲到排行榜,初始播放量设置为 0
    113.         rankingSystem.addSong("song:101", 0);
    114.         rankingSystem.addSong("song:102", 0);
    115.         rankingSystem.addSong("song:103", 0);
    116.         rankingSystem.addSong("song:104", 0);
    117.         rankingSystem.addSong("song:105", 0);
    118.         // 模拟点击播放,不同歌曲点击次数不同
    119.         rankingSystem.clickPlay("song:101");
    120.         rankingSystem.clickPlay("song:102");
    121.         rankingSystem.clickPlay("song:101");
    122.         rankingSystem.clickPlay("song:103");
    123.         rankingSystem.clickPlay("song:102");
    124.         rankingSystem.clickPlay("song:101");
    125.         rankingSystem.clickPlay("song:104");
    126.         rankingSystem.clickPlay("song:105");
    127.         rankingSystem.clickPlay("song:103");
    128.         rankingSystem.clickPlay("song:105");
    129.         // 获取排行榜前 3 名的歌曲
    130.         rankingSystem.getTopSongs(3);
    131.         // 获取指定歌曲的排名与播放量
    132.         rankingSystem.getSongRank("song:101");
    133.         rankingSystem.getSongPlayCount("song:101");
    134.         // 获取排行榜中索引 0~4 区间内的歌曲信息
    135.         rankingSystem.getSongsWithPlayCount(0, 4);
    136.         // 关闭连接
    137.         rankingSystem.close();
    138.     }
    139. }
    复制代码
  • 实现:以用户积分作为分数,利用 ZREVRANGE​ 获取排名前 N 的用户。
    1. ZREVRANGE leaderboard 0 9 WITHSCORES  # 获取Top10玩家及分数
    复制代码
2. 优先级队列



  • 实现:使命分数表示优先级,通过 BZPOPMAX​ 优先处理高优先级使命。
3. 时间轴/变乱排序



  • 实现:以时间戳为分数,存储用户行为日志,按时间范围查询。
    1. ZADD user:1001:actions 1620000000 "login"
    2. ZRANGEBYSCORE user:1001:actions 1620000000 1620003600  # 查询某时间段行为
    复制代码
4. 动态统计与过滤



  • 实现:统计分数区间内元素(如商品代价区间)。
    1. ZCOUNT products 100 500  # 统计价格在100-500的商品数量
    复制代码
5. 数据去重与聚合



  • 实现:通过 ZINTERSTORE​ 计算用户兴趣标签的交集。

四、调优发起


  • 内存优化

    • 小规模数据优先使用 ziplist,调整 zset-max-ziplist-entries​ 和 zset-max-ziplist-value​ 参数。

  • 性能优化

    • 高频写入场景发起增大 ziplist 阈值,淘汰跳表转换开销。

  • 查询优化

    • 避免全量遍历(如 ZRANGE 0 -1​),改用分页或迭代器(ZSCAN​)。




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

鼠扑

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表