Redis有序集合(ZSet):排行榜功能的最优解,原理与实战

Redis有序集合(ZSet):排行榜功能的最优解,原理与实战

1. Redis有序集合概述

1.1 什么是Redis有序集合

Redis有序集合(Sorted Set,简称ZSet)是Redis中最复杂也是最强大的数据结构之一。它结合了Set的唯一性按分数排序的特性,每个成员都关联一个分数(score),Redis根据分数对成员进行排序。

1.2 有序集合的特点

特性 描述 优势
有序性 按分数自动排序 天然的排序功能
唯一性 成员不重复 自动去重
双索引 支持按成员和分数查询 查询灵活高效
范围操作 支持分数和排名范围查询 适合排行榜场景

2. 底层实现原理

2.1 编码方式

编码方式 使用条件 特点
ziplist 元素数≤128且成员长度≤64字节 内存紧凑
skiplist+hashtable 其他情况 查询高效

2.2 跳表(Skip List)原理

跳表是ZSet的核心数据结构:

  • 多层链表结构,支持O(log N)查询
  • 每层都是有序链表
  • 通过概率性算法维护层次结构

3. 基本操作

3.1 添加和更新

bash 复制代码
# 添加成员
127.0.0.1:6379> ZADD leaderboard 100 "player1" 200 "player2" 150 "player3"
(integer) 3

# 更新分数
127.0.0.1:6379> ZADD leaderboard 250 "player1"
(integer) 0   # 成员已存在,返回0

3.2 查询操作

bash 复制代码
# 按排名查询(分数从小到大)
127.0.0.1:6379> ZRANGE leaderboard 0 -1 WITHSCORES
1) "player3"
2) "150"
3) "player2"
4) "200"
5) "player1"
6) "250"

# 按排名查询(分数从大到小)
127.0.0.1:6379> ZREVRANGE leaderboard 0 2 WITHSCORES
1) "player1"
2) "250"
3) "player2"
4) "200"
5) "player3"
6) "150"

# 按分数范围查询
127.0.0.1:6379> ZRANGEBYSCORE leaderboard 150 200
1) "player3"
2) "player2"

# 获取成员分数
127.0.0.1:6379> ZSCORE leaderboard "player1"
"250"

# 获取成员排名
127.0.0.1:6379> ZRANK leaderboard "player1"
(integer) 2

127.0.0.1:6379> ZREVRANK leaderboard "player1"
(integer) 0   # 从大到小排名

# 获取集合大小
127.0.0.1:6379> ZCARD leaderboard
(integer) 3

4. 高级操作

4.1 分数操作

bash 复制代码
# 增加分数
127.0.0.1:6379> ZINCRBY leaderboard 50 "player2"
"250"

# 按分数范围计数
127.0.0.1:6379> ZCOUNT leaderboard 200 300
(integer) 2

4.2 删除操作

bash 复制代码
# 删除成员
127.0.0.1:6379> ZREM leaderboard "player3"
(integer) 1

# 按排名删除
127.0.0.1:6379> ZREMRANGEBYRANK leaderboard 0 0
(integer) 1

# 按分数删除
127.0.0.1:6379> ZREMRANGEBYSCORE leaderboard 200 250
(integer) 1

5. 实战应用场景

5.1 游戏排行榜系统

