基于Java和Redis实现排行榜功能

引言

排行榜是现代应用程序中常见的功能,无论是游戏积分榜、销售排行还是用户活跃度统计,都需要高效的排序和查询机制。Redis的有序集合(Sorted Set)数据结构为实现排行榜提供了完美的解决方案,它能够自动维护元素的排序,并支持高效的范围查询和排名操作。

Redis Sorted Set简介

Redis的有序集合(Sorted Set)是一种特殊的数据结构,它具有以下特点:

  • 唯一性:集合中的每个元素都是唯一的

  • 有序性:每个元素都关联一个分数(score),Redis根据分数自动排序

  • 高效性:插入、删除、查找操作的时间复杂度都是O(log N)

  • 范围查询:支持按分数范围或排名范围查询元素

环境准备

Maven依赖

首先,在项目的pom.xml中添加必要的依赖:

复制代码
<dependencies>
    <!-- Jedis Redis客户端 -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>4.3.1</version>
    </dependency>
    
    <!-- Spring Boot Redis Starter (可选) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <version>2.7.0</version>
    </dependency>
    
    <!-- JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.14.2</version>
    </dependency>
</dependencies>

Redis配置

创建Redis连接配置类:

复制代码
@Configuration
public class RedisConfig {
    
    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setHostName("localhost");
        factory.setPort(6379);
        factory.setDatabase(0);
        return factory;
    }
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        return template;
    }
}

核心实现

1. 排行榜服务类

复制代码
@Service
public class LeaderboardService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String LEADERBOARD_KEY = "game:leaderboard";
    
    /**
     * 更新用户分数
     * @param userId 用户ID
     * @param score 分数
     */
    public void updateScore(String userId, double score) {
        redisTemplate.opsForZSet().add(LEADERBOARD_KEY, userId, score);
    }
    
    /**
     * 增加用户分数
     * @param userId 用户ID
     * @param increment 增加的分数
     * @return 更新后的总分数
     */
    public Double incrementScore(String userId, double increment) {
        return redisTemplate.opsForZSet().incrementScore(LEADERBOARD_KEY, userId, increment);
    }
    
    /**
     * 获取用户当前分数
     * @param userId 用户ID
     * @return 用户分数
     */
    public Double getUserScore(String userId) {
        return redisTemplate.opsForZSet().score(LEADERBOARD_KEY, userId);
    }
    
    /**
     * 获取用户排名(从1开始)
     * @param userId 用户ID
     * @return 用户排名,如果用户不存在返回null
     */
    public Long getUserRank(String userId) {
        Long rank = redisTemplate.opsForZSet().reverseRank(LEADERBOARD_KEY, userId);
        return rank != null ? rank + 1 : null;
    }
    
    /**
     * 获取前N名排行榜
     * @param topN 获取前几名
     * @return 排行榜列表
     */
    public List<LeaderboardEntry> getTopN(int topN) {
        Set<ZSetOperations.TypedTuple<Object>> tuples = 
            redisTemplate.opsForZSet().reverseRangeWithScores(LEADERBOARD_KEY, 0, topN - 1);
        
        List<LeaderboardEntry> leaderboard = new ArrayList<>();
        int rank = 1;
        for (ZSetOperations.TypedTuple<Object> tuple : tuples) {
            LeaderboardEntry entry = new LeaderboardEntry();
            entry.setUserId((String) tuple.getValue());
            entry.setScore(tuple.getScore());
            entry.setRank(rank++);
            leaderboard.add(entry);
        }
        return leaderboard;
    }
    
    /**
     * 获取指定排名范围的排行榜
     * @param startRank 起始排名(从1开始)
     * @param endRank 结束排名(包含)
     * @return 排行榜列表
     */
    public List<LeaderboardEntry> getRangeByRank(int startRank, int endRank) {
        Set<ZSetOperations.TypedTuple<Object>> tuples = 
            redisTemplate.opsForZSet().reverseRangeWithScores(
                LEADERBOARD_KEY, startRank - 1, endRank - 1);
        
        List<LeaderboardEntry> leaderboard = new ArrayList<>();
        int rank = startRank;
        for (ZSetOperations.TypedTuple<Object> tuple : tuples) {
            LeaderboardEntry entry = new LeaderboardEntry();
            entry.setUserId((String) tuple.getValue());
            entry.setScore(tuple.getScore());
            entry.setRank(rank++);
            leaderboard.add(entry);
        }
        return leaderboard;
    }
    
    /**
     * 获取指定分数范围的用户
     * @param minScore 最小分数
     * @param maxScore 最大分数
     * @return 用户列表
     */
    public List<LeaderboardEntry> getRangeByScore(double minScore, double maxScore) {
        Set<ZSetOperations.TypedTuple<Object>> tuples = 
            redisTemplate.opsForZSet().reverseRangeByScoreWithScores(
                LEADERBOARD_KEY, minScore, maxScore);
        
        List<LeaderboardEntry> result = new ArrayList<>();
        for (ZSetOperations.TypedTuple<Object> tuple : tuples) {
            LeaderboardEntry entry = new LeaderboardEntry();
            entry.setUserId((String) tuple.getValue());
            entry.setScore(tuple.getScore());
            // 获取具体排名
            Long rank = redisTemplate.opsForZSet().reverseRank(LEADERBOARD_KEY, tuple.getValue());
            entry.setRank(rank != null ? rank.intValue() + 1 : 0);
            result.add(entry);
        }
        return result;
    }
    
    /**
     * 获取排行榜总人数
     * @return 总人数
     */
    public Long getTotalCount() {
        return redisTemplate.opsForZSet().zCard(LEADERBOARD_KEY);
    }
    
    /**
     * 删除用户
     * @param userId 用户ID
     * @return 是否删除成功
     */
    public Boolean removeUser(String userId) {
        Long removed = redisTemplate.opsForZSet().remove(LEADERBOARD_KEY, userId);
        return removed != null && removed > 0;
    }
}

