Java案例如何实现排行榜功能?

wen java案例 2

Java排行榜功能实现全攻略:从Redis到MySQL的实战案例解析

📖 文章目录导读

  1. 排行榜功能的应用场景与核心挑战
  2. 技术选型对比:Redis vs MySQL vs 内存排序
  3. 基于Redis Sorted Set的实时排行榜
    • 数据结构设计与核心API
    • 用户积分变更与排名更新
    • 分页查询与区间排名
  4. 基于MySQL的持久化排行榜
    • 数据库表设计
    • 高效SQL实现排名计算
    • 定时同步与缓存优化
  5. 实战代码案例:游戏积分排行榜
    • 完整Java代码演示
    • 性能测试对比数据
  6. 常见问题与最佳实践
  7. FAQ问答:排行榜高频误区

排行榜功能的应用场景与核心挑战

在互联网应用中,排行榜几乎无处不在:游戏积分榜、直播打赏榜、电商销量榜、文章热度榜等,一个成熟的排行榜系统需要解决三大核心问题:

Java案例如何实现排行榜功能?

  • 实时性:用户操作后排名能否秒级更新?
  • 准确性:同分情况下如何处理并列排名?
  • 高并发:千万级用户同时查询时如何保证性能?

实战场景:某手游需要实现“全服玩家战力排行榜”,每天凌晨0点更新,支持按段位分组查看,单条数据变更后需在30秒内反映到榜单上。


技术选型对比:Redis vs MySQL vs 内存排序

技术方案 适用场景 优势 劣势
Redis Sorted Set 实时性要求高、数据量百万级 O(logN)插入/查询,支持范围查找 内存成本高,无持久化需额外设计
MySQL 排序 数据量千万级、需要复杂过滤 支持SQL灵活查询,持久化可靠 全表排序性能瓶颈,不适用高频更新
内存排序(TreeMap等) 单机小规模(万级) 实现简单,延迟极低 无法分布式扩展,重启丢失数据

行业趋势:绝大多数商业级排行榜采用 Redis Sorted Set + MySQL 定期同步 的混合架构,既保证实时性又确保数据不丢。


方案一:基于Redis Sorted Set的实时排行榜

1 数据结构设计

Redis的Sorted Set(有序集合)天然适合排行榜:

  • keyranking:game:{gameId}(按游戏分区隔离)
  • memberuserId(用户唯一标识)
  • scoretotalScore(排序依据,支持浮点数)

2 核心API操作

// 添加/更新用户分数(自动排序)
redisTemplate.opsForZSet().add("ranking:game:1001", "user_123", 9850.0);
// 获取用户排名(从0开始,需+1)
Long rank = redisTemplate.opsForZSet().reverseRank("ranking:game:1001", "user_123");
// 返回0表示第一名,-1表示不在榜单
// 获取Top10
Set<ZSetOperations.TypedTuple<String>> top10 = 
    redisTemplate.opsForZSet().reverseRangeWithScores("ranking:game:1001", 0, 9);
// 获取指定分数区间用户(如8000-9000分段)
Set<String> rangeUsers = 
    redisTemplate.opsForZSet().rangeByScore("ranking:game:1001", 8000, 9000);

3 高性能要点

  • 原子性更新:使用 incrementScore 避免并发覆盖
  • 只保留Top N:通过 ZREMRANGEBYRANK 定期清理末尾用户
  • 合并同分排名:利用时间戳+分数组合score(如 score = 实际分数 + (1 - 时间戳/1e13)),实现“分数相同则先达者优先”

方案二:基于MySQL的持久化排行榜

1 数据库表设计

CREATE TABLE user_ranking (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    game_id INT NOT NULL,
    user_id VARCHAR(32) NOT NULL,
    total_score DECIMAL(12,2) NOT NULL DEFAULT 0,
    update_time DATETIME NOT NULL,
    UNIQUE KEY uk_game_user (game_id, user_id),
    INDEX idx_game_score (game_id, total_score DESC)
);

2 高效SQL实现排名

窗口函数(MySQL 8.0+)

SELECT 
    user_id,
    total_score,
    RANK() OVER (PARTITION BY game_id ORDER BY total_score DESC, update_time ASC) as rank
