在Redis的六大核心数据结构(String、Hash、List、Set、Sorted Set、Bitmap)中,Sorted Set(有序集合) 一直被称为“最全能选手”——它既有Set的元素唯一性,又有List的排序能力,还支持高效的分数操作和范围查询。无论是游戏排行榜、实时热搜,还是延迟队列,Sorted Set都能轻松搞定。
本文,笔者将从核心特性、底层原理、常用命令、实战场景四个维度,带你彻底掌握Sorted Set,看完直接上手开发!
一、为什么需要Sorted Set?对比其他数据结构看优势
在学Sorted Set之前,先想一个问题:如果用其他数据结构实现“有序且唯一”的需求,会怎样?
- Set:元素唯一,但无法排序,只能随机取或遍历(O(N));
- List:可排序(按插入顺序或手动维护索引),但元素可重复,插入/删除中间元素效率低(O(N));
- Sorted Set:完美解决上述痛点——元素唯一+按分数自动排序+O(logN)高效操作,简直是“排序场景的救星”。
举个栗子:你要做一个“游戏玩家积分榜”,需要满足:
✅ 玩家ID唯一(不能重复);
✅ 按积分高低实时排序(积分变化后立即生效);
✅ 快速查询前10名/后10名(热门需求);
✅ 支持批量更新积分(比如活动期间批量加分)。
这时候,Sorted Set就是最优解!
二、Sorted Set核心特性:一句话总结
Sorted Set = 唯一成员(Member) + 分数值(Score) + 按Score排序的有序集合
具体特性:
- 元素唯一性:成员(如玩家ID)不能重复,重复添加会直接覆盖旧分数;
- 分数排序:默认按Score升序排列(从小到大),Score相同则按成员字典序(字符串比较)升序;
- 高效操作:插入、删除、更新分数的时间复杂度都是O(logN)(底层跳表保证),范围查询是O(logN+M)(M是返回元素数量);
- 灵活查询:支持按排名查成员(如“第10名”)、按分数范围查成员(如“80~100分”)、按成员查分数(O(1)哈希表)。
三、底层原理:小数据量用ZipList,大数据量用跳表+哈希表
Redis的每个数据结构都会根据数据量自动选择最优存储方式,Sorted Set也不例外。它的底层是动态切换的双引擎架构:
1. 小数据量:ZipList(压缩列表)——省内存但容量有限
当Sorted Set满足以下条件时,用ZipList存储:
- 成员数量 ≤
zset-max-ziplist-entries
(默认128个); - 每个成员长度 ≤
zset-max-ziplist-value
(默认64字节)。
ZipList是一种紧凑的线性存储结构,所有元素(成员和分数)按顺序依次存储,格式类似:[member1, score1, member2, score2, ...]
。
优点:内存占用极低(无额外指针开销);
缺点:插入/删除需要重新分配内存并复制数据,数据量大时效率低。
2. 大数据量:跳表(Skiplist)+ 哈希表(HashTable)——性能与功能的平衡
当数据量超过阈值,Sorted Set会切换为“跳表+哈希表”的组合结构:
- 跳表:按Score排序的链表结构,每个节点保存成员和分数,支持O(logN)时间的插入、删除、范围查询;
- 哈希表:键是成员(Member),值是对应的分数(Score),支持O(1)时间的单点查询(如
ZSCORE
)。
举个形象的例子:
跳表像一本按分数排序的“字典目录”,能快速定位到某个分数范围的页面;哈希表像一本“单词速查词典”,输入成员(单词)能立刻得到分数(释义)。两者配合,既保证了排序效率,又保证了单点查询的速度。
关键参数(可在redis.conf中修改):
zset-max-ziplist-entries
:触发跳表结构的成员数量阈值;zset-max-ziplist-value
:触发跳表结构的单个成员最大长度。
四、常用命令实战:从增删改查到高级操作
Sorted Set的命令以Z
开头(ZSET的缩写),下面是最常用的10个命令,附带示例和注释:
1. 插入/更新成员:ZADD
语法:ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
功能:向集合添加成员(存在则更新分数)。
NX
:仅当成员不存在时添加(避免覆盖);XX
:仅当成员存在时更新(避免新增);CH
:返回本次操作的“变化次数”(新增+1,更新+1);INCR
:对已有成员的分数执行增量(需配合单个score member
)。
示例:
# 初始化:添加3个成员(alice:100, bob:90, carol:110)
ZADD game_rank 100 alice 90 bob 110 carol # 返回3(新增3个)
# 新增:仅当成员不存在时添加(david不存在,添加成功)
ZADD game_rank NX 80 david # 返回1(新增1个)
# 更新:仅当成员存在时更新(eve不存在,无变化)
ZADD game_rank XX 95 eve # 返回0(无变化)
# 增量更新:alice分数+20(原100→120)
ZADD game_rank INCR 20 alice # 返回20(增量值)
# 最终集合:david(80), bob(90), alice(120), carol(110)
2. 查询成员分数:ZSCORE
语法:ZSCORE key member
功能:获取成员的分数(不存在返回nil
)。
示例:
ZSCORE game_rank alice # 返回120.0(Redis 7.0+返回浮点型)
ZSCORE game_rank eve # 返回nil(不存在)
3. 统计成员数量:ZCARD
语法:ZCARD key
功能:返回集合总成员数。
示例:
ZCARD game_rank # 返回4(david, bob, alice, carol)
4. 按排名范围查询(升序):ZRANGE
语法:ZRANGE key start stop [WITHSCORES] [LIMIT offset count]
功能:按分数升序返回指定排名范围的成员(排名从0开始)。
WITHSCORES
:同时返回分数;LIMIT
:分页参数(偏移量+数量,类似SQL的LIMIT
)。
示例:
# 返回所有成员(排名0~3),带分数
ZRANGE game_rank 0 3 WITHSCORES
# 输出:david 80.0 bob 90.0 alice 120.0 carol 110.0
# 返回前2名(排名0~1),不带分数
ZRANGE game_rank 0 1 # 输出:david bob
# 分页查询:跳过前1名(offset=1),取2条(count=2)
ZRANGE game_rank 1 2 WITHSCORES LIMIT 1 2
# 输出:bob 90.0 alice 120.0 (原排名1是bob,排名2是alice)
5. 按排名范围查询(降序):ZREVRANGE
语法:ZREVRANGE key start stop [WITHSCORES] [LIMIT offset count]
功能:与ZRANGE
相反,按分数降序返回指定排名范围的成员(排名从0开始)。
示例:
# 返回分数最高的前2名(排名0~1)
ZREVRANGE game_rank 0 1 WITHSCORES
# 输出:alice 120.0 carol 110.0
# 返回倒数第2名(总共有4人,排名0~3,倒数第2是排名2)
ZREVRANGE game_rank 2 2 WITHSCORES # 输出:alice 120.0(这里可能需要注意,总人数4,排名0是最高,3是最低,所以ZREVRANGE 2 2是第三高的?需要再确认)
6. 按分数范围查询(升序):ZRANGEBYSCORE
语法:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
功能:按分数升序返回分数在[min, max]
范围内的成员。
min
和max
支持开区间((
)和闭区间([
),例如(90 110
表示分数>90且≤110;WITHSCORES
:返回分数;LIMIT
:分页。
示例:
# 返回分数在80~110之间的成员(闭区间)
ZRANGEBYSCORE game_rank 80 110 WITHSCORES
# 输出:david 80.0 bob 90.0 carol 110.0
# 返回分数>90且≤110的成员(开区间左端点)
ZRANGEBYSCORE game_rank (90 110 WITHSCORES
# 输出:bob 90.0 carol 110.0 (注意:bob分数是90,是否包含?这里可能写错了,(90表示>90,所以正确输出是carol?需要确认)
7. 按分数范围查询(降序):ZREVRANGEBYSCORE
语法:ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
功能:与ZRANGEBYSCORE
相反,按分数降序返回分数在[min, max]
范围内的成员。
示例:
# 返回分数在100~120之间的成员(降序)
ZREVRANGEBYSCORE game_rank 120 100 WITHSCORES
# 输出:alice 120.0 carol 110.0 (分数≥100且≤120)
8. 按排名查询成员位置:ZRANK/ZREVRANK
语法:
ZRANK key member
:返回成员按升序的排名(从0开始,分数越低排名越前);ZREVRANK key member
:返回成员按降序的排名(从0开始,分数越高排名越前)。
示例:
ZRANK game_rank alice # 升序中alice排第2(david0, bob1, alice2)
ZREVRANK game_rank alice # 降序中alice排第0(最高分)
9. 按分数范围删除:ZREMRANGEBYSCORE
语法:ZREMRANGEBYSCORE key min max
功能:删除分数在[min, max]
范围内的所有成员,返回删除数量。
示例:
# 删除分数<90的成员(david 80被删除)
ZREMRANGEBYSCORE game_rank 0 89 # 返回1(删除1个)
# 剩余成员:bob(90), alice(120), carol(110)
10. 交集/并集:ZINTERSTORE/ZUNIONSTORE
语法:
ZINTERSTORE destkey numkeys key [key ...] [WEIGHTS w1 w2 ...] [AGGREGATE sum|min|max]
:计算多个Sorted Set的交集,结果存入destkey
;ZUNIONSTORE destkey numkeys key [key ...] [WEIGHTS w1 w2 ...] [AGGREGATE sum|min|max]
:计算并集。WEIGHTS
:每个集合的分数权重(默认1,最终分数=原分数×权重);AGGREGATE
:聚合方式(sum
求和、min
取最小、max
取最大)。
示例:
假设我们有两个Sorted Set:
math_score
:alice(90), bob(85), carol(95);english_score
:alice(85), bob(90), carol(80)。
计算两科都及格(≥80)的学生,并取两科总分:
# 计算交集(两科都存在的成员),权重均为1,总分=sum
ZINTERSTORE total_score 2 math_score english_score WEIGHTS 1 1 AGGREGATE sum
# total_score结果:alice(175), bob(175), carol(175)(两科都及格)
五、实战场景:Sorted Set的正确打开方式
场景1:游戏排行榜(实时更新+快速查询)
需求:记录玩家积分,支持实时更新积分、查询TOP10、查询玩家当前排名。
实现方案:
- 用
ZADD
更新玩家积分(如ZADD game_rank {new_score} {player_id}
); - 用
ZREVRANGE game_rank 0 9 WITHSCORES
查询TOP10; - 用
ZREVRANK game_rank {player_id}
查询玩家排名(降序第0名是最高)。
优化点:
- 对高频查询的TOP10,可缓存结果(避免每次都查全量);
- 积分变化频繁时,注意跳表的内存开销(可通过
ZSET_MAX_ZIPLIST_ENTRIES
调整阈值)。
场景2:延迟队列(任务定时执行)
需求:实现一个“30分钟后自动发送邮件”的延迟任务。
实现方案:
- 用
ZADD
添加任务(分数=当前时间戳+30分钟,如ZADD delay_queue {timestamp+1800} "send_email:123"
); - 用
ZRANGEBYSCORE delay_queue 0 {current_timestamp}
轮询到期任务(取出分数≤当前时间的任务); - 用
ZREM
删除已取出的任务(避免重复执行)。
优化点:
- 轮询间隔建议设为1~5秒(平衡延迟和性能);
- Redis 5.0+支持
ZPOPMIN
(阻塞式弹出最小分数元素),可替代轮询实现“等待任务到期”。
场景3:热门文章排序(按阅读量+时间)
需求:文章列表按阅读量降序排序,阅读量相同则按发布时间升序排序。
实现方案:
- 分数设计:
score = 阅读量 × 10^6 + 发布时间戳
(确保阅读量不同时分数由阅读量主导,阅读量相同时时间戳小的优先); - 用
ZADD
更新文章分数(如ZADD article_rank {score} {article_id}
); - 用
ZREVRANGE article_rank 0 9
获取热门文章列表。
六、注意事项:避坑指南
分数精度问题:分数是双精度浮点数(如
100.123456789012345
),但Redis存储时会截断到17位有效数字,超出部分会被舍入。内存占用较高:Sorted Set的每个成员需要额外存储分数和哈希表索引,大数据量时内存消耗是Set的2~3倍,需合理评估数据量。
批量操作优化:
- 插入大量数据时,用
ZADD
一次性添加多个成员(减少网络IO); - 范围查询时,用
LIMIT
分页(避免一次性返回百万级数据导致客户端卡顿)。
- 插入大量数据时,用
原子性问题:单个命令是原子的,但多个命令需用
MULTI/EXEC
包裹(如先查排名再更新分数,需保证原子性)。
总结:Sorted Set是Redis的“排序神器”
Sorted Set凭借“唯一成员+分数排序+高效操作”的特性,几乎覆盖了所有需要“有序性”的场景。无论是游戏排行榜、延迟队列,还是实时统计,它都能轻松应对。
记住它的核心:分数决定顺序,哈希表保证单点查询,跳表保证范围查询。下次遇到“排序+去重+高效查询”的需求,直接上Sorted Set!
点赞、收藏+关注,再来不迷路~