2. 排行榜条目实体类

复制代码
public class LeaderboardEntry {
    private String userId;
    private Double score;
    private Integer rank;
    
    // 构造函数
    public LeaderboardEntry() {}
    
    public LeaderboardEntry(String userId, Double score, Integer rank) {
        this.userId = userId;
        this.score = score;
        this.rank = rank;
    }
    
    // Getter和Setter方法
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
    
    public Double getScore() { return score; }
    public void setScore(Double score) { this.score = score; }
    
    public Integer getRank() { return rank; }
    public void setRank(Integer rank) { this.rank = rank; }
    
    @Override
    public String toString() {
        return String.format("LeaderboardEntry{userId='%s', score=%.2f, rank=%d}", 
                           userId, score, rank);
    }
}

3. REST API控制器

复制代码
@RestController
@RequestMapping("/api/leaderboard")
public class LeaderboardController {
    
    @Autowired
    private LeaderboardService leaderboardService;
    
    /**
     * 更新用户分数
     */
    @PostMapping("/score")
    public ResponseEntity<String> updateScore(@RequestBody ScoreUpdateRequest request) {
        leaderboardService.updateScore(request.getUserId(), request.getScore());
        return ResponseEntity.ok("Score updated successfully");
    }
    
    /**
     * 增加用户分数
     */
    @PostMapping("/increment")
    public ResponseEntity<Double> incrementScore(@RequestBody ScoreIncrementRequest request) {
        Double newScore = leaderboardService.incrementScore(request.getUserId(), request.getIncrement());
        return ResponseEntity.ok(newScore);
    }
    
    /**
     * 获取用户排名和分数
     */
    @GetMapping("/user/{userId}")
    public ResponseEntity<UserRankInfo> getUserInfo(@PathVariable String userId) {
        Double score = leaderboardService.getUserScore(userId);
        Long rank = leaderboardService.getUserRank(userId);
        
        if (score == null) {
            return ResponseEntity.notFound().build();
        }
        
        UserRankInfo info = new UserRankInfo(userId, score, rank);
        return ResponseEntity.ok(info);
    }
    