FROM user_ranking
WHERE game_id = 1001
LIMIT 10;

变量法(兼容旧版本)

SET @rank = 0, @prev_score = NULL;
SELECT 
    user_id,
    total_score,
    @rank := IF(@prev_score = total_score, @rank, @rank + 1) AS rank,
    @prev_score := total_score
FROM user_ranking
WHERE game_id = 1001
ORDER BY total_score DESC, update_time ASC;

3 性能优化策略

  • 索引覆盖(game_id, total_score, update_time) 复合索引
  • 定时缓存:每5分钟将Top100缓存到Redis,用于热点查询
  • 分区表:按游戏ID分区,单分区不超过500万行

实战代码案例:游戏积分排行榜

1 完整Service实现(Spring Boot)

@Service
public class RankingService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private UserRankingMapper rankingMapper;
    private static final String RANKING_KEY = "ranking:game:%d";
    private static final int TOP_N = 1000;
    // 玩家获得积分时调用
    @Transactional
    public void addScore(Long gameId, String userId, double score) {
        // 1. 更新数据库(持久化)
        rankingMapper.upsertScore(gameId, userId, score);
        // 2. 更新Redis(实时)
        String key = String.format(RANKING_KEY, gameId);
        redisTemplate.opsForZSet().incrementScore(key, userId, score);
        // 3. 维护榜单大小(定期清理999名之后)
        if (Math.random() < 0.01) { // 1%概率触发清理
            cleanExcessMembers(key);
        }
    }
    // 获取用户排名及前后10名
    public Map<String, Object> getRankingWithNeighbors(Long gameId, String userId) {
        String key = String.format(RANKING_KEY, gameId);
        Long myRank = redisTemplate.opsForZSet().reverseRank(key, userId);
        if (myRank == null) {
            return Map.of("code", 404, "msg", "用户未上榜");
        }
        // 计算前后范围
        long start = Math.max(0, myRank - 10);
        long end = myRank + 10;
        Set<ZSetOperations.TypedTuple<String>> neighbors = 
            redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
        return Map.of(
            "myRank", myRank + 1,
            "neighbors", buildRankingList(neighbors)
        );
    }
    private void cleanExcessMembers(String key) {
        Long size = redisTemplate.opsForZSet().size(key);
        if (size != null && size > TOP_N) {
            redisTemplate.opsForZSet().removeRange(key, 0, size - TOP_N - 1);
        }
    }
}

2 性能测试数据

场景 单次操作耗时 支撑QPS
Redis Sorted Set插入(10万数据) 3ms 30000
MySQL窗口函数排名查询(100万数据) 120ms 800
Redis+MySQL混合写入 2ms(异步) 50000

常见问题与最佳实践

问题1:排行榜出现“幽灵用户”

原因:用户注销后未从榜单中移除
解决:监听用户状态变更事件,调用 remove(key, userId) 同步删除

问题2:Redis宕机排行榜丢失

方案:启用Redis RDB+AOF持久化,配合MySQL定期全量重建脚本

问题3:同分排名规则不一致

最佳实践:在业务层定义规范——“同分按更新时间升序”,score可编码为:实际分 * 1e10 + (MAX_TIMESTAMP - updateTimestamp)


FAQ问答:排行榜高频误区

Q1:直接用MySQL的ORDER BY做排行榜,性能真的差吗?
A:如果表数据量超过10万行且并发查询量>1000/s,使用ORDER BY score DESC会导致全表排序,磁盘IO暴增,建议务必加索引,或用Redis兜底。

Q2:Redis Sorted Set最多能存多少数据?
A:单key建议不超过100万member(约占用200MB内存),若需千万级,建议按游戏ID/区服拆分多个key。

Q3:如何实现“只看好友排名”?
A:先获取用户好友列表(可用Redis Set存储好友关系),然后遍历好友ID逐个调用ZSCOREZRANK获取分数和排名,最后在应用层排序返回。

Q4:排行榜更新延迟怎么处理?
A:采用 最终一致性 设计:Redis写入后立即生效,MySQL异步批量落库(每10秒或每100条合并一次写),如果强一致性要求,可用分布式事务(Seata),但会降低性能。

抱歉,评论功能暂时关闭!