在日常开发中,排行榜场景几乎无处不在——电商的销量榜、短视频的点赞榜、游戏的战力榜、社区的积分榜,甚至是公司内部的业绩榜,都需要高效、实时地展示数据排序结果。
而提到“实时排行榜+高并发”,Redis绝对是首选方案。相比于数据库排序(慢查询、扛不住高并发)、本地缓存排序(集群不一致),Redis凭借其高效的Sorted Set(有序集合)结构,能轻松支撑百万级数据的实时排序,响应速度毫秒级,而且实现成本极低。
今天就带大家从实战出发,不搞虚的理论,一步步实现一个可直接上线的Redis排行榜功能,涵盖核心API用法、完整代码实现、高并发优化,以及新手最容易踩的坑,看完直接复制可用!
一、先搞懂:Redis为什么能搞定排行榜?
很多新手会疑惑,为什么不用MySQL的ORDER BY做排序?其实答案很简单——效率和并发扛不住。
如果排行榜数据存在MySQL中,每次查询都要执行“SELECT * FROM xxx ORDER BY score DESC LIMIT 10”,一旦数据量达到10万+,每次排序都是全表扫描/索引扫描,响应时间会飙升到几百毫秒甚至几秒;更别说高并发场景下,频繁的排序查询会直接把数据库压垮。
而Redis的Sorted Set(有序集合),天生就是为排行榜设计的,核心优势有3点:
- 底层优化,排序无压力:Sorted Set底层采用“跳表+哈希表”结构,添加、删除、修改分数(score)、排序的时间复杂度都是O(logN),百万级数据操作也能毫秒级响应。
- 自带排序,无需额外处理:存入数据时,Redis会自动根据你设置的score(排序权重)排序,无需手动执行排序逻辑,查询时直接取TopN即可。
- 支持多维度,灵活适配场景:可根据score正序、倒序排序,支持获取某个元素的排名、分数,还能批量更新分数,适配销量榜(按销量)、积分榜(按积分)、热度榜(按综合分数)等各种场景。
核心结论:只要是“实时排序+高并发”的排行榜,Redis Sorted Set就是最优解,没有之一。
二、实战准备:环境与核心API梳理
在写代码之前,先梳理好必备环境和核心API,避免写代码时手忙脚乱,新手也能快速跟上。
1. 环境准备(通用版)
- Redis:5.0+(推荐6.0+,稳定性更好,支持更多新特性)
- 客户端:Spring Data Redis(简化Redis操作,无需手动写原生API)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><scope>runtime</scope></dependency>
2. 核心API(重中之重,记牢这5个就够了)
Redis Sorted Set的API不多,实现排行榜只需要用到以下5个,直接对应排行榜的核心操作,建议收藏:
| | | |
|---|
| | redisTemplate.opsForZSet().add(key, member, score) | member是唯一标识(如用户ID、商品ID),score是排序权重(如积分、销量);存在则更新score,不存在则新增 |
| ZREVRANGE key 0 n-1 WITHSCORES | redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, n-1) | 倒序(score从大到小),获取前n名,WITHSCORES表示同时获取score |
| | redisTemplate.opsForZSet().reverseRank(key, member) | 倒序排名,返回值从0开始(第1名返回0,第2名返回1) |
| | redisTemplate.opsForZSet().score(key, member) | |
| | redisTemplate.opsForZSet().remove(key, member) | 适用于元素下架、用户注销等场景,删除对应排行榜数据 |
补充:如果需要正序排行榜(score从小到大),把reverse开头的方法换成普通方法即可(如rangeWithScores、rank)。
三、完整实战:实现一个用户积分排行榜
接下来,我们以“用户积分排行榜”为例,实现一个可直接上线的功能,涵盖:添加/更新积分、获取Top10排行榜、获取单个用户排名和积分、删除用户排行榜数据,全程实战,代码可直接复制。
1. 需求梳理(贴近真实业务)
- 用户完成任务、签到、消费等操作,增加积分(更新排行榜score);
- 首页展示积分Top10排行榜,显示用户名、积分、排名;
2. 代码实现(Spring Boot + Redis)
我们采用“分层架构”,分为Service层(核心业务逻辑)、Controller层(接口暴露),简化冗余代码,新手也能看懂。
(1)配置Redis(application.yml)
spring:redis:host:127.0.0.1# 你的Redis地址port:6379# Redis端口password:# 没有密码则留空database:0# 数据库索引(默认0)jedis:pool:max-active:100# 最大连接数max-idle:20# 最大空闲连接min-idle:5# 最小空闲连接timeout:1000ms# 连接超时时间
(2)核心Service层(实现排行榜逻辑)
这里封装了所有排行榜相关的操作,方法名清晰,注释详细,直接调用即可。
import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.ZSetOperations;import org.springframework.stereotype.Service;import javax.annotation.Resource;import java.util.ArrayList;import java.util.List;import java.util.Set;/** * Redis用户积分排行榜Service */@Servicepublic classRedisRankingService{// 排行榜的Redis Key(统一命名,避免混淆)private static final String RANKING_KEY = "user:score:ranking";@Resourceprivate RedisTemplate<String, Object> redisTemplate;/** * 1. 添加/更新用户积分(核心操作) * @param userId 用户ID(member) * @param score 积分(排序权重,可正数增加、负数减少) */publicvoidupdateUserScore(Long userId, Double score){// ZADD:存在则更新score,不存在则新增 redisTemplate.opsForZSet().add(RANKING_KEY, userId, score); }/** * 2. 获取积分TopN排行榜(倒序,从高到低) * @param topN 前N名(如10则返回Top10) * @return 排行榜列表(包含用户ID、积分、排名) */public List<RankingVO> getTopRanking(Integer topN){// ZREVRANGE WITHSCORES:倒序获取前topN名,包含score Set<ZSetOperations.TypedTuple<Object>> typedTuples = redisTemplate.opsForZSet() .reverseRangeWithScores(RANKING_KEY, 0, topN - 1); List<RankingVO> rankingVOList = new ArrayList<>();if (typedTuples == null || typedTuples.isEmpty()) {return rankingVOList; }// 遍历结果,封装成VO(排名从1开始)int rank = 1;for (ZSetOperations.TypedTuple<Object> tuple : typedTuples) { Long userId = (Long) tuple.getValue(); Double score = tuple.getScore(); RankingVO vo = new RankingVO(userId, score, rank); rankingVOList.add(vo); rank++; }return rankingVOList; }/** * 3. 获取单个用户的排名和积分 * @param userId 用户ID * @return RankingVO(无数据则返回null) */public RankingVO getUserRanking(Long userId){// 获取用户积分 Double score = redisTemplate.opsForZSet().score(RANKING_KEY, userId);if (score == null) {return null; // 该用户未参与排行榜 }// 获取用户排名(倒序,返回0开始,需+1) Long rank = redisTemplate.opsForZSet().reverseRank(RANKING_KEY, userId);returnnew RankingVO(userId, score, rank.intValue() + 1); }/** * 4. 删除用户排行榜数据(用户注销等场景) * @param userId 用户ID */publicvoiddeleteUserFromRanking(Long userId){ redisTemplate.opsForZSet().remove(RANKING_KEY, userId); }}
(3)VO类(封装返回结果)
用于向前端返回排行榜数据,避免直接返回Redis原生结果,规范接口返回格式。
import lombok.AllArgsConstructor;import lombok.Data;/** * 排行榜返回VO */@Data@AllArgsConstructorpublicclassRankingVO{// 用户IDprivate Long userId;// 用户积分private Double score;// 排名(从1开始)private Integer rank;}
(4)Controller层(暴露接口,供前端调用)
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;import java.util.List;/** * 积分排行榜接口 */@RestController@RequestMapping("/api/ranking")public classRankingController{@Resourceprivate RedisRankingService redisRankingService;/** * 更新用户积分 * @param userId 用户ID * @param score 积分(正数增加,负数减少) */@PostMapping("/updateScore")publicvoidupdateScore(@RequestParam Long userId, @RequestParam Double score){ redisRankingService.updateUserScore(userId, score); }/** * 获取TopN排行榜 * @param topN 前N名(默认10) * @return 排行榜列表 */@GetMapping("/top")public List<RankingVO> getTopRanking(@RequestParam(required = false, defaultValue = "10") Integer topN) {return redisRankingService.getTopRanking(topN); }/** * 获取单个用户的排名和积分 * @param userId 用户ID * @return 单个用户排行榜信息 */@GetMapping("/user")public RankingVO getUserRanking(@RequestParam Long userId){return redisRankingService.getUserRanking(userId); }/** * 删除用户排行榜数据 * @param userId 用户ID */@DeleteMapping("/delete/{userId}")publicvoiddeleteUserRanking(@PathVariable Long userId){ redisRankingService.deleteUserFromRanking(userId); }}
3. 接口测试(验证功能是否可用)
我们用Postman测试接口,确保每个功能都能正常运行,新手也可以跟着测:
- 更新用户积分:POST /api/ranking/updateScore?userId=1&score=100(用户1积分100);同理添加用户2(score=200)、用户3(score=150);
- 获取Top10排行榜:GET /api/ranking/top,返回结果应该是 user2(200分,第1名)→ user3(150分,第2名)→ user1(100分,第3名);
- 获取单个用户排名:GET /api/ranking/user?userId=3,返回 rank=2,score=150;
- 删除用户:DELETE /api/ranking/delete/1,再查询Top10,用户1会被移除。
测试通过后,就可以对接前端页面,一个简单的积分排行榜就实现完成了,直接上线无压力!
四、高并发优化:避开这3个坑,支撑百万级流量
上面的代码适用于中小流量场景,但如果是高并发场景(如电商大促、游戏活动,每秒上千次积分更新),直接用会出现问题,这里分享3个核心优化点,避开新手常踩的坑。
坑1:积分更新错乱(并发修改score)
问题:高并发下,多个请求同时更新同一个用户的积分(如同时给用户1加10分),会出现积分错乱(比如应该加20分,实际只加了10分)。
解决方案:用Redis的ZINCRBY命令,原子性更新score,替代ZADD(ZADD是非原子性的)。
// 优化后的更新积分方法(原子性操作)publicvoidupdateUserScore(Long userId, Double score){// ZINCRBY:原子性增加score,score可正可负(负数就是减少积分) redisTemplate.opsForZSet().incrementScore(RANKING_KEY, userId, score);}
说明:ZINCRBY是原子性命令,Redis会保证同一时间只有一个请求能修改该用户的score,彻底解决并发错乱问题。
坑2:排行榜查询卡顿(高并发查询TopN)
问题:每秒上千次查询Top10排行榜,虽然Redis响应快,但频繁查询还是会增加Redis压力,导致卡顿。
解决方案:排行榜结果缓存+定时更新,减少Redis查询次数。
- 用Redis的String结构,缓存Top10排行榜结果(序列化RankingVOList);
- 用定时任务(如每10秒),重新查询Top10,更新缓存;
- 前端查询时,直接查缓存,而非直接查Sorted Set。
补充:定时任务的间隔,根据业务需求调整(如积分榜10秒更新一次,销量榜1分钟更新一次),兼顾实时性和性能。
坑3:Redis内存溢出(海量数据堆积)
问题:如果排行榜有百万级用户,Sorted Set会占用大量Redis内存,长期堆积会导致内存溢出。
解决方案:设置过期时间+限制排行榜数量,清理无效数据。
// 1. 设置排行榜过期时间(如7天,根据业务调整)redisTemplate.expire(RANKING_KEY, 7, TimeUnit.DAYS);// 2. 限制排行榜数量(只保留前1000名,超出部分删除,减少内存占用)// 每次更新积分后,删除排名1000以后的用户redisTemplate.opsForZSet().removeRange(RANKING_KEY, 0, -1001);
说明:大部分业务场景下,用户只关心前几百名,保留前1000名完全足够,既节省内存,又不影响用户体验。
五、扩展场景:不止是简单排行榜
上面实现的是“单一维度排行榜”(按积分排序),但实际业务中,经常会遇到更复杂的场景,这里分享2个常见扩展,只需简单修改代码就能实现。
扩展1:多维度排行榜(如日榜、周榜、月榜)
需求:同时展示用户积分的日榜、周榜、月榜,各自独立排序。
解决方案:用不同的Redis Key区分不同榜单,如:
- 日榜Key:user:score:ranking:day:20260220(日期拼接,每天一个新Key);
- 周榜Key:user:score:ranking:week:202608(年份+周数);
- 月榜Key:user:score:ranking:month:202602(年份+月份)。
然后在Service层,增加一个参数(榜单类型),根据类型选择对应的Key即可。
扩展2:带条件的排行榜(如指定用户群体)
需求:只展示“VIP用户”的积分排行榜,普通用户不参与排序。
解决方案:用Redis的Hash结构,存储用户类型(VIP/普通),更新积分时,先判断用户类型,只有VIP用户才加入排行榜。
六、总结:实战核心要点
Redis排行榜功能,核心就是“利用Sorted Set的有序特性”,实战中记住3个核心要点,就能应对大部分场景:
- 核心API:记住ZADD/ZINCRBY(更新)、ZREVRANGE(查TopN)、ZREVRANK(查排名),5个API搞定80%场景;
- 高并发优化:用ZINCRBY保证原子性,用缓存+定时任务减少查询压力,用过期时间+数量限制控制内存;
- 灵活扩展:多维度榜单用不同Key,带条件榜单结合Hash结构,无需重构核心代码。
📌 关注我,每天 5 分钟,带你从 Java 小白变身编程高手!
👉 点赞 + 关注,让更多小伙伴一起进步!