一亿玩家实时排名怎么实现?

前言面试中被问到 “1 亿玩家实时排名如何实现”,90% 的开发者只会说 “用 Redis”,但能讲清分片策略、同分数排序、性能优化 的人不足 10%。

这个问题的核心诉求远不止 “能用 Redis”,而是要满足:

排名毫秒级实时更新(玩家分数变化立即反映);

高并发查询(百万 QPS 下排名查询不卡顿);

海量数据支撑(1 亿玩家无性能瓶颈);

边界场景兼容(同分数、跨分片全局排名)。

在游戏、直播等互联网场景中,实时排名系统是提升用户参与感和刺激竞争的核心功能。本文将从零开始,详细讲解如何设计并实现一个支持1亿玩家、高并发实时更新的排名系统。

核心实现思路:为什么选 Redis ZSet?传统 MySQL 实现排名的痛点:

排序操作(ORDER BY)时间复杂度 O (n log n),1 亿数据直接卡死;

分数更新需要行锁/表锁,并发性能极差;

无法支撑高频次的排名查询。

而 Redis ZSet(有序集合)是最优解,核心优势:

性能极致:底层基于「跳表 + 哈希表」,增 / 删 / 改 / 查排名的时间复杂度均为 O (log N),1 亿数据的 log₂(1e8)≈27,毫秒级响应;

天然有序:自动按分数排序,无需手动维护;

原子操作:ZINCRBY、ZADD 等命令天然原子性,避免并发问题。

但单 ZSet 无法承载 1 亿数据,需补充 3 个关键策略:

分片策略:按玩家 ID 哈希拆分到多个 ZSet(如 100 个分片,每个 1000 万数据),规避单 Key 性能瓶颈;

分数重构:解决同分数排序问题(如 “先到钻石段位的玩家排名靠前”);

高可用保障:Redis 集群(主从 + 哨兵)+ 异步持久化到 MySQL 兜底,避免数据丢失。

一、Redis ZSet 基础先科普一下zset,Redis 的有序集合(Sorted Set,简称 ZSet)是一个功能强大且独特的数据结构。它的每个元素都由两部分组成:

成员 (Member):就像排行榜上的名字,比如玩家的ID、商品的编号,它在集合中是唯一的,不能重复。

分数 (Score):一个双精度浮点数,代表成员的“权重”或“分值”,比如玩家的得分、商品的销量。分数是排序的依据,可以重复。

ZSet 最核心的特性是,它会自动根据分数对成员进行从小到大的排序。如果多个成员分数相同,则再按成员自身的字典顺序排序 。它既保持了集合(Set)成员唯一的特性,又像列表(List)一样有序,但排序规则不是插入顺序,而是分数。