java 复制代码
@Service
public class GameLeaderboardService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String LEADERBOARD_KEY = "game:leaderboard:global";
    
    /**
     * 更新玩家分数
     */
    public void updatePlayerScore(String playerId, long score) {
        redisTemplate.opsForZSet().add(LEADERBOARD_KEY, playerId, score);
    }
    
    /**
     * 增加玩家分数
     */
    public Double addPlayerScore(String playerId, long deltaScore) {
        return redisTemplate.opsForZSet().incrementScore(LEADERBOARD_KEY, playerId, deltaScore);
    }
    
    /**
     * 获取排行榜TOP N
     */
    public List<PlayerRank> getTopPlayers(int topN) {
        Set<ZSetOperations.TypedTuple<String>> tuples = 
            redisTemplate.opsForZSet().reverseRangeWithScores(LEADERBOARD_KEY, 0, topN - 1);
        
        List<PlayerRank> rankings = new ArrayList<>();
        int rank = 1;
        for (ZSetOperations.TypedTuple<String> tuple : tuples) {
            PlayerRank playerRank = new PlayerRank();
            playerRank.setRank(rank++);
            playerRank.setPlayerId(tuple.getValue());
            playerRank.setScore(tuple.getScore().longValue());
            rankings.add(playerRank);
        }
        return rankings;
    }
    
    /**
     * 获取玩家排名和分数
     */
    public PlayerRank getPlayerRank(String playerId) {
        Double score = redisTemplate.opsForZSet().score(LEADERBOARD_KEY, playerId);
        if (score == null) {
            return null;
        }
        
        Long rank = redisTemplate.opsForZSet().reverseRank(LEADERBOARD_KEY, playerId);
        
        PlayerRank playerRank = new PlayerRank();
        playerRank.setPlayerId(playerId);
        playerRank.setScore(score.longValue());
        playerRank.setRank(rank != null ? rank.intValue() + 1 : -1);
        
        return playerRank;
    }
    
    /**
     * 获取玩家周围排名
     */
    public List<PlayerRank> getPlayersAround(String playerId, int range) {
        Long playerRank = redisTemplate.opsForZSet().reverseRank(LEADERBOARD_KEY, playerId);
        if (playerRank == null) {
            return new ArrayList<>();
        }
        
        long start = Math.max(0, playerRank - range);
        long end = playerRank + range;
        
        Set<ZSetOperations.TypedTuple<String>> tuples = 
            redisTemplate.opsForZSet().reverseRangeWithScores(LEADERBOARD_KEY, start, end);
        
        List<PlayerRank> rankings = new ArrayList<>();
        int rank = (int) start + 1;
        for (ZSetOperations.TypedTuple<String> tuple : tuples) {
            PlayerRank pr = new PlayerRank();
            pr.setRank(rank++);
            pr.setPlayerId(tuple.getValue());
            pr.setScore(tuple.getScore().longValue());
            rankings.add(pr);
        }
        return rankings;
    }
}

@Data
class PlayerRank {
    private String playerId;
    private Integer rank;
    private Long score;
}

5.2 热门内容排行

java 复制代码
@Service
public class HotContentService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 更新内容热度分数
     */
    public void updateContentScore(String contentId, String category) {
        String key = "hot:content:" + category;
        
        // 计算热度分数(浏览量 + 点赞数 + 评论数 + 时间衰减)
        double score = calculateHotScore(contentId);
        
        redisTemplate.opsForZSet().add(key, contentId, score);
        redisTemplate.expire(key, 1, TimeUnit.DAYS);
    }
    
    /**
     * 获取热门内容
     */
    public List<String> getHotContents(String category, int count) {
        String key = "hot:content:" + category;
        Set<String> contentIds = redisTemplate.opsForZSet()
            .reverseRange(key, 0, count - 1);
        return new ArrayList<>(contentIds);
    }
    
    /**
     * 计算热度分数
     */
    private double calculateHotScore(String contentId) {
        // 简化的热度计算公式
        long views = getViews(contentId);
        long likes = getLikes(contentId);
        long comments = getComments(contentId);
        long timeDecay = getTimeDecay(contentId);
        
        return (views * 1.0 + likes * 2.0 + comments * 3.0) * timeDecay;
    }
    
    private long getViews(String contentId) { return 100; }
    private long getLikes(String contentId) { return 50; }
    private long getComments(String contentId) { return 20; }
    private long getTimeDecay(String contentId) { return 1; }
}

5.3 延时队列实现