    /**
     * 获取前N名排行榜
     */
    @GetMapping("/top/{n}")
    public ResponseEntity<List<LeaderboardEntry>> getTopN(@PathVariable int n) {
        List<LeaderboardEntry> leaderboard = leaderboardService.getTopN(n);
        return ResponseEntity.ok(leaderboard);
    }
    
    /**
     * 获取指定排名范围的排行榜
     */
    @GetMapping("/range")
    public ResponseEntity<List<LeaderboardEntry>> getRangeByRank(
            @RequestParam int startRank, 
            @RequestParam int endRank) {
        List<LeaderboardEntry> leaderboard = leaderboardService.getRangeByRank(startRank, endRank);
        return ResponseEntity.ok(leaderboard);
    }
    
    /**
     * 获取排行榜统计信息
     */
    @GetMapping("/stats")
    public ResponseEntity<LeaderboardStats> getStats() {
        Long totalCount = leaderboardService.getTotalCount();
        List<LeaderboardEntry> topUsers = leaderboardService.getTopN(1);
        
        LeaderboardStats stats = new LeaderboardStats();
        stats.setTotalUsers(totalCount);
        if (!topUsers.isEmpty()) {
            stats.setTopUser(topUsers.get(0));
        }
        
        return ResponseEntity.ok(stats);
    }
}

高级功能

1. 多个排行榜管理

复制代码
@Service
public class MultiLeaderboardService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String LEADERBOARD_PREFIX = "leaderboard:";
    
    /**
     * 获取排行榜键名
     */
    private String getLeaderboardKey(String leaderboardType) {
        return LEADERBOARD_PREFIX + leaderboardType;
    }
    
    /**
     * 更新指定排行榜中用户分数
     */
    public void updateScore(String leaderboardType, String userId, double score) {
        String key = getLeaderboardKey(leaderboardType);
        redisTemplate.opsForZSet().add(key, userId, score);
    }
    
    /**
     * 获取用户在多个排行榜中的排名
     */
    public Map<String, UserRankInfo> getUserRanksInMultipleBoards(String userId, List<String> leaderboardTypes) {
        Map<String, UserRankInfo> results = new HashMap<>();
        
        for (String type : leaderboardTypes) {
            String key = getLeaderboardKey(type);
            Double score = redisTemplate.opsForZSet().score(key, userId);
            Long rank = redisTemplate.opsForZSet().reverseRank(key, userId);
            
            if (score != null && rank != null) {
                results.put(type, new UserRankInfo(userId, score, rank + 1));
            }
        }
        
        return results;
    }
}

2. 时间窗口排行榜

复制代码
@Service
public class TimeWindowLeaderboardService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 获取日排行榜键名
     */
    private String getDailyLeaderboardKey() {
        String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        return "leaderboard:daily:" + today;
    }
    
    /**
     * 获取周排行榜键名
     */
    private String getWeeklyLeaderboardKey() {
        LocalDate now = LocalDate.now();
        int weekOfYear = now.get(WeekFields.ISO.weekOfYear());
        return String.format("leaderboard:weekly:%d-%02d", now.getYear(), weekOfYear);
    }
    
    /**
     * 更新日排行榜和总排行榜
     */
    public void updateDailyScore(String userId, double score) {
        String dailyKey = getDailyLeaderboardKey();
        String totalKey = "leaderboard:total";
        
        // 使用Redis事务确保一致性
        redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                operations.multi();
                operations.opsForZSet().add(dailyKey, userId, score);
                operations.opsForZSet().incrementScore(totalKey, userId, score);
                return operations.exec();
            }
        });
        
        // 设置日排行榜过期时间(7天)
        redisTemplate.expire(dailyKey, 7, TimeUnit.DAYS);
    }
}

3. 排行榜缓存优化