高频命令速查(实战常用)命令示例:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113# 添加数据,张三(100分)、李四(95分)、王五(98分)ZADD player_scores 100 "zhangsan" 95 "lisi" 98 "wangwu" 90 "zhaoliu" 88 "sunqi" 80 "zhouba" # 添加数据ZADD player_scores 60 "wujiu"# 给整个 Key 设置过期时间(两种常用方式)# 方式1:相对时间(单位:秒),示例:1小时后过期(3600秒)EXPIRE player_scores 3600# 方式2:绝对时间(单位:毫秒时间戳)PEXPIREAT player_scores 1770266046000# ====验证过期时间======# 返回剩余过期时间(秒),-1=永不过期,-2=已过期TTL player_scores# 返回剩余过期时间(毫秒)PTTL player_scores # 获取 王五 分数ZSCORE player_scores "wangwu"# 给王五增加10排序分数ZINCRBY player_scores 10 "wangwu"# 按排名查询,升序,前三名ZRANGE player_scores 0 2 # 按排名查询,升序,前三名及其积分WITHSCORES选项会同时返回积分ZRANGE player_scores 0 2 WITHSCORES# 按排名查询,升序,所有人ZRANGE player_scores 0 -1 WITHSCORES# 查询赵六排第几名,升序,索引从0开始ZRANK player_scores "zhaoliu"# 查询赵六排第几名,降序,索引从0开始ZREVRANK player_scores "zhaoliu"# 按排名查询,降序,前三名ZREVRANGE player_scores 0 2 WITHSCORES# ========== 按分数区间精准查询 ==========# ZRANGEBYSCORE:按分数升序查85-100分的成员(含边界,返回成员+分数)ZRANGEBYSCORE player_scores 85 100 WITHSCORES# 查≤100分的成员(-inf表示负无穷)ZRANGEBYSCORE player_scores -inf 100 WITHSCORES# 查>90且≤100分的成员(( 表示开区间,不含90)ZRANGEBYSCORE player_scores (90 100 WITHSCORES# ZREVRANGEBYSCORE:按分数降序查90-110分的成员ZREVRANGEBYSCORE player_scores 110 90 WITHSCORES# ========== 弹出元素(移除+返回) ==========# ZPOPMIN:弹出分数最小的1个成员(可指定数量,如ZPOPMIN key 3)ZPOPMIN player_scores# ZPOPMAX:弹出分数最大的1个成员ZPOPMAX player_scores# BZPOPMIN/BZPOPMAX:阻塞式弹出(无元素时阻塞,超时单位秒)# 阻塞5秒,无元素返回nilBZPOPMIN player_scores 5# 阻塞10秒,适用于异步消费场景BZPOPMAX player_scores 10# 删除指定成员ZREM player_scores "lisi"# 删除多个成员ZREM player_scores "lisi" "wangwu"# 按积分区间删除,删除积分在0到50之间的所有成员ZREMRANGEBYSCORE player_scores 0 50# 按排名区间删除,删除正序排名中最低的三名玩家ZREMRANGEBYRANK player_scores 0 2# 查询积分区间人数ZCOUNT player_scores 90 110# 查询成员数量ZCARD player_scores# ========== 多集合聚合运算 ==========# 先创建第二个集合(示例:另一场比赛的分数)ZADD player_scores_2 95 "zhangsan" 92 "lisi" 98 "zhaoliu"# ZUNIONSTORE:合并2个集合的并集,结果存入union_scores,分数求和ZUNIONSTORE union_scores 2 player_scores player_scores_2 AGGREGATE SUM# 查看并集结果ZRANGE union_scores 0 -1 WITHSCORES # ZINTERSTORE:计算2个集合的交集,结果存入inter_scores,分数取最大值ZINTERSTORE inter_scores 2 player_scores player_scores_2 AGGREGATE MAX# 查看交集结果ZRANGE inter_scores 0 -1 WITHSCORES # ========== 字典序查询(分数相同场景) ==========# 创建分数全为0的集合(字典序查询要求分数一致)ZADD dict_zset 0 "apple" 0 "banana" 0 "cherry" 0 "date"# ZLEXCOUNT:统计字典序区间内的成员数([b (d 表示b≤x

关键注意点

ZSet 的 Score 是双精度浮点数,需注意精度丢失(后文会用 Long 组合分数规避);

单 ZSet 建议数据量不超过 1000 万(Redis 单 Key 极限);

避免用 ZRANGE 0 -1 遍历大集合,优先用 ZSCAN 游标遍历。

二、实战为了方便操作redis,使用Redisson的RScoredSortedSet,简化了开发复杂度。

1、Maven 依赖123456789101112131415161718192021 17 3.52.0 org.springframework.boot spring-boot-starter-web org.projectlombok lombok org.redisson redisson-spring-boot-starter ${redisson.version}

