Redis ZSet 数据结构深度解析:原理、实现与实战全揭密!

一、前言:为什么要学习 ZSet?

在 Redis 的五大基础数据类型中,ZSet(Sorted Set,有序集合)是一种非常强大而灵活的数据结构,广泛应用于排行榜、延时队列、权重排名等场景。

如果说 String 是 Redis 的“最小原子”,那么 ZSet 就是 Redis 的“重量级选手”——不仅能存数据,还能排序查询,这正是它的魅力所在!

二、ZSet 是什么?和 Set 有啥区别?

ZSet = Set + Score + 排序!

特性 Set ZSet
是否唯一 是(按成员)
是否可排序 是(按 score)
是否可以按分值查找
是否支持范围查找

ZSet 的定义:每个元素(成员)都关联一个称为 score 的 double 类型的分数,Redis 会按 score 从小到大自动排序。

ZADD ranking 100 Alice
ZADD ranking 200 Bob
ZADD ranking 150 Tom

最终集合顺序为:Alice(100) -> Tom(150) -> Bob(200)

三、ZSet 的底层数据结构解析

ZSet 在 Redis 内部是由两种数据结构组合实现的:

1. 哈希表(dict)

  • 成员(member)作为 key,对应的分数(score)作为 value。
  • 时间复杂度:O(1)

2. 跳表(SkipList)

跳表是一种多层链表结构,通过层级索引提升搜索效率。

Redis 中跳表最多支持 32 层。每个节点包含:

  • 分数(score)
  • 成员(member)
  • 多个 forward 指针(指向同层的下一个节点)
  • backward 指针(支持反向查找)
  • span(支持排名计算)

四、ZSet 的编码方式(Redis 版本差异)

❗ Redis 7.0 重要变更
在 Redis 7.0 中,取消了 ziplist 编码,统一采用 skiplist + dict 作为底层结构,性能更稳定。

旧版本(Redis < 7.0)编码规则

1.ziplist(压缩列表)

  • 元素少(<128 个)且每个元素较短(<64 字节)时使用。
  • 内存占用小,适用于小集合。

2.skiplist + dict(标准结构)

  • 元素多或 score 分布跨度大时使用,支持高效范围查询。

查看编码命令:

OBJECT ENCODING myZset

五、ZSet 核心命令详解

命令 说明
ZADD key score member 添加元素
ZREM key member 删除元素
ZSCORE key member 获取分数
ZRANK key member 获取排名
ZREVRANK key member 获取倒序排名
ZRANGE key start stop [WITHSCORES] 获取排名区间元素
ZREVRANGE key start stop [WITHSCORES] 获取倒序区间元素
ZRANGEBYSCORE key min max 获取分数区间元素
ZREMRANGEBYRANK key start stop 删除排名区间元素
ZREMRANGEBYSCORE key min max 删除分数区间元素
ZINCRBY key increment member 分数自增

Java 操作 ZSet 示例(Jedis 客户端)

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class RedisZSetDemo {
    public static void main(String[] args) {
        // 连接 Redis
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 1. 添加元素
        jedis.zadd("ranking", 100, "Alice");
        jedis.zadd("ranking", 200, "Bob");
        jedis.zadd("ranking", 150, "Tom");
        
        // 2. 获取正序排名区间元素(带分数)
        Set<Tuple> rangeResult = jedis.zrangeWithScores("ranking", 0, -1);
        System.out.println("正序排名结果:");
        for (Tuple tuple : rangeResult) {
            System.out.println(tuple.getElement() + " -> " + tuple.getScore());
        }
        
        // 3. 分数自增
        jedis.zincrby("ranking", 50, "Tom");
        double newScore = jedis.zscore("ranking", "Tom");
        System.out.println("Tom 新分数:" + newScore);
        
        // 4. 获取倒序排名前 2 名
        Set<Tuple> revRange = jedis.zrevrangeWithScores("ranking", 0, 1);
        System.out.println("倒序前 2 名:");
        for (Tuple tuple : revRange) {
            System.out.println(tuple.getElement() + " -> " + tuple.getScore());
        }
        
        jedis.close();
    }
}

六、ZSet 的实战应用场景

1. 排行榜系统(如游戏积分排名)

# 命令行示例
ZADD leaderboard 3000 Jack
ZADD leaderboard 2800 Rose
ZADD leaderboard 3200 Lucy
ZREVRANGE leaderboard 0 2 WITHSCORES  # 获取前三名

Java 实现排行榜查询

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class LeaderboardSystem {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 初始化排行榜
        jedis.zadd("game:leaderboard", 3000, "Jack");
        jedis.zadd("game:leaderboard", 2800, "Rose");
        jedis.zadd("game:leaderboard", 3200, "Lucy");
        
        // 查询前三名(倒序)
        Set<Tuple> top3 = jedis.zrevrangeWithScores("game:leaderboard", 0, 2);
        System.out.println("游戏排行榜 TOP3:");
        int rank = 1;
        for (Tuple tuple : top3) {
            System.out.println(rank + ". " + tuple.getElement() + " - 积分:" + tuple.getScore());
            rank++;
        }
        
        jedis.close();
    }
}

