Skip to content
清晨的一缕阳光
返回

Redis ZSet 数据类型详解

Redis ZSet 数据类型详解

ZSet(Sorted Set)是 Redis 最强大的数据类型之一,结合了 Set 的去重特性和分数排序能力。本文将深入 ZSet 的底层跳表结构,掌握排行榜等核心应用场景。

一、ZSet 基础概念

1.1 什么是 ZSet

ZSet 是有序集合,每个元素关联一个分数(score):

# 添加元素
ZADD leaderboard 100 "Alice"
ZADD leaderboard 200 "Bob"
ZADD leaderboard 150 "Charlie"

# 获取排名
ZRANK leaderboard "Alice"        # 0(从低到高)
ZREVRANK leaderboard "Alice"     # 2(从高到低)

# 获取范围
ZRANGE leaderboard 0 -1 WITHSCORES
# 1) "Alice"
# 2) "100"
# 3) "Charlie"
# 4) "150"
# 5) "Bob"
# 6) "200"

1.2 应用场景

场景说明
排行榜游戏排名、销售排名
优先级队列任务调度、消息优先级
时间线动态排序、Feed 流
延迟队列定时任务、延迟消息

二、底层数据结构

2.1 压缩列表(ziplist)

小数据量使用

// ziplist 结构
┌────────────────────────────────────┐
│ zlbytes │ zltail │ zlen │ entries │
4B4B2B   │ 可变    │
└────────────────────────────────────┘

entries:
┌─────────┬─────────┬─────────┬─────────┐
│ score1  │ member1 │ score2  │ member2 │
"100""Alice""200""Bob"
└─────────┴─────────┴─────────┴─────────┘

触发条件

# Redis 配置
zset-max-intset-entries 128    # 元素数 < 128
zset-max-ziplist-value 64      # 元素值 < 64 字节

2.2 跳表(skiplist)

大数据量使用

// 跳表节点
typedef struct zskiplistNode {
    sds ele;                    // 元素
    double score;               // 分数
    struct zskiplistNode *backward;  // 后退指针
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 前进指针
        unsigned int span;              // 跨度
    } level[];                  // 层级数组
} zskiplistNode;

// 跳表结构
typedef struct zskiplist {
    struct zskiplistNode *header;   // 头节点
    struct zskiplistNode *tail;     // 尾节点
    unsigned long length;           // 长度
    int level;                      // 最大层级
} zskiplist;

2.3 跳表结构图示

跳表示例(4 层):

Level 3:  H ──────────────────────────────────▶ TAIL
Level 2:  H ──────▶ N2 ───────────────────────▶ TAIL
Level 1:  H ──▶ N1 ──▶ N2 ──────▶ N3 ─────────▶ TAIL
Level 0:  H ──▶ N1 ──▶ N2 ──▶ N3 ──▶ N4 ─────▶ TAIL

H = Header (头节点)
N1 = Node(score=100, "Alice")
N2 = Node(score=150, "Charlie")
N3 = Node(score=200, "Bob")
N4 = Node(score=250, "David")

2.4 跳表优势

操作时间复杂度说明
查找O(log N)多层索引加速
插入O(log N)自动排序
删除O(log N)定位后删除
范围查询O(log N + M)M 为结果数量

为什么不用红黑树

跳表 vs 红黑树:
1. 实现简单
2. 范围查询更快
3. 并发友好
4. 内存局部性好

三、核心命令详解

3.1 添加元素

# 添加单个元素
ZADD key score member

# 添加多个元素
ZADD key 100 "A" 200 "B" 300 "C"

# 特殊选项
ZADD key NX 100 "A"      # 仅当不存在
ZADD key XX 100 "A"      # 仅当存在
ZADD key CH 100 "A"      # 返回变更的元素数
ZADD key INCR 100 "A"    # 自增模式(类似 ZINCRBY)

3.2 查询操作

# 按排名查询
ZRANGE key start stop [WITHSCORES]
ZREVRANGE key start stop [WITHSCORES]  # 逆序

# 按分数查询
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
ZREVRANGEBYSCORE key max min [WITHSCORES]

# 获取排名
ZRANK key member           # 从低到高
ZREVRANK key member        # 从高到低

# 获取分数
ZSCORE key member

示例

# 排行榜前 10 名
ZREVRANGE leaderboard 0 9 WITHSCORES

# 60-80 分的用户
ZRANGEBYSCORE scores 60 80 WITHSCORES

# 分页查询(每页 10 条)
ZRANGE leaderboard 0 9 WITHSCORES        # 第 1 页
ZRANGE leaderboard 10 19 WITHSCORES      # 第 2 页

3.3 删除操作

# 删除元素
ZREM key member [member ...]

# 删除排名范围
ZREMRANGEBYRANK key start stop

# 删除分数范围
ZREMRANGEBYSCORE key min max

# 示例
ZREM leaderboard "Alice"
ZREMRANGEBYRANK leaderboard 0 9     # 删除最后 10 名
ZREMRANGEBYSCORE leaderboard 0 60   # 删除不及格的

3.4 统计操作

# 元素数量
ZCARD key

# 分数范围数量
ZCOUNT key min max

# 分数求和
ZUNIONSTORE dest numkeys key [key ...]

# 分数交集
ZINTERSTORE dest numkeys key [key ...]

四、排行榜实现

4.1 基础排行榜

# 添加分数
ZADD game:leaderboard 1000 "player1"
ZADD game:leaderboard 2000 "player2"
ZADD game:leaderboard 1500 "player3"

# 获取前 10 名
ZREVRANGE game:leaderboard 0 9 WITHSCORES

# 获取个人排名
ZREVRANK game:leaderboard "player1"

