上一篇我们详解了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添加/更新元素,指定分值,存在则覆盖scoreZADD 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)已全部讲解完毕,从基础缓存到排序场景,覆盖绝大多数业务需求。

发表回复