java 复制代码
@Service
public class DelayedQueueService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String DELAY_QUEUE_KEY = "delay:queue";
    
    /**
     * 添加延时任务
     */
    public void addDelayedTask(String taskId, String taskData, long delaySeconds) {
        long executeTime = System.currentTimeMillis() + delaySeconds * 1000;
        String taskInfo = taskId + ":" + taskData;
        
        redisTemplate.opsForZSet().add(DELAY_QUEUE_KEY, taskInfo, executeTime);
    }
    
    /**
     * 获取到期任务
     */
    public List<String> getExpiredTasks(int batchSize) {
        long currentTime = System.currentTimeMillis();
        
        Set<String> expiredTasks = redisTemplate.opsForZSet()
            .rangeByScore(DELAY_QUEUE_KEY, 0, currentTime, 0, batchSize);
        
        if (!expiredTasks.isEmpty()) {
            // 删除已获取的任务
            redisTemplate.opsForZSet().removeRangeByScore(DELAY_QUEUE_KEY, 0, currentTime);
        }
        
        return new ArrayList<>(expiredTasks);
    }
    
    /**
     * 取消延时任务
     */
    public boolean cancelDelayedTask(String taskId) {
        // 需要根据taskId模糊匹配删除
        Set<String> allTasks = redisTemplate.opsForZSet().range(DELAY_QUEUE_KEY, 0, -1);
        
        for (String task : allTasks) {
            if (task.startsWith(taskId + ":")) {
                return redisTemplate.opsForZSet().remove(DELAY_QUEUE_KEY, task) > 0;
            }
        }
        return false;
    }
    
    /**
     * 获取队列统计信息
     */
    public Map<String, Object> getQueueStats() {
        Map<String, Object> stats = new HashMap<>();
        
        Long totalTasks = redisTemplate.opsForZSet().zCard(DELAY_QUEUE_KEY);
        Long expiredTasks = redisTemplate.opsForZSet()
            .count(DELAY_QUEUE_KEY, 0, System.currentTimeMillis());
        
        stats.put("total_tasks", totalTasks);
        stats.put("expired_tasks", expiredTasks);
        stats.put("pending_tasks", totalTasks - expiredTasks);
        
        return stats;
    }
}

5.4 用户积分系统

java 复制代码
@Service
public class UserPointsService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String POINTS_LEADERBOARD = "user:points:leaderboard";
    private static final String MONTHLY_POINTS = "user:points:monthly:";
    
    /**
     * 增加用户积分
     */
    public void addUserPoints(String userId, long points, String reason) {
        // 更新总积分排行榜
        redisTemplate.opsForZSet().incrementScore(POINTS_LEADERBOARD, userId, points);
        
        // 更新月度积分排行榜
        String monthlyKey = MONTHLY_POINTS + getCurrentMonth();
        redisTemplate.opsForZSet().incrementScore(monthlyKey, userId, points);
        redisTemplate.expire(monthlyKey, 35, TimeUnit.DAYS);
        
        // 记录积分变动历史
        recordPointsHistory(userId, points, reason);
    }
    
    /**
     * 获取用户总积分和排名
     */
    public UserPointsInfo getUserPointsInfo(String userId) {
        Double totalPoints = redisTemplate.opsForZSet().score(POINTS_LEADERBOARD, userId);
        Long totalRank = redisTemplate.opsForZSet().reverseRank(POINTS_LEADERBOARD, userId);
        
        String monthlyKey = MONTHLY_POINTS + getCurrentMonth();
        Double monthlyPoints = redisTemplate.opsForZSet().score(monthlyKey, userId);
        Long monthlyRank = redisTemplate.opsForZSet().reverseRank(monthlyKey, userId);
        
        UserPointsInfo info = new UserPointsInfo();
        info.setUserId(userId);
        info.setTotalPoints(totalPoints != null ? totalPoints.longValue() : 0);
        info.setTotalRank(totalRank != null ? totalRank.intValue() + 1 : -1);
        info.setMonthlyPoints(monthlyPoints != null ? monthlyPoints.longValue() : 0);
        info.setMonthlyRank(monthlyRank != null ? monthlyRank.intValue() + 1 : -1);
        
        return info;
    }
    
    /**
     * 获取积分排行榜
     */
    public List<UserPointsRank> getPointsLeaderboard(String type, int topN) {
        String key = "total".equals(type) ? POINTS_LEADERBOARD : MONTHLY_POINTS + getCurrentMonth();
        
        Set<ZSetOperations.TypedTuple<String>> tuples = 
            redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, topN - 1);
        
        List<UserPointsRank> rankings = new ArrayList<>();
        int rank = 1;
        for (ZSetOperations.TypedTuple<String> tuple : tuples) {
            UserPointsRank userRank = new UserPointsRank();
            userRank.setRank(rank++);
            userRank.setUserId(tuple.getValue());
            userRank.setPoints(tuple.getScore().longValue());
            rankings.add(userRank);
        }
        return rankings;
    }
    
    private String getCurrentMonth() {
        return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
    }
    
    private void recordPointsHistory(String userId, long points, String reason) {
        // 实现积分历史记录逻辑
    }
}

