Redis ZSet 数据类型详解
ZSet(Sorted Set)是 Redis 最强大的数据类型之一,结合了 Set 的去重特性和分数排序能力。本文将深入 ZSet 的底层跳表结构,掌握排行榜等核心应用场景。
一、ZSet 基础概念
1.1 什么是 ZSet
ZSet 是有序集合,每个元素关联一个分数(score):
- 元素唯一(类似 Set)
- 按分数排序
- 支持范围查询
# 添加元素
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 │
│ 4B │ 4B │ 2B │ 可变 │
└────────────────────────────────────┘
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) |
| 唯一性 | 元素唯一 |
| 排序 | 按分数升序 |
最佳实践:
- 小数据用 ziplist,大数据用 skiplist
- 批量操作使用 ZADD 多参数
- 排行榜限制数量(ZREMRANGEBYRANK)
- 延迟队列用时间戳作为分数
- 定期清理过期数据
掌握 ZSet,轻松实现排行榜、延迟队列等功能!