2. 延迟任务队列

# 命令行示例(时间戳为任务执行时间)
ZADD delay_queue 1710000000 "task_1"
ZADD delay_queue 1710000500 "task_2"
ZRANGEBYSCORE delay_queue -inf 1710000500  # 获取可执行任务

Java 实现延迟任务处理

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class DelayTaskQueue {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 添加延迟任务(时间戳为秒级)
        long now = System.currentTimeMillis() / 1000;
        jedis.zadd("delay:queue", now + 10, "sendEmail");  // 10秒后执行
        jedis.zadd("delay:queue", now + 30, "cleanCache"); // 30秒后执行
        
        // 获取当前可执行的任务
        Set<Tuple> executableTasks = jedis.zrangeByScoreWithScores("delay:queue", 0, now);
        System.out.println("当前可执行任务:");
        for (Tuple task : executableTasks) {
            System.out.println("任务:" + task.getElement() + ",计划执行时间:" + task.getScore());
            // 处理任务后删除
            jedis.zrem("delay:queue", task.getElement());
        }
        
        jedis.close();
    }
}

3. 热度排序推荐(类似方法就不提供相关代码)

将点赞/浏览等计算后作为 score,实现实时推荐排序。

4. 用户活跃度排序(类似方法就不提供相关代码)

用户活跃行为计分后,存入 ZSet,取前 N 名。

七、ZSet 跳表实现原理

数据结构定义(C 语言)

typedef struct zskiplistNode {
    sds ele;          // 成员字符串
    double score;     // 分数
    struct zskiplistNode *backward;  // 反向指针
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 正向指针
        unsigned int span;             // 跨距(用于计算排名)
    } level[];  // 层级数组(柔性数组,长度由插入时随机决定)
} zskiplistNode;

特性

  • 多层索引: 通过随机层级减少搜索次数,平均搜索复杂度 O (logN)。
  • span 字段: 记录节点间跨距,可快速计算成员排名。
  • 双向指针: forward 支持正向遍历,backward 支持反向遍历。

插入过程

  1. 随机生成层级: 通过抛硬币算法(概率 1/2)决定层级,最大 32 层。
  2. 定位插入点: 从最高层开始向下查找,记录各层的前驱节点。
  3. 更新指针: 插入新节点并更新前驱节点的 forward 指针及 span。

八、ZSet 的复杂度分析

操作 平均复杂度 说明
ZADD O(logN) 插入并维护跳表索引
ZREM O(logN) 删除节点并更新索引
ZRANK O(logN) 通过跳表层级快速定位排名
ZRANGE O(logN + M) 定位起始点后遍历M个元素
ZSCORE O(1) 直接查询哈希表
ZINCRBY O(logN) 分数更新后调整跳表节点位置

九、持久化与内存管理

RDB 快照

将 ziplist/skiplist 数据结构保存为二进制快照。

AOF 日志

记录操作命令,如:

ZADD leaderboard 3000 Jack

内存占用查询

MEMORY USAGE leaderboard

十、ZSet 优化技巧

  • 使用 ZINCRBY 替代重复 ZADD
  • 控制成员大小和数量,避免升级为跳表
  • 设置自动清理策略:
ZREMRANGEBYRANK leaderboard 100 -1

十一、常见问题解析

1. score 精度丢失

建议使用整数 score *1000 来控制小数误差。

2. 排序异常?

确认 score 无重复,或成员字典序排序无误。

3. 元素过多?

定期清理低排名元素 + 设置最大长度限制。

十二、ZSet 与其他结构对比

功能需求 推荐结构
唯一值集合,无排序 Set
唯一值集合,有排序 ZSet
需要权重排行榜 ZSet
映射字段 Hash
栈/队列 List

十三、源码入口与拓展学习

ZSet 相关源码:t_zset.c,跳表实现:zskiplist.c

阅读建议:

  • Redis 源码注释版
  • redis.io 官方文档
  • 相关博客(如美团/字节的中间件实践)

✅ 十四、总结

ZSet 是 Redis 的黄金数据结构之一。它集合了哈希表的高效查询与跳表的有序特性,是实现高性能排行榜、调度系统、热度算法的首选结构。掌握 ZSet,不仅能让你 Redis 玩得更转,还能提升你的架构设计能力。

一文深入 Redis ZSet 的底层结构、命令原理与实战技巧。适合初学者入门进阶、也适合架构师优化系统。收藏 + 关注不迷路!

你可能感兴趣的:(Redis,redis,数据结构,缓存)