实现“上亿用户的积分实时排行榜功能”
是一个极具挑战性且需要高性能架构支持的任务。以下为您讲解其核心思路和关键技术:
一、关键目标和难点
- 实时性:用户积分变动后,排行榜需要迅速更新。
- 高并发:上亿用户同时操作,系统需要承受高并发请求。
- 排序效率:需要快速计算出排名,尤其是前几名和某个用户的排名。
- 存储优化:存储上亿用户的积分和排名数据,同时保证系统性能。
二、实现思路
2-1 使用高性能的排序数据结构
-
为了实时计算用户的积分排名,可以利用 Redis 的有序集合(Sorted Set)。
-
Redis 的有序集合是一种基于分数排序的数据结构,数据以键值对的形式存储,键为用户 ID 或用户名,值为用户的积分。其主要特性包括:
- 分数(Score):一个浮点数,用于对集合内的成员排序。
- 成员(Member):集合中的唯一标识符,通常是用户 ID。
- 支持根据
分数的范围
、排名范围
进行查找
和排序
操作。 - 假设我们有如下用户积分数据:
用户 ID 用户名 积分(Score) 1 Alice 102 2 Bob 98 3 Charlie 120 - 在 Redis 中的存储结构如下:
Sorted Set Key: leaderboard Member: 用户 ID Score: 用户积分
-
Redis Sorted Set 的特性:
- 支持
O(logN)
的插入、删除和更新操作。 - 能够按照分数快速排序,并支持根据分数范围或排名范围获取数据。
- 可以直接获取用户的排名(如
ZRANK
命令)或查看排行榜前 N 的用户(如ZREVRANGE
命令)。
- 支持
示例:
- 用户积分变动时,用
ZADD
命令更新用户积分到排行榜:ZADD leaderboard score user_id
- 查询用户排名:
ZREVRANK leaderboard user_id
- 查询排行榜前 10 名:
ZREVRANGE leaderboard 0 9 WITHSCORES
- 实现流程图
- 以下是积分排行榜功能的流程:
- 使用 Redis 的有序集合管理积分排行榜
以下代码实现了基本的用户积分操作与排行榜查询:
- Maven 依赖(使用 Redis 的 Jedis 客户端)
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
- Java 实现
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;
public class LeaderboardService {
private static final String LEADERBOARD_KEY = "leaderboard"; // Redis 中的有序集合键名
private final Jedis jedis;
public LeaderboardService() {
this.jedis = new Jedis("localhost", 6379); // 连接 Redis
}
// 更新用户积分
public void updateScore(String userId, double score) {
jedis.zadd(LEADERBOARD_KEY, score, userId); // 使用 ZADD 命令更新积分
}
// 获取用户的排名
public long getUserRank(String userId) {
Long rank = jedis.zrevrank(LEADERBOARD_KEY, userId); // 使用 ZREVRANK 查询排名(从高到低)
return rank == null ? -1 : rank + 1; // Redis 返回的排名从 0 开始计数
}
// 获取排行榜前 N 名
public Set<Tuple> getTopN(int n) {
return jedis.zrevrangeWithScores(LEADERBOARD_KEY, 0, n - 1); // ZREVRANGE 获取前 N 名
}
public static void main(String[] args) {
LeaderboardService leaderboard = new LeaderboardService();
// 更新积分
leaderboard.updateScore("1", 102); // Alice
leaderboard.updateScore("2", 98); // Bob
leaderboard.updateScore("3", 120); // Charlie
// 查询排名
System.out.println("Alice 的排名:" + leaderboard.getUserRank("1"));
// 查询前 3 名
Set<Tuple> top3 = leaderboard.getTopN(3);
System.out.println("排行榜前 3 名:");
for (Tuple tuple : top3) {
System.out.println("用户 ID: " + tuple.getElement() + ",积分: " + tuple.getScore());
}
}
}
- 输出结果
Alice 的排名:2
排行榜前 3 名:
用户 ID: 3,积分: 120.0
用户 ID: 1,积分: 102.0
用户 ID: 2,积分: 98.0
2-2 分区存储
- 上亿用户的数据量可能会超出单台Redis实例的处理能力。因此,可以使用 分片(Sharding)方案:
- 将用户按
某种规则
(如用户ID
的哈希值
)划分到不同的Redis
实例中。 每个分片
维护一部分
用户的积分排行榜
。
- 将用户按
- 全局排行榜可以通过 定期合并分片数据 的方式生成。
-
异步计算与缓存
- 对于实时性要求较高的功能(如前 100 名排行榜),可以提前异步计算并缓存结果:
- 每次积分更新时,触发后台任务更新前 N 名榜单。
- 客户端直接从缓存中读取榜单数据,减轻实时查询的压力。
- 对于实时性要求较高的功能(如前 100 名排行榜),可以提前异步计算并缓存结果:
-
增量更新和冷热分离
- 并非所有用户的积分会频繁变动。将活跃用户和不活跃用户分开处理:
- 活跃用户积分变动时,实时更新Redis。
- 不活跃用户可以定期批量更新排行榜,减少系统压力。
- 并非所有用户的积分会频繁变动。将活跃用户和不活跃用户分开处理:
-
数据库持久化与备份
- Redis 虽然性能高,但属于内存数据库,可能会因为重启或崩溃导致数据丢失。
- 可以将积分数据定期同步到关系型数据库(如 MySQL)或分布式存储系统(如 HBase)中,作为持久化备份。
-
分片(Sharding)方案示例
- 分片策略
-
- 将用户的积分数据按用户 ID 的哈希值分配到不同的 Redis 实例中。
-
- 每个 Redis 实例维护一个子排行榜(分片排行榜)。
-
- 定期将各分片排行榜的数据合并,生成全局排行榜。
-
- 分片示例
import redis.clients.jedis.Jedis; import java.util.HashMap; import java.util.Map; public class ShardedLeaderboardService { private final Map<Integer, Jedis> redisShards = new HashMap<>(); public ShardedLeaderboardService() { // 假设有两个 Redis 实例,分别监听 6379 和 6380 端口 redisShards.put(0, new Jedis("localhost", 6379)); redisShards.put(1, new Jedis("localhost", 6380)); } // 根据用户 ID 计算分片编号 private int getShardIndex(String userId) { return Math.abs(userId.hashCode() % redisShards.size()); } // 更新用户积分 public void updateScore(String userId, double score) { int shardIndex = getShardIndex(userId); Jedis shard = redisShards.get(shardIndex); shard.zadd("leaderboard_" + shardIndex, score, userId); // 在对应分片中更新积分 } // 合并分片排行榜 public void mergeGlobalLeaderboard() { Map<String, Double> globalScores = new HashMap<>(); for (int i = 0; i < redisShards.size(); i++) { Jedis shard = redisShards.get(i); Map<String, Double> scores = shard.zrangeWithScores("leaderboard_" + i, 0, -1) .stream() .collect(HashMap::new, (m, t) -> m.put(t.getElement(), t.getScore()), HashMap::putAll); scores.forEach((userId, score) -> globalScores.merge(userId, score, Double::max)); // 合并分数 } // 将合并后的结果写回全局排行榜 Jedis globalRedis = redisShards.get(0); // 假设全局排行榜存储在第一个实例 globalScores.forEach((userId, score) -> globalRedis.zadd("global_leaderboard", score, userId)); } }
-
三、技术架构示例
以下是一个典型实现架构:
-
用户操作层(客户端):
- 用户通过手机或网页发起积分相关操作(如完成任务、消费积分等)。
- 调用后端接口更新积分。
-
后端服务层:
- 接收用户的积分更新请求,将积分变动写入 Redis 的有序集合。
- 提供查询接口返回排行榜前 N 名或某用户的排名。
-
缓存和分布式存储:
- 使用 Redis 存储实时排行榜数据。
- 对于大规模用户分片存储数据,提升性能。
-
数据持久化:
- 定期将 Redis 中的积分数据同步到数据库,确保数据安全。
-
异步任务系统:
- 利用消息队列(如 Kafka 或 RabbitMQ)处理积分更新的异步任务。
- 定期计算全局排行榜或分片合并。
四、常见优化策略
-
热点优化:
- 对于频繁查询的热门数据(如 Top 10 榜单),通过缓存机制减少重复计算。
-
限流和降级:
- 当并发量过高时,可以限制排行榜的实时更新频率(如每分钟更新一次)。
-
监控和报警:
- 部署监控工具(如 Prometheus),实时监控系统的性能和异常。
-
优化策略的代码实践
-
缓存热门排行榜
- 为了减少访问 Redis 的请求次数,可以将热门排行榜(如前
100 名
)存储在缓存(如Guava Cache
)中:
import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.concurrent.TimeUnit; public class CachingLeaderboardService extends LeaderboardService { private final Cache<String, Set<Tuple>> cache = CacheBuilder.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) // 缓存 5 分钟 .maximumSize(100) // 缓存大小为 100 .build(); @Override public Set<Tuple> getTopN(int n) { return cache.get("top_" + n, () -> super.getTopN(n)); // 缓存中查找,否则从 Redis 查询 } }
- 为了减少访问 Redis 的请求次数,可以将热门排行榜(如前
五、场景应用
模拟真实应用场景,结合 Java 和 Redis 编写代码,实现积分实时排行榜功能。
应用场景背景描述:
假设我们正在开发一个手机游戏——《冒险之旅》。玩家完成任务、击败怪物、参加活动等行为都会获得积分,积分用于衡量玩家的游戏成就。为了增加竞争性,系统需要展示:
- 全服排行榜:展示所有玩家的实时积分排名。
- 好友排行榜:展示玩家所在好友圈的排名。
- 玩家排名查询:查询某个玩家的当前排名与积分。
功能描述:
- 玩家完成任务后,积分更新到数据库,同时更新本地缓存和
Redis
缓存。 - 支持查询某玩家的
积分排名
和总积分
。 - 支持分页查询排行榜
前 N 名
。 - 数据存储在
MySQL
,通过Redis
提供实时缓存,使用Guava Cache
提供热点数据的本地缓存。
功能需求:
- 实时更新积分:玩家完成任务后,积分应立即更新,并反映在排行榜中。
- 高效查询排名:能够快速查询玩家的排名。
- 分页显示排行榜:支持获取排行榜的任意分页数据(如
前 10 名
、前 50 名
)。 - 分布式支持:支持上亿玩家参与,系统需具备高并发处理能力。
技术栈
- Spring Boot 2.7+
- Redis (Jedis)
- MySQL (持久化存储)
- MyBatis-Plus (ORM 框架)
- Guava Cache (本地缓存)
- Maven (项目管理)
技术设计思路
- 使用 Redis 有序集合(Sorted Set) 存储玩家积分排行榜。
- Key:
global_leaderboard
(全服排行榜) - Member:
玩家ID(userId)
- Score:
玩家积分(score)
- Key:
- 分布式支持(可选): 通过分片(
sharding
)方案,将玩家按 ID 分布到不同Redis
实例中。 - 缓存优化: 热门排行榜(如
Top 100
)定期缓存,减少实时查询Redis
的压力。 - 数据持久化: 定期将排行榜数据同步到
MySQL
数据库,以便查询历史数据和备份。
全局积分排行榜代码实现
数据库表结构设计(MySQL)
创建一个表 player_score
来存储玩家的积分信息:
CREATE TABLE player_score (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
user_id VARCHAR(64) NOT NULL COMMENT '玩家ID',
score DOUBLE NOT NULL DEFAULT 0 COMMENT '玩家积分',
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='玩家积分表';
Maven 依赖
在 pom.xml
中添加以下依赖:
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Guava Cache -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
</dependencies>
配置文件
配置文件 application.yml
:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/game?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
Redis 数据结构
Key | Member | Score |
---|---|---|
global_leaderboard | 玩家 ID(user1) | 玩家积分(300) |
Service 层(单实例)
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;
public class GameLeaderboard {
private static final String GLOBAL_LEADERBOARD = "global_leaderboard";
private final Jedis jedis;
public GameLeaderboard() {
this.jedis = new Jedis("localhost", 6379); // 连接 Redis
}
/***
* 更新游戏积分
* @param userId 玩家ID
* @param score 玩家新增的积分
*/
public void updatePlayerScore(String userId, double score) {
jedis.zincrby(GLOBAL_LEADERBOARD, score, userId); // 使用 ZINCRBY 命令累加积分
}
/***
* 获取玩家排名
* @param userId 玩家ID
* @return 返回玩家排名(从1开始计数)
*/
public long getPlayerRank(String userId) {
Long rank = jedis.zrevrank(GLOBAL_LEADERBOARD, userId); // ZREVRANK 从高到低排名
return (rank == null) ? -1 : rank + 1; // Redis 的排名从0开始
}
/***
* 获取玩家积分
* @param userId 玩家ID
* @return 返回玩家当前积分
*/
public double getPlayerScore(String userId) {
Double score = jedis.zscore(GLOBAL_LEADERBOARD, userId); // ZSCORE 获取积分
return (score == null) ? 0.0 : score;
}
/**
* 获取排行榜分页数据
* @param start 开始排名(从0开始)
* @param end 结束排名
* @return 返回对应范围内的玩家及其积分
*/
public Set<Tuple> getLeaderboard(int start, int end) {
return jedis.zrevrangeWithScores(GLOBAL_LEADERBOARD, start, end); // ZREVRANGE 获取范围内的排名
}
public static void main(String[] args) {
GameLeaderboard leaderboard = new GameLeaderboard();
// 模拟玩家积分更新
leaderboard.updatePlayerScore("user1", 300);
leaderboard.updatePlayerScore("user2", 450);
leaderboard.updatePlayerScore("user3", 200);
// 查询某玩家排名
System.out.println("User1 的排名:" + leaderboard.getPlayerRank("user1"));
System.out.println("User1 的积分:" + leaderboard.getPlayerScore("user1"));
// 查询排行榜前 10 名
Set<Tuple> topPlayers = leaderboard.getLeaderboard(0, 9);
System.out.println("排行榜前 10 名:");
for (Tuple player : topPlayers) {
System.out.println("玩家ID: " + player.getElement() + ", 积分: " + player.getScore());
}
}
}
Java 实现代码
实体类
PlayerScore.java
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class PlayerScore {
private Long id;
private String userId;
private Double score;
private LocalDateTime updateTime;
}
Mapper 接口
PlayerScoreMapper.java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PlayerScoreMapper extends BaseMapper<PlayerScore> {
}
Service 层PlayerScoreService.java
import com.baomidou.mybatisplus.extension.service.IService;
public interface PlayerScoreService extends IService<PlayerScore> {
void updatePlayerScore(String userId, double score);
PlayerScore getPlayerScore(String userId);
}
Service 层(更新用户积分)PlayerScoreServiceImpl.java
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Service
public class PlayerScoreServiceImpl extends ServiceImpl<PlayerScoreMapper, PlayerScore> implements PlayerScoreService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 本地缓存(Guava Cache)
private final Cache<String, PlayerScore> localCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
@Override
public void updatePlayerScore(String userId, double score) {
// 1. 更新数据库
PlayerScore playerScore = this.getById(userId);
if (playerScore == null) {
playerScore = new PlayerScore();
playerScore.setUserId(userId);
playerScore.setScore(score);
this.save(playerScore);
} else {
playerScore.setScore(playerScore.getScore() + score);
this.updateById(playerScore);
}
// 2. 更新本地缓存
localCache.put(userId, playerScore);
// 3. 更新 Redis 缓存
redisTemplate.opsForZSet().add("global_leaderboard", userId, playerScore.getScore());
}
@Override
public PlayerScore getPlayerScore(String userId) {
// 1. 尝试从本地缓存获取
PlayerScore playerScore = localCache.getIfPresent(userId);
if (playerScore != null) {
return playerScore;
}
// 2. 尝试从 Redis 缓存获取
Double score = redisTemplate.opsForZSet().score("global_leaderboard", userId);
if (score != null) {
playerScore = new PlayerScore();
playerScore.setUserId(userId);
playerScore.setScore(score);
localCache.put(userId, playerScore); // 回填到本地缓存
return playerScore;
}
// 3. 最后从数据库获取
playerScore = this.getById(userId);
if (playerScore != null) {
localCache.put(userId, playerScore); // 回填到本地缓存
}
return playerScore;
}
}
Controller 层PlayerScoreController.java
(单实例)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Set;
@RestController
@RequestMapping("/leaderboard")
public class PlayerScoreController {
@Autowired
private PlayerScoreService playerScoreService;
@PostMapping("/update")
public String updateScore(@RequestParam String userId, @RequestParam double score) {
playerScoreService.updatePlayerScore(userId, score);
return "积分更新成功";
}
@GetMapping("/rank")
public PlayerScore getRank(@RequestParam String userId) {
return playerScoreService.getPlayerScore(userId);
}
@GetMapping("/top")
public Set<Object> getTop(@RequestParam int start, @RequestParam int end) {
return playerScoreService.getRedisTemplate().opsForZSet().reverseRange("global_leaderboard", start, end);
}
}
运行说明
- 启动 MySQL 并创建
player_score
表。 - 启动 Redis。
- 配置好
application.yml
文件中的 MySQL 和 Redis 地址。 - 启动 Spring Boot 项目。
- 使用 Postman 或其他工具测试:
- POST
/leaderboard/update?userId=user1&score=100
用于更新积分。 - GET
/leaderboard/rank?userId=user1
用于查询用户积分。 - GET
/leaderboard/top?start=0&end=9
用于查询排行榜前 10 名。
- POST
分布式实现(可选)
当玩家规模超过单个 Redis
实例的承载能力时,可对排行榜进行分片存储。
分片策略
- 将玩家的
ID
按哈希值分布到多个Redis
实例中。 - 每个实例维护一个
子排行榜
。 - 定期
合并分片
数据生成全服排行榜。
import java.util.*;
public class DistributedLeaderboard {
private final List<Jedis> redisShards = new ArrayList<>();
public DistributedLeaderboard() {
// 初始化多个 Redis 实例
redisShards.add(new Jedis("localhost", 6379)); // 第一个实例
redisShards.add(new Jedis("localhost", 6380)); // 第二个实例
}
// 根据玩家ID计算分片索引
private int getShardIndex(String userId) {
return Math.abs(userId.hashCode() % redisShards.size());
}
// 更新积分
public void updateScore(String userId, double score) {
int shardIndex = getShardIndex(userId);
Jedis jedis = redisShards.get(shardIndex);
jedis.zincrby("leaderboard_" + shardIndex, score, userId);
}
// 合并全服排行榜
public Map<String, Double> mergeLeaderboards() {
Map<String, Double> globalLeaderboard = new HashMap<>();
for (int i = 0; i < redisShards.size(); i++) {
Jedis jedis = redisShards.get(i);
Set<Tuple> shardData = jedis.zrevrangeWithScores("leaderboard_" + i, 0, -1);
for (Tuple entry : shardData) {
globalLeaderboard.merge(entry.getElement(), entry.getScore(), Double::max);
}
}
return globalLeaderboard;
}
}
Spring Boot 中配置 Redis 的分布式支持:RedisConfig.java
(分布式)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class RedisConfig {
@Bean
public List<Jedis> redisShards() {
List<Jedis> redisShards = new ArrayList<>();
redisShards.add(new Jedis("localhost", 6379)); // 第一个实例
redisShards.add(new Jedis("localhost", 6380)); // 第二个实例
return redisShards;
}
}
Service 层接口定义: DistributedLeaderboardService.java
(分布式)
import java.util.List;
import java.util.Map;
public interface DistributedLeaderboardService {
// 更新玩家积分
void updatePlayerScore(String userId, double score);
// 获取玩家积分和排名
Map<String, Object> getPlayerRank(String userId);
PlayerScore getPlayerScore(String userId);
// 获取全服排行榜(分页)
List<Map<String, Object>> getGlobalLeaderboard(int start, int end);
}
实现类: DistributedLeaderboardServiceImpl.java
(分布式)
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.*;
@Service
public class DistributedLeaderboardServiceImpl implements DistributedLeaderboardService {
@Autowired
private List<Jedis> redisShards;
// 根据玩家ID计算分片索引
private int getShardIndex(String userId) {
return Math.abs(userId.hashCode() % redisShards.size());
}
@Override
public void updatePlayerScore(String userId, double score) {
int shardIndex = getShardIndex(userId);
Jedis jedis = redisShards.get(shardIndex);
jedis.zincrby("leaderboard_" + shardIndex, score, userId);
}
@Override
public Map<String, Object> getPlayerRank(String userId) {
Map<String, Object> result = new HashMap<>();
int shardIndex = getShardIndex(userId);
Jedis jedis = redisShards.get(shardIndex);
// 获取玩家当前积分
Double score = jedis.zscore("leaderboard_" + shardIndex, userId);
if (score == null) {
result.put("userId", userId);
result.put("rank", -1);
result.put("score", 0);
return result;
}
// 获取玩家排名
Long rank = jedis.zrevrank("leaderboard_" + shardIndex, userId);
result.put("userId", userId);
result.put("rank", rank != null ? rank + 1 : -1); // Redis 排名从 0 开始
result.put("score", score);
return result;
}
@Override
public List<Map<String, Object>> getGlobalLeaderboard(int start, int end) {
Map<String, Double> globalLeaderboard = new HashMap<>();
// 从每个分片中获取子排行榜
for (int i = 0; i < redisShards.size(); i++) {
Jedis jedis = redisShards.get(i);
Set<Tuple> shardData = jedis.zrevrangeWithScores("leaderboard_" + i, 0, -1);
// 合并排行榜数据
for (Tuple entry : shardData) {
globalLeaderboard.merge(entry.getElement(), entry.getScore(), Double::max);
}
}
// 按积分排序
List<Map.Entry<String, Double>> sortedList = new ArrayList<>(globalLeaderboard.entrySet());
sortedList.sort((a, b) -> Double.compare(b.getValue(), a.getValue()));
// 取分页
List<Map<String, Object>> result = new ArrayList<>();
int total = sortedList.size();
for (int i = Math.max(0, start); i <= Math.min(end, total - 1); i++) {
Map<String, Object> item = new HashMap<>();
item.put("userId", sortedList.get(i).getKey());
item.put("score", sortedList.get(i).getValue());
item.put("rank", i + 1);
result.add(item);
}
return result;
}
}
Controller 实现: DistributedLeaderboardController.java
(分布式)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/leaderboard")
public class DistributedLeaderboardController {
@Autowired
private DistributedLeaderboardService leaderboardService;
// 更新玩家积分
@PostMapping("/update")
public String updateScore(@RequestParam String userId, @RequestParam double score) {
leaderboardService.updatePlayerScore(userId, score);
return "积分更新成功";
}
// 查询玩家排名和积分
@GetMapping("/rank")
public Map<String, Object> getRank(@RequestParam String userId) {
return leaderboardService.getPlayerRank(userId);
}
// 获取全服排行榜
@GetMapping("/top")
public List<Map<String, Object>> getTop(@RequestParam int start, @RequestParam int end) {
return leaderboardService.getGlobalLeaderboard(start, end);
}
}
测试用例(Postman 或其他工具)
-
更新玩家积分
- 请求方式:
POST
- 接口地址:
http://localhost:8080/leaderboard/update
- 请求参数:
userId=user1&score=100
- 返回结果:
积分更新成功
- 请求方式:
-
查询玩家排名和积分
- 请求方式:
GET
- 接口地址:
http://localhost:8080/leaderboard/rank
- 请求参数:
userId=user1
- 返回结果(示例 JSON):
{ "userId": "user1", "rank": 1, "score": 100.0 }
- 请求方式:
-
获取全服排行榜
- 请求方式:
GET
- 接口地址:
http://localhost:8080/leaderboard/top
- 请求参数:
start=0&end=9
- 返回结果(示例 JSON):
[ { "userId": "user1", "score": 100.0, "rank": 1 }, { "userId": "user2", "score": 90.0, "rank": 2 }, { "userId": "user3", "score": 80.0, "rank": 3 } ]
- 请求方式:
前端展示分页
排行榜前端可以分页展示,后端支持传递分页参数:
Set<Tuple> page = leaderboard.getLeaderboard((pageNumber - 1) * pageSize, pageNumber * pageSize - 1);
实际应用中的优化策略
-
缓存优化:
- 使用
Guava Cache
或Redis
缓存Top 100
,减少Redis
查询次数。
- 使用
-
持久化支持:
- 定期将
Redis
中的积分同步到MySQL
数据库。
- 定期将
-
异步处理:
- 玩家积分更新时,利用消息队列(如
Kafka
)异步写Redis
,提升写入效率。
- 玩家积分更新时,利用消息队列(如
通过以上代码和架构设计,可以有效支持上亿玩家的积分实时排行榜,并满足高并发需求。
六、总结
利用 Redis 的有序集合功能,可以高效实现积分的实时排行。通过分区存储、异步计算、缓存优化等技术,可以在上亿用户的规模下满足实时性和高并发的需求。