# 获取个人分数
ZSCORE game:leaderboard "player1"

4.2 分页排行榜

# 分页查询
# 第 1 页(0-9)
ZRANGE leaderboard 0 9 WITHSCORES

# 第 2 页(10-19)
ZRANGE leaderboard 10 19 WITHSCORES

# 第 N 页
page = 3
page_size = 10
start = (page - 1) * page_size
stop = start + page_size - 1
ZRANGE leaderboard start stop WITHSCORES

4.3 附近排名

# 获取自己的排名
my_rank = ZREVRANK leaderboard "my_id"

# 获取前后各 5 名
start = max(0, my_rank - 5)
stop = my_rank + 5
ZRANGE leaderboard start stop WITHSCORES

4.4 多日排行榜

# 每日排行榜
ZADD leaderboard:2024-01-01 1000 "player1"
ZADD leaderboard:2024-01-02 1500 "player1"

# 总排行榜
ZUNIONSTORE leaderboard:total 2 leaderboard:2024-01-01 leaderboard:2024-01-02

# 周排行榜
ZUNIONSTORE leaderboard:week 7 leaderboard:2024-01-01 ... leaderboard:2024-01-07

五、高级应用

4.1 延迟队列

# 添加延迟任务(score = 执行时间戳)
current_time = time.time()
execute_time = current_time + 3600  # 1 小时后
ZADD delay:queue execute_time "task_id"

# 获取可执行的任务
ready_tasks = ZRANGEBYSCORE delay:queue 0 current_time

# 处理并删除
for task in ready_tasks:
    process(task)
    ZREM delay:queue task

Python 实现

import redis
import time

class DelayQueue:
    def __init__(self, redis_client, key):
        self.redis = redis_client
        self.key = key
    
    def add(self, task_id, delay_seconds):
        """添加延迟任务"""
        score = int(time.time() * 1000) + delay_seconds * 1000
        self.redis.zadd(self.key, {task_id: score})
    
    def poll(self, count=1):
        """获取可执行的任务"""
        now = int(time.time() * 1000)
        tasks = self.redis.zrangebyscore(self.key, 0, now, start=0, num=count)
        if tasks:
            self.redis.zrem(self.key, *tasks)
        return tasks

4.2 优先级队列

# 高优先级(分数小)先执行
ZADD priority:queue 1 "high_priority_task"
ZADD priority:queue 10 "normal_task"
ZADD priority:queue 100 "low_priority_task"

# 获取任务(优先级高的先出)
ZRANGE priority:queue 0 0

# 处理并删除
ZPOPMIN priority:queue

4.3 时间线 Feed 流

# 发布动态(score = 时间戳)
timestamp = time.time()
ZADD user:1001:feed timestamp "post_id_1"
ZADD user:1001:feed timestamp+1 "post_id_2"

# 获取最新动态
ZREVRANGE user:1001:feed 0 20

# 分页
ZREVRANGE user:1001:feed 0 19  # 第 1 页
ZREVRANGE user:1001:feed 20 39 # 第 2 页

4.4 权重评分

# 多维度评分
# score = 点赞数 * 1 + 评论数 * 2 + 分享数 * 3
post_score = likes * 1 + comments * 2 + shares * 3
ZADD post:ranking post_score "post_id"

# 热榜
ZREVRANGE post:ranking 0 49 WITHSCORES

六、性能优化

6.1 批量操作

# 不推荐:多次网络往返
ZADD key 100 "a"
ZADD key 200 "b"
ZADD key 300 "c"

# 推荐:批量添加
ZADD key 100 "a" 200 "b" 300 "c"

# 或使用 Pipeline
PIPELINE
  ZADD key 100 "a"
  ZADD key 200 "b"
  ZADD key 300 "c"
END

6.2 限制数量

# 排行榜只保留前 1000 名
ZADD leaderboard 1000 "new_player"

# 删除 1000 名之后
ZREMRANGEBYRANK leaderboard 1000 -1

6.3 定期清理

# 清理过期数据
ZREMRANGEBYSCORE leaderboard 0 old_timestamp

# 使用过期时间
EXPIRE leaderboard:2024-01-01 2592000  # 30 天

七、常见问题

Q1: ZSet 最大元素数?

理论最大值:2^32 - 1(约 40 亿)
实际建议:< 10 万

Q2: 分数精度?

# 分数是 double 类型
ZADD key 3.14159 "pi"
ZADD key 1.23e10 "large"

# 注意浮点数精度问题
# 建议使用整数(毫秒时间戳)

Q3: 相同分数排序?

分数相同时,按元素字典序排序

ZADD key 100 "b"
ZADD key 100 "a"

ZRANGE key 0 -1
# 1) "a"  # 字典序小的在前
# 2) "b"

Q4: 如何实现去重?

# ZSet 天然去重
ZADD key 100 "member"
ZADD key 200 "member"  # 更新分数

# 检查是否存在
ZSCORE key "member"

总结

Redis ZSet 核心要点:

特性说明
底层结构ziplist(小)/ skiplist(大)
查找复杂度O(log N)
范围查询O(log N + M)
唯一性元素唯一
排序按分数升序

最佳实践

  1. 小数据用 ziplist,大数据用 skiplist
  2. 批量操作使用 ZADD 多参数
  3. 排行榜限制数量(ZREMRANGEBYRANK)
  4. 延迟队列用时间戳作为分数
  5. 定期清理过期数据

掌握 ZSet,轻松实现排行榜、延迟队列等功能!

参考资料


分享这篇文章到:

上一篇文章
RAG 分块策略与优化实战
下一篇文章
MySQL 综合调优实战案例