@Data
class UserPointsInfo {
    private String userId;
    private Long totalPoints;
    private Integer totalRank;
    private Long monthlyPoints;
    private Integer monthlyRank;
}

@Data
class UserPointsRank {
    private Integer rank;
    private String userId;
    private Long points;
}

6. 性能优化与最佳实践

6.1 性能特点

操作类型 时间复杂度 性能说明
ZADD O(log N) 插入效率高
ZRANGE O(log N + M) M为返回元素数
ZSCORE O(1) 获取分数很快
ZRANK O(log N) 排名查询高效
ZREMRANGEBYSCORE O(log N + M) 范围删除

6.2 最佳实践

6.2.1 控制集合大小
java 复制代码
@Service
public class OptimizedZSetService {
    
    private static final int MAX_LEADERBOARD_SIZE = 10000;
    
    /**
     * 安全的排行榜更新
     */
    public void safeUpdateLeaderboard(String key, String member, double score) {
        redisTemplate.opsForZSet().add(key, member, score);
        
        // 保持排行榜大小
        Long size = redisTemplate.opsForZSet().zCard(key);
        if (size > MAX_LEADERBOARD_SIZE) {
            redisTemplate.opsForZSet().removeRange(key, 0, size - MAX_LEADERBOARD_SIZE - 1);
        }
    }
}
6.2.2 使用Pipeline优化
java 复制代码
/**
 * 批量更新排行榜
 */
public void batchUpdateLeaderboard(String key, Map<String, Double> playerScores) {
    redisTemplate.executePipelined(new RedisCallback<Object>() {
        @Override
        public Object doInRedis(RedisConnection connection) {
            for (Map.Entry<String, Double> entry : playerScores.entrySet()) {
                connection.zAdd(key.getBytes(), entry.getValue(), entry.getKey().getBytes());
            }
            return null;
        }
    });
}

总结

Redis有序集合是排行榜和排序场景的最佳选择:

核心知识点

  1. 底层原理:跳表+哈希表的双重数据结构
  2. 基本操作:添加、查询、删除、范围操作
  3. 高级功能:分数操作、排名查询、范围删除
  4. 实战应用:游戏排行榜、热门内容、延时队列、积分系统

关键要点

  • 双重索引:同时支持按成员和分数的高效查询
  • 自动排序:插入时自动按分数排序,无需手动维护
  • 范围操作:强大的分数和排名范围查询能力
  • 高性能:O(log N)的查询性能,适合大数据量

实战建议

  1. 合理设计分数:根据业务需求设计合适的分数计算逻辑
  2. 控制数据规模:定期清理或限制ZSet大小
  3. 优化查询范围:避免大范围查询,使用分页
  4. 监控性能:关注内存使用和操作耗时
  5. 善用范围操作:充分利用ZSet的范围查询能力

通过本文学习,你应该能够熟练使用Redis有序集合实现各种排序和排行榜功能。


下一篇预告《Redis Stream:Redis 5.0+新数据结构详解,替代消息队列的新选择》


相关推荐
麦兜*2 小时前
MongoDB 与 GraphQL 结合:现代 API 开发新范式
java·数据库·spring boot·mongodb·spring·maven·graphql
亭台烟雨中3 小时前
SQL优化简单思路
数据库·sql
老华带你飞4 小时前
畅阅读小程序|畅阅读系统|基于java的畅阅读系统小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·小程序·毕设·畅阅读系统小程序
Codeking__6 小时前
mysql基础——库与表的操作
数据库·mysql
_苏沐6 小时前
cte功能oracle与pg执行模式对比
数据库·oracle
一氧化二氢.h7 小时前
通俗解释redis高级:redis持久化(RDB持久化、AOF持久化)、redis主从、redis哨兵、redis分片集群
redis·分布式·缓存
qq_5088234010 小时前
金融数据库--3Baostock
数据库·金融
悦数图数据库11 小时前
图技术重塑金融未来:悦数图数据库如何驱动行业创新与风控变革
数据库·金融
九河云11 小时前
华为云 GaussDB:金融级高可用数据库,为核心业务保驾护航
网络·数据库·科技·金融·华为云·gaussdb