复制代码
@Service
public class CachedLeaderboardService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String CACHE_KEY_PREFIX = "cache:leaderboard:";
    private static final int CACHE_EXPIRE_SECONDS = 300; // 5分钟缓存
    
    /**
     * 获取缓存的前N名排行榜
     */
    @Cacheable(value = "leaderboard", key = "'top:' + #topN")
    public List<LeaderboardEntry> getCachedTopN(int topN) {
        String cacheKey = CACHE_KEY_PREFIX + "top:" + topN;
        
        // 尝试从缓存获取
        List<LeaderboardEntry> cached = (List<LeaderboardEntry>) 
            redisTemplate.opsForValue().get(cacheKey);
        
        if (cached != null) {
            return cached;
        }
        
        // 缓存未命中,从排行榜获取
        List<LeaderboardEntry> leaderboard = getTopN(topN);
        
        // 存入缓存
        redisTemplate.opsForValue().set(cacheKey, leaderboard, 
                                       CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
        
        return leaderboard;
    }
    
    /**
     * 清除相关缓存
     */
    public void clearLeaderboardCache() {
        Set<String> keys = redisTemplate.keys(CACHE_KEY_PREFIX + "*");
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
        }
    }
}

性能优化建议

1. 批量操作

对于大量的分数更新操作,建议使用批量处理:

复制代码
public void batchUpdateScores(Map<String, Double> userScores) {
    redisTemplate.executePipelined(new SessionCallback<Object>() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            ZSetOperations<String, Object> zSetOps = operations.opsForZSet();
            userScores.forEach((userId, score) -> {
                zSetOps.add(LEADERBOARD_KEY, userId, score);
            });
            return null;
        }
    });
}

2. 连接池配置

优化Redis连接池配置以提升性能:

复制代码
@Configuration
public class RedisPoolConfig {
    
    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(50);
        poolConfig.setMaxIdle(20);
        poolConfig.setMinIdle(10);
        poolConfig.setTestOnBorrow(true);
        poolConfig.setTestOnReturn(true);
        
        JedisConnectionFactory factory = new JedisConnectionFactory(poolConfig);
        factory.setHostName("localhost");
        factory.setPort(6379);
        factory.setTimeout(3000);
        
        return factory;
    }
}

测试示例

复制代码
@SpringBootTest
public class LeaderboardServiceTest {
    
    @Autowired
    private LeaderboardService leaderboardService;
    
    @Test
    public void testLeaderboard() {
        // 添加测试数据
        leaderboardService.updateScore("user1", 1000);
        leaderboardService.updateScore("user2", 1500);
        leaderboardService.updateScore("user3", 800);
        leaderboardService.updateScore("user4", 1200);
        
        // 测试获取前3名
        List<LeaderboardEntry> top3 = leaderboardService.getTopN(3);
        assertEquals(3, top3.size());
        assertEquals("user2", top3.get(0).getUserId());
        assertEquals(1500.0, top3.get(0).getScore(), 0.01);
        
        // 测试用户排名
        Long rank = leaderboardService.getUserRank("user2");
        assertEquals(Long.valueOf(1), rank);
        
        // 测试分数增加
        Double newScore = leaderboardService.incrementScore("user1", 600);
        assertEquals(1600.0, newScore, 0.01);
        
        // 验证排名变化
        Long newRank = leaderboardService.getUserRank("user1");
        assertEquals(Long.valueOf(1), newRank);
    }
}

总结

使用Redis实现排行榜功能具有以下优势:

  1. 高性能:Redis的内存数据库特性保证了快速的读写操作

  2. 自动排序:Sorted Set自动维护元素排序,无需额外的排序操作

  3. 丰富的操作:支持范围查询、排名查询、分数更新等多种操作

  4. 扩展性强:可以轻松实现多种类型的排行榜和时间窗口排行榜

通过合理的缓存策略和批量操作优化,Redis排行榜系统可以轻松应对高并发场景,为用户提供实时、准确的排名服务。在实际项目中,还可以根据具体需求添加更多功能,如排行榜历史记录、用户分组排行等。

相关推荐
掘了5 分钟前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅30 分钟前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 小时前
jwt介绍
前端
爱敲代码的小鱼1 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT062 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法