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 小时前
72、IMX6ULL驱动实战:设备树(DTS/DTB)+ GPIO子系统+Platform总线
linux·服务器·arm开发·数据库·单片机
likangbinlxa2 小时前
【Oracle11g SQL详解】UPDATE 和 DELETE 操作的正确使用
数据库·sql
r i c k2 小时前
数据库系统学习笔记
数据库·笔记·学习
野犬寒鸦3 小时前
从零起步学习JVM || 第一章:类加载器与双亲委派机制模型详解
java·jvm·数据库·后端·学习
IvorySQL4 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·4 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德4 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫4 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i4 小时前
完全卸载MariaDB
数据库·mariadb
期待のcode4 小时前
Redis的主从复制与集群
运维·服务器·redis