2、配置文件12345678910111213141516171819202122spring: application: name: springboot-redis-zset data: redis: host: 127.0.0.1 port: 6379 password: yourPassword timeout: 5000ms lettuce: pool: max-active: 200 max-idle: 8 min-idle: 0 max-wait: 1000ms# 自定义排名配置rank: # 分片数(1亿/100=1000万/分片,单节点Redis可承载) shard-count: 200 key-prefix: "game:player:rank:"

3、当分数相同时,如何保证公平?在实际场景中(如王者荣耀段位排名),大量玩家可能拥有相同分数,但系统要求先达到该分数的玩家排名更靠前。

我们设计了分数组合策略:将原始分数与时间戳组合成一个Long类型数值:

1234567891011121314// 主分数占用高30位,时间戳占用低34位private static final int TIMESTAMP_BITS = 34;private static final long TIMESTAMP_MASK = (1L << TIMESTAMP_BITS) - 1;public static long combineScore(long mainScore, long updateTime) { if (mainScore < 0 || mainScore > 1_000_000_000) { throw new IllegalArgumentException("主分数必须在0-10亿之间: " + mainScore); } long timestampPart = updateTime & TIMESTAMP_MASK; timestampPart = TIMESTAMP_MASK - timestampPart; // 反转时间戳 return (mainScore << TIMESTAMP_BITS) | timestampPart;}

这种设计确保了相同分数下,先达到分数的玩家获得更高的组合分数,从而排名更靠前。

我们还有需要一个通过组合分数得到真正的主分数的方法:

