Java排行榜功能实现全攻略:从Redis到MySQL的实战案例解析
📖 文章目录导读
- 排行榜功能的应用场景与核心挑战
- 技术选型对比:Redis vs MySQL vs 内存排序
- 基于Redis Sorted Set的实时排行榜
- 数据结构设计与核心API
- 用户积分变更与排名更新
- 分页查询与区间排名
- 基于MySQL的持久化排行榜
- 数据库表设计
- 高效SQL实现排名计算
- 定时同步与缓存优化
- 实战代码案例:游戏积分排行榜
- 完整Java代码演示
- 性能测试对比数据
- 常见问题与最佳实践
- FAQ问答:排行榜高频误区
排行榜功能的应用场景与核心挑战
在互联网应用中,排行榜几乎无处不在:游戏积分榜、直播打赏榜、电商销量榜、文章热度榜等,一个成熟的排行榜系统需要解决三大核心问题:

- 实时性:用户操作后排名能否秒级更新?
- 准确性:同分情况下如何处理并列排名?
- 高并发:千万级用户同时查询时如何保证性能?
实战场景:某手游需要实现“全服玩家战力排行榜”,每天凌晨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(有序集合)天然适合排行榜:
- key:
ranking:game:{gameId}(按游戏分区隔离) - member:
userId(用户唯一标识) - score:
totalScore(排序依据,支持浮点数)
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逐个调用ZSCORE和ZRANK获取分数和排名,最后在应用层排序返回。
Q4:排行榜更新延迟怎么处理?
A:采用 最终一致性 设计:Redis写入后立即生效,MySQL异步批量落库(每10秒或每100条合并一次写),如果强一致性要求,可用分布式事务(Seata),但会降低性能。