上一篇我们详解了Redis Hash的对象存储方案,依靠**字段级操作、原子计数、高效内存**三大特性,完美解决了结构化数据缓存痛点。而在电商、游戏、内容平台等场景中,我们常常需要实现**排行榜、热度排序、范围筛选**等功能——普通集合无法排序、列表难以动态更新排名,Redis有序集合(ZSet)正是为这类排序场景量身打造的数据结构。
ZSet兼具**有序性、唯一性、可排序性**,通过分值(score)实现动态排序,支持排名查询、范围筛选、分值增减等核心操作,是实现各类排行榜的首选方案。本篇从ZSet核心原理、基础命令、排名/得分实战、业务落地到生产避坑,带你快速入门并落地高可用排序场景。
核心定位:Redis ZSet(有序集合)是带分值的唯一集合,元素按score自动排序,支持精准排名、范围查询、动态更新,天然适配排行榜、热度排序、优先级队列等场景。
一、Redis ZSet 核心特性与底层原理
ZSet底层采用压缩列表(ziplist)+ 跳表(skiplist)混合结构,数据量小时用ziplist节省内存,数据量大时切换为跳表,保证O(logN)级别的排序与查询效率,核心特性如下:
- 元素唯一性:集合内无重复元素,和Set保持一致,避免重复统计
- 有序性:元素按score分值自动排序(默认升序,支持降序),score相同则按元素字典序排序
- 分值灵活:score支持整数、浮点数,可动态增减,实时更新排名
- 双维度查询:既可以按分值范围查元素,也可以按排名查元素
- 原子操作:分值增减、排名更新均为原子性,高并发下无数据错乱
使用提示:ZSet适合**动态排序、唯一性、范围查询**的场景;海量数据(超10万条)需合理分页查询,避免全量拉取阻塞Redis。
二、ZSet 核心命令实战(命令行+Java代码)
ZSet操作围绕元素添加、分值更新、排名查询、范围筛选、删除展开,分为升序(默认)和降序两类命令,配合SpringBoot RedisTemplate可快速落地业务。
1. 元素添加与分值更新
添加元素时指定score,元素存在则更新分值,排名自动重排,是构建排序场景的基础操作。
| 命令 | 作用 | 示例 |
|---|---|---|
ZADD key score member | 添加/更新元素,指定分值,存在则覆盖score | ZADD rank:game 99 "player1001" |
ZINCRBY key increment member | 原子增减元素分值(支持正负),自动更新排名 | ZINCRBY rank:game 5 "player1001" |
/**
* 添加/更新有序集合元素(单个)
* @param key 集合键
* @param score 分值
* @param member 元素
*/
public void zadd(String key, double score, String member) {
redisTemplate.opsForZSet().add(key, member, score);
}
/**
* 原子增减元素分值(积分/热度更新)
* @param increment 增减值(正数加分,负数减分)
*/
public Double zincrby(String key, double increment, String member) {
return redisTemplate.opsForZSet().incrementScore(key, member, increment);
}
/**
* 批量添加元素
*/
public void zaddBatch(String key, Map<String, Double> memberScoreMap) {
Set<ZSetOperations.TypedTuple<String>> tuples = memberScoreMap.entrySet().stream()
.map(entry -> new DefaultTypedTuple<>(entry.getKey(), entry.getValue()))
.collect(Collectors.toSet());
redisTemplate.opsForZSet().add(key, tuples);
}
2. 排名查询(核心场景:获取名次)
查询单个元素的排名,分为升序排名(分值越小排名越前)和降序排名(分值越大排名越前),日常排行榜多用降序。
| 命令 | 作用 | 示例 |
|---|---|---|
ZRANK key member | 查询升序排名(0为第一名) | ZRANK rank:game "player1001" |
ZREVRANK key member | 查询降序排名(0为第一名,排行榜常用) | ZREVRANK rank:game "player1001" |
/**
* 查询降序排名(排行榜:分值越高排名越前)
* @return 排名(从0开始,0=第一名,null=元素不存在)
*/
public Long getReverseRank(String key, String member) {
return redisTemplate.opsForZSet().reverseRank(key, member);
}
/**
* 查询升序排名
*/
public Long getRank(String key, String member) {
return redisTemplate.opsForZSet().rank(key, member);
}
/**
* 封装前端友好排名(从1开始计数)
*/
public Long getFrontRank(String key, String member) {
Long rank = getReverseRank(key, member);
return rank == null ? -1 : rank + 1;
}
3. 范围查询:按排名/分值拉取列表
ZSet核心能力,支持按排名区间、分值区间筛选元素,搭配升序/降序实现排行榜分页、范围筛选。
| 命令 | 作用 | 示例 |
|---|---|---|
ZRANGE key start end | 按升序排名查区间元素(0=首位,-1=末位) | ZRANGE rank:game 0 9 |
ZREVRANGE key start end | 按降序排名查区间元素(排行榜分页) | ZREVRANGE rank:game 0 9 WITHSCORES |
ZRANGEBYSCORE key min max | 按升序分值查区间元素 | ZRANGEBYSCORE rank:game 80 100 |
ZREVRANGEBYSCORE key max min | 按降序分值查区间元素 | ZREVRANGEBYSCORE rank:game 100 80 |
/**
* 降序查询TopN排行榜(带分值)
* @param topN 前N名
*/
public Map<String, Double> getTopN(String key, int topN) {
Set<ZSetOperations.TypedTuple<String>> tuples = redisTemplate.opsForZSet()
.reverseRangeWithScores(key, 0, topN - 1);
if (CollUtil.isEmpty(tuples)) {
return new HashMap<>();
}
return tuples.stream()
.collect(Collectors.toMap(
TypedTuple::getValue,
tuple -> tuple.getScore() == null ? 0D : tuple.getScore()
));
}
/**
* 按分值范围查询元素(降序)
* @param min 最小分值
* @param max 最大分值
*/
public Set<String> getByScoreRange(String key, double min, double max) {
return redisTemplate.opsForZSet()
.reverseRangeByScore(key, max, min);
}
4. 辅助操作:统计与删除
| 命令 | 作用 | 示例 |
|---|---|---|
ZCARD key | 统计集合总元素数 | ZCARD rank:game |
ZCOUNT key min max | 统计分值区间内元素数量 | ZCOUNT rank:game 90 100 |
ZREM key member | 删除指定元素 | ZREM rank:game "player1001" |
// 统计集合元素总数
public Long zcard(String key) {
return redisTemplate.opsForZSet().size(key);
}
// 删除指定元素
public Long zrem(String key, String member) {
return redisTemplate.opsForZSet().remove(key, member);
}
三、ZSet 经典业务场景实战
场景1:游戏积分排行榜(最常用)
实时统计玩家积分,动态更新排名,快速查询Top10和个人名次,高并发下积分增减无错乱。
// 排行榜Key规范
private static final String GAME_RANK_KEY = "rank:game:score";
/**
* 更新玩家积分(完成任务/获胜加分)
*/
public void updatePlayerScore(String playerId, double addScore) {
zincrby(GAME_RANK_KEY, addScore, playerId);
}
/**
* 获取游戏Top10排行榜
*/
public Map<String, Double> getGameTop10() {
return getTopN(GAME_RANK_KEY, 10);
}
/**
* 查询个人排名与积分
*/
public Map<String, Object> getPlayerRank(String playerId) {
Map<String, Object> result = new HashMap<>();
// 排名(从1开始)
Long rank = getFrontRank(GAME_RANK_KEY, playerId);
// 积分
Double score = redisTemplate.opsForZSet().score(GAME_RANK_KEY, playerId);
result.put("rank", rank);
result.put("score", score == null ? 0D : score);
return result;
}
场景2:商品/文章热度排序
按浏览量、点赞数、销量计算热度分值,ZSet自动排序,实现热门商品、爆款文章推荐。
场景3:延时任务/优先级队列
用时间戳作为score,按时间排序实现延时任务;按优先级分值排序,实现高优先级任务优先执行。
场景4:用户签到/连续天数统计
按日期分值存储签到记录,快速查询连续签到天数、月度签到统计。
四、ZSet 生产避坑与最佳实践
1. 常见坑点规避
- 全量查询阻塞:禁止用
ZRANGE key 0 -1拉取全量数据,大数据量必须分页查询 - 分值精度问题:score为64位浮点数,超大整数分值可能丢失精度,建议用整数计分
- 排名计数误区:Redis排名默认从0开始,前端展示需+1转换
- 内存占用高:跳表结构内存开销大于List/Set,定期清理冷数据(如过期排行榜)
2. 生产最佳实践
- 排行榜分页:用
ZREVRANGE实现分页,避免一次性加载大量数据 - 结合TTL过期:周期性排行榜(日榜/周榜)设置TTL,自动清理过期数据
- 分值设计合理:避免分值过大/过小,积分、热度等场景用整数计分
- 热点排名缓存:TopN排行榜缓存结果,减少Redis查询压力
- 监控BigKey:单ZSet元素控制在10万以内,超量则拆分(如按分区/品类拆分排行榜)
五、ZSet 核心命令速查表
| 操作类型 | 核心命令 | 适用场景 |
|---|---|---|
| 添加更新 | ZADD、ZINCRBY | 初始化数据、动态加分/减分 |
| 排名查询 | ZRANK、ZREVRANK | 个人名次查询 |
| 范围查询 | ZREVRANGE、ZRANGEBYSCORE | 排行榜分页、分值筛选 |
| 统计删除 | ZCARD、ZCOUNT、ZREM | 数量统计、清理无效元素 |
核心总结:ZSet是Redis排序场景的最优解,依靠分值排序、唯一元素、原子操作,轻松实现各类排行榜、热度筛选功能;把控分页查询、分值设计、内存清理三大要点,即可稳定支撑高并发排序业务。
结语与下篇预告
至此,Redis五大核心数据结构(String、List、Hash、Set、ZSet)已全部讲解完毕,从基础缓存到排序场景,覆盖绝大多数业务需求。