1234public static long parseMainScore(double combinedScore) { // 右移34位获取主分数 return (long)combinedScore >>> TIMESTAMP_BITS;}

4、分片策略:平衡负载与查询效率单ZSet承载1亿数据仍有压力,我们采用哈希分片策略:

分片数过多(如 1000)→ 全局排名需查询 1000 个分片,性能下降.

分片数过少(如 10)→ 单个 ZSet 数据量 1 亿,Redis 单 Key 瓶颈

单分片数据量控制在 500 万~1000 万,1 亿玩家建议 100~200 个分片

分片键计算:

12345678910/** * 计算玩家所属的分片Key * @param playerId 玩家ID * @param rankType 排名类型 * @return 分片键 */private String getShardKey(Long playerId, int rankType) { int shardIndex = (int) (playerId % shardCount); return rankKeyPrefix + rankType + ":" + shardIndex;}

5、核心服务类设计1234567891011121314151617181920212223@Service@RequiredArgsConstructorpublic class PlayerRankService { private final RedissonClient redissonClient; // 分片配置 @Value("${rank.shard-count:100}") private int shardCount; @Value("${rank.key-prefix:game:player:rank:}") private String rankKeyPrefix; // 关键方法:实时更新玩家分数 public void updatePlayerScore(Long playerId, int rankType, long mainScore, long updateTime) { double combineScore = combineScore(mainScore, updateTime); String shardKey = getShardKey(playerId, rankType); RScoredSortedSet rankSet = redissonClient.getScoredSortedSet(shardKey); // 原子操作:添加或更新分数 rankSet.add(combineScore, playerId); }}

6、获取玩家所在的全局排名12345678910111213141516171819202122232425262728293031323334353637383940/** * 获取玩家详细信息 */@SneakyThrowspublic RankItem getPlayerRankDetail(Long playerId, int rankType) { String shardKey = getShardKey(playerId, rankType); RScoredSortedSet rankSet = redissonClient.getScoredSortedSet(shardKey); // 获取玩家排序分数 Double combineScore = rankSet.getScore(playerId); if (combineScore == null) { return null; } // 并行计算所有分片中分数大于当前玩家的数量 List> futures = new ArrayList<>(); for (int i = 0; i < shardCount; i++) { String currentShardKey = rankKeyPrefix + rankType + ":" + i; CompletableFuture future = CompletableFuture.supplyAsync(() -> { RScoredSortedSet shardSet = redissonClient.getScoredSortedSet(currentShardKey); // 对于当前玩家所在的分片,排除等于自己分数的情况 if (currentShardKey.equals(shardKey)) { return shardSet.count(combineScore, false, Double.MAX_VALUE, true); } // 对于其他分片,统计所有分数大于当前分数的玩家 else { return shardSet.count(combineScore, false, Double.MAX_VALUE, true); } }); futures.add(future); } // 汇总所有分片的结果 long higherCount = 0; for (CompletableFuture future : futures) { higherCount += future.get(); } long rank = higherCount + 1; long mainScore = parseMainScore(combineScore); return new RankItem(playerId, mainScore, rank, combineScore);}

我们采用并行查询+本地聚合策略

7、查询榜单前N名1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950/** * 获取榜单前N名(带分数和排名) */public List getTopRankList(int rankType, int start, int end) throws ExecutionException, InterruptedException { int neededCount = end; // 需要获取到第end名 // 从每个分片获取前neededCount名 List>>> futures = new ArrayList<>(); for (int i = 0; i < shardCount; i++) { String shardKey = rankKeyPrefix + rankType + ":" + i; CompletableFuture>> future = CompletableFuture.supplyAsync(() -> { RScoredSortedSet rankSet = redissonClient.getScoredSortedSet(shardKey); // 获取每个分片的前neededCount名(包含分数) Collection> entries = rankSet.entryRangeReversed(0, neededCount - 1); return new ArrayList<>(entries); }); futures.add(future); } // 合并所有分片结果 List> allEntries = new ArrayList<>(); for (CompletableFuture>> future : futures) { allEntries.addAll(future.get()); } // 按分数降序排序 allEntries.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); // 转换为RankItem列表,并添加排名 List result = new ArrayList<>(); int currentRank = 0; Double lastScore = null; Long actualRank = 1L; for (int i = 0; i < allEntries.size(); i++) { ScoredEntry entry = allEntries.get(i); double combineScore = entry.getScore(); int playerId = entry.getValue(); // 解析主分数 long mainScore = parseMainScore(combineScore); actualRank = (long) (i + 1); result.add(new RankItem(playerId, mainScore, actualRank, combineScore)); } // 返回指定范围的排名 int fromIndex = Math.min(start - 1, result.size()); int toIndex = Math.min(end, result.size()); return result.subList(fromIndex, toIndex);}

对于榜单前N名查询,我们采用各分片预取+全局归并策略

这样有个问题就是,如果end如果很大,在allEntries.addAll 可能会存在内存溢出引发 OOM,所以搞了个优化版

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778@SneakyThrowspublic List getTopRankListOptimize(int rankType, int start, int end) { if (start < 1 || end < start) { throw new IllegalArgumentException("排名范围无效: start=" + start + ", end=" + end); } // 需要获取的实际数量(从第1名到第end名) int needCount = end; // ========== 使用最小堆聚合所有分片的Top N ========== // 定义最小堆:堆顶是分数最小的,方便淘汰 PriorityQueue> minHeap = new PriorityQueue<>( needCount + 1, // 容量稍大,避免频繁扩容 Comparator.comparingDouble(ScoredEntry::getScore) // 按分数升序 ); // 并行查询所有分片,每个分片取needCount名 List> futures = new ArrayList<>(shardCount); for (int i = 0; i < shardCount; i++) { final int shardIndex = i; CompletableFuture future = CompletableFuture.runAsync(() -> { try { String shardKey = rankKeyPrefix + rankType + ":" + shardIndex; RScoredSortedSet rankSet = redissonClient.getScoredSortedSet(shardKey); // 从每个分片获取前needCount名(降序:分数高在前) Collection> entries = rankSet.entryRangeReversed(0, needCount - 1); // 同步加锁更新堆(多线程安全) synchronized (minHeap) { for (ScoredEntry entry : entries) { minHeap.offer(entry); // 堆大小超过needCount,弹出最小值(淘汰机制) if (minHeap.size() > needCount) { minHeap.poll(); } } } } catch (Exception e) { log.error("查询分片 {} 失败", shardIndex, e); } }, executor); futures.add(future); } // 等待所有分片查询完成 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); // 堆转列表并降序排序(分数高在前) List> topEntries = new ArrayList<>(minHeap); topEntries.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); // 降序 // ========== 第三步:转换为RankItem,处理同分排名逻辑 ========== List result = new ArrayList<>(topEntries.size()); // 真实排名(同分排名相同) long actualRank = 1; // 当前索引(从0开始) int currentIndex = 0; for (ScoredEntry entry : topEntries) { double combineScore = entry.getScore(); Integer playerId = entry.getValue(); // 解析主分数 long mainScore = parseMainScore(combineScore); // 分数变化:排名 = 当前索引 + 1 actualRank = currentIndex + 1; // 构建RankItem result.add(new RankItem(playerId, mainScore, actualRank, combineScore)); currentIndex++; } // 截取指定范围 [start, end] // 注意:start/end 从1开始,列表索引从0开始 int fromIndex = Math.max(0, start - 1); int toIndex = Math.min(end, result.size()); if (fromIndex >= toIndex) { return new ArrayList<>(); // 范围无效,返回空列表 } return result.subList(fromIndex, toIndex);}

定义最小堆:minHeap,添加同步锁,避免并发修改异常(ConcurrentModificationException)。

随便这样避免了内存溢出,但是因为加了锁会导致性能下降,如果end可控,第一版更好,根据业务自行决断

8、获取玩家周围的排名1234567891011121314151617181920212223/** * 获取玩家周围的排名(用于显示自己及前后几名) */public List getSurroundingRanks(Long playerId, int rankType, int before, int after) throws ExecutionException, InterruptedException { RankItem playerRank = getPlayerRankDetail(playerId, rankType); if (playerRank == null) { return new ArrayList<>(); } long playerRankValue = playerRank.getRank(); int start = (int) Math.max(1, playerRankValue - before); int end = (int) (playerRankValue + after); List range = getRankRange(rankType, start, end); // 标记当前玩家 for (RankItem item : range) { if (item.getPlayerId().equals(playerId)) { // 可以在这里添加标记,或者调用方根据ID判断 item.setIsCurrentUser(Boolean.TRUE); } } return range;}

9、测试接口Controller123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269import lombok.RequiredArgsConstructor;import lombok.SneakyThrows;import org.springframework.web.bind.annotation.*;import top.lrshuai.redis.zset.service.PlayerRankService;import top.lrshuai.redis.zset.vo.R;import top.lrshuai.redis.zset.vo.RankItem;import java.util.Arrays;import java.util.List;import java.util.Map;import java.util.concurrent.*;import java.util.stream.Collectors;/** * 玩家排名接口层 */@RestController@RequestMapping("/api/rank")@RequiredArgsConstructorpublic class PlayerRankController { private final PlayerRankService playerRankService; // 批次 private final long BATCH_SIZE=10000; private final long TOTAL=100000000; private final ThreadLocalRandom random = ThreadLocalRandom.current(); // 核心线程池 private final ExecutorService executor = new ThreadPoolExecutor( 5, // 核心线程数(初始并发) 10, // 最大线程数(峰值并发) 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), // 任务队列,避免线程过多 new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时,由调用线程执行(避免任务丢失) ); /** * 批量生成示例排名数据 */ @GetMapping("/batchAdd") public R batchAddPlayerData( @RequestParam(defaultValue = "1") int rankType, @RequestParam(defaultValue = "100") int totalCount, @RequestParam(defaultValue = "0") long beginId, //从什么用户ID开始 @RequestParam(defaultValue = "100") long minScore, @RequestParam(defaultValue = "30000") long maxScore) { try { if (totalCount <= 0 || minScore > maxScore) { return R.fail("参数错误:数量需大于0,最小分数不能大于最大分数"); } long batchNum = totalCount / BATCH_SIZE; long remainCount = totalCount % BATCH_SIZE; long updateTime = System.currentTimeMillis(); // 创建CountDownLatch,用于等待所有任务完成 CountDownLatch latch = new CountDownLatch((int)(batchNum + (remainCount > 0 ? 1 : 0))); // 提交分批任务 for (long batch = 0; batch < batchNum; batch++) { long startId = batch * BATCH_SIZE + 1+beginId; // 玩家ID起始值 long endId = (batch + 1) * BATCH_SIZE+beginId; submitBatchTaskWithLatch(rankType, startId, endId, minScore, maxScore, updateTime, latch); // 每批提交后稍微停顿一下,避免压力过大 Thread.sleep(100); } // 处理剩余数据 if (remainCount > 0) { long startId = batchNum * BATCH_SIZE + 1+beginId; long endId = batchNum * BATCH_SIZE + remainCount+beginId; submitBatchTaskWithLatch(rankType, startId, endId, minScore, maxScore, updateTime, latch); } // 等待所有批次任务完成 boolean allCompleted = latch.await(5, TimeUnit.MINUTES); // 设置5分钟超时 if (!allCompleted) { return R.fail("批量生成任务超时,部分数据可能未完成"); } return R.ok("批量生成" + totalCount + "条排名数据成功,所有任务已完成"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return R.fail("批量添加被中断:" + e.getMessage()); } catch (Exception e) { return R.fail("批量添加失败:" + e.getMessage()); } } /** * 提交单批插入任务(带同步锁) */ private void submitBatchTaskWithLatch(int rankType, long startId, long endId, long minScore, long maxScore, long updateTime, CountDownLatch latch) { executor.submit(() -> { try { for (long i = startId; i <= endId; i++) { // 生成随机主分数// long mainScore = minScore + random.nextLong() % (maxScore - minScore + 1); // 为了方便测试定位,分数和id 有关 long mainScore = TOTAL-i; System.out.println("i=" + i + ", mainScore=" + mainScore); // 调用服务插入 playerRankService.updatePlayerScore(i, rankType, mainScore, updateTime); } System.out.println("批次完成:player_" + startId + " ~ player_" + endId); } catch (Exception e) { System.err.println("批次插入失败:" + startId + "~" + endId + ",原因:" + e.getMessage()); } finally { // 无论成功还是失败,都释放锁 latch.countDown(); } }); } /** * 更新玩家分数(核心接口) * * @param playerId 玩家ID * @param rankType 榜单类型(1=战力榜,2=积分榜) * @param mainScore 主分数(如战力值) */ @PostMapping("/update") public R updateScore( @RequestParam Long playerId, @RequestParam int rankType, @RequestParam long mainScore) { // 用当前时间戳作为次分数,保证同主分数时的排序 long updateTime = System.currentTimeMillis(); playerRankService.updatePlayerScore(playerId, rankType, mainScore, updateTime); return R.ok("分数更新成功"); } /** * 查询榜单前N名玩家 * * @param rankType 榜单类型 * @param start 起始排名(1开始) * @param end 结束排名(1开始) */ @SneakyThrows @GetMapping("/top/{rankType}/{start}/{end}") public R getTopRankList(@PathVariable int rankType, @PathVariable int start, @PathVariable int end) { return R.ok(playerRankService.getTopRankList(rankType, start, end)); } /** * 获取玩家排名详情(包含分数和排名) * * @param playerId 玩家ID * @param rankType 榜单类型 */ @SneakyThrows @GetMapping("/detail/{rankType}/{playerId}") public R getPlayerRankDetail(@PathVariable int rankType, @PathVariable Long playerId) { RankItem rankDetail = playerRankService.getPlayerRankDetail(playerId, rankType); if (rankDetail == null) { return R.fail("玩家不存在或未参与排名"); } return R.ok(rankDetail); } /** * 获取指定分数的所有玩家 * * @param rankType 榜单类型 * @param mainScore 主分数 */ @SneakyThrows @GetMapping("/players-by-score/{rankType}/{mainScore}") public R getPlayersByMainScore(@PathVariable int rankType, @PathVariable long mainScore) { List players = playerRankService.getPlayersByMainScore(rankType, mainScore); return R.ok(players); } /** * 批量获取玩家排名信息 * * @param rankType 榜单类型 * @param playerIds 玩家ID列表,用逗号分隔 */ @SneakyThrows @GetMapping("/batch-detail/{rankType}") public R batchGetRankDetails(@PathVariable int rankType, @RequestParam String playerIds) { List idList = Arrays.stream(playerIds.split(",")).map(Long::parseLong).collect(Collectors.toList()); List rankDetails = playerRankService.batchGetRankDetails(idList, rankType); return R.ok(rankDetails); } /** * 获取玩家周围的排名(显示玩家及前后几名) * * @param rankType 榜单类型 * @param playerId 玩家ID * @param before 向前获取几名 * @param after 向后获取几名 */ @SneakyThrows @GetMapping("/surrounding/{rankType}/{playerId}") public R getSurroundingRanks(@PathVariable int rankType, @PathVariable Long playerId, @RequestParam(defaultValue = "5") int before, @RequestParam(defaultValue = "5") int after) { List surrounding = playerRankService.getSurroundingRanks(playerId, rankType, before, after); return R.ok(surrounding); } /** * 分页查询排行榜 * * @param rankType 榜单类型 * @param page 页码(从1开始) * @param pageSize 每页大小 */ @SneakyThrows @GetMapping("/page/{rankType}") public R getRankByPage(@PathVariable int rankType, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize) { if (page < 1 || pageSize < 1 || pageSize > 1000) { return R.fail("参数错误:页码需大于0,每页大小在1-1000之间"); } int start = (page - 1) * pageSize + 1; int end = page * pageSize; List rankList = playerRankService.getTopRankList(rankType, start, end); return R.ok(rankList); } /** * 获取排行榜统计信息 * * @param rankType 榜单类型 */ @SneakyThrows @GetMapping("/stats/{rankType}") public R getRankStats(@PathVariable int rankType) { // 这里可以返回一些统计信息,比如总玩家数、平均分数等 // 注意:这些数据可能需要额外计算,这里只是示例 Map stats = new ConcurrentHashMap<>(); stats.put("rankType", rankType); stats.put("timestamp", System.currentTimeMillis()); // 获取前10名 List top10 = playerRankService.getTopRankList(rankType, 1, 10); stats.put("top10", top10); // 获取玩家总数(需要遍历所有分片) long totalPlayers = 0; for (int i = 0; i < playerRankService.getShardCount(); i++) { String shardKey = playerRankService.getRankKeyPrefix() + rankType + ":" + i; org.redisson.api.RScoredSortedSet rankSet = playerRankService.getRedissonClient().getScoredSortedSet(shardKey); totalPlayers += rankSet.size(); } stats.put("totalPlayers", totalPlayers); return R.ok(stats); }}

我的电脑配置一般,测试了新增100万数据,需要76秒。

三、结尾实现的思路和代码写完了,虽然有点粗糙,但是思路是没问题的,通过分片策略、分数组合算法和并行查询优化,实现了高性能、高可用的排名服务。

资源获取:本文完整代码已上传至 GitHub,欢迎 Star ⭐ 和 Fork

Gitee地址:https://gitee.com/rstyro/enterprise-examples

Github地址:https://github.com/rstyro/enterprise-examples

如果这篇文章帮到了你,欢迎点赞、收藏、转发~关注我,后续分享更多高并发、分布式实战方案!🔥

Copyright © 2088 王者太极网游活动福利平台 All Rights Reserved.
友情链接