如何实现上亿玩家的实时积分排行版?Java + Redis + MySQL完整实现

944 阅读15分钟

实现“上亿用户的积分实时排行榜功能”是一个极具挑战性且需要高性能架构支持的任务。以下为您讲解其核心思路和关键技术:


一、关键目标和难点

  1. 实时性:用户积分变动后,排行榜需要迅速更新。
  2. 高并发:上亿用户同时操作,系统需要承受高并发请求。
  3. 排序效率:需要快速计算出排名,尤其是前几名和某个用户的排名。
  4. 存储优化:存储上亿用户的积分和排名数据,同时保证系统性能。

二、实现思路

2-1 使用高性能的排序数据结构

  • 为了实时计算用户的积分排名,可以利用 Redis 的有序集合(Sorted Set)

  • Redis 的有序集合是一种基于分数排序的数据结构,数据以键值对的形式存储,键为用户 ID 或用户名,值为用户的积分。其主要特性包括:

    • 分数(Score):一个浮点数,用于对集合内的成员排序。
    • 成员(Member):集合中的唯一标识符,通常是用户 ID。
    • 支持根据分数的范围排名范围进行查找排序操作。
    • 假设我们有如下用户积分数据:
    用户 ID用户名积分(Score)
    1Alice102
    2Bob98
    3Charlie120
    • 在 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
    
  1. 实现流程图
  • 以下是积分排行榜功能的流程:

image.png


  1. 使用 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实例中。
    • 每个分片维护一部分用户的积分排行榜
  • 全局排行榜可以通过 定期合并分片数据 的方式生成。
  1. 异步计算与缓存

    • 对于实时性要求较高的功能(如前 100 名排行榜),可以提前异步计算并缓存结果:
      • 每次积分更新时,触发后台任务更新前 N 名榜单。
      • 客户端直接从缓存中读取榜单数据,减轻实时查询的压力。
  2. 增量更新和冷热分离

    • 并非所有用户的积分会频繁变动。将活跃用户和不活跃用户分开处理:
      • 活跃用户积分变动时,实时更新Redis。
      • 不活跃用户可以定期批量更新排行榜,减少系统压力。
  3. 数据库持久化与备份

    • Redis 虽然性能高,但属于内存数据库,可能会因为重启或崩溃导致数据丢失。
    • 可以将积分数据定期同步到关系型数据库(如 MySQL)或分布式存储系统(如 HBase)中,作为持久化备份。
  4. 分片(Sharding)方案示例

  • 分片策略
      1. 将用户的积分数据按用户 ID 的哈希值分配到不同的 Redis 实例中。
      1. 每个 Redis 实例维护一个子排行榜(分片排行榜)。
      1. 定期将各分片排行榜的数据合并,生成全局排行榜。
      1. 分片示例
      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));
          }
      }
      

三、技术架构示例

以下是一个典型实现架构:

  1. 用户操作层(客户端)

    • 用户通过手机或网页发起积分相关操作(如完成任务、消费积分等)。
    • 调用后端接口更新积分。
  2. 后端服务层

    • 接收用户的积分更新请求,将积分变动写入 Redis 的有序集合。
    • 提供查询接口返回排行榜前 N 名或某用户的排名。
  3. 缓存和分布式存储

    • 使用 Redis 存储实时排行榜数据。
    • 对于大规模用户分片存储数据,提升性能。
  4. 数据持久化

    • 定期将 Redis 中的积分数据同步到数据库,确保数据安全。
  5. 异步任务系统

    • 利用消息队列(如 Kafka 或 RabbitMQ)处理积分更新的异步任务。
    • 定期计算全局排行榜或分片合并。

四、常见优化策略

  1. 热点优化

    • 对于频繁查询的热门数据(如 Top 10 榜单),通过缓存机制减少重复计算。
  2. 限流和降级

    • 当并发量过高时,可以限制排行榜的实时更新频率(如每分钟更新一次)。
  3. 监控和报警

    • 部署监控工具(如 Prometheus),实时监控系统的性能和异常。
  4. 优化策略的代码实践

  • 缓存热门排行榜

    • 为了减少访问 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 查询
        }
    }
    

五、场景应用

模拟真实应用场景,结合 JavaRedis 编写代码,实现积分实时排行榜功能。


应用场景背景描述:

假设我们正在开发一个手机游戏——《冒险之旅》。玩家完成任务、击败怪物、参加活动等行为都会获得积分,积分用于衡量玩家的游戏成就。为了增加竞争性,系统需要展示:

  1. 全服排行榜:展示所有玩家的实时积分排名。
  2. 好友排行榜:展示玩家所在好友圈的排名。
  3. 玩家排名查询:查询某个玩家的当前排名与积分。

功能描述:

  1. 玩家完成任务后,积分更新到数据库,同时更新本地缓存和 Redis 缓存。
  2. 支持查询某玩家的积分排名总积分
  3. 支持分页查询排行榜前 N 名
  4. 数据存储在 MySQL,通过 Redis 提供实时缓存,使用 Guava Cache 提供热点数据的本地缓存。

功能需求:

  • 实时更新积分:玩家完成任务后,积分应立即更新,并反映在排行榜中。
  • 高效查询排名:能够快速查询玩家的排名。
  • 分页显示排行榜:支持获取排行榜的任意分页数据(如前 10 名前 50 名)。
  • 分布式支持:支持上亿玩家参与,系统需具备高并发处理能力。

技术栈

  • Spring Boot 2.7+
  • Redis (Jedis)
  • MySQL (持久化存储)
  • MyBatis-Plus (ORM 框架)
  • Guava Cache (本地缓存)
  • Maven (项目管理)

技术设计思路

  1. 使用 Redis 有序集合(Sorted Set) 存储玩家积分排行榜。
    • Key: global_leaderboard(全服排行榜)
    • Member: 玩家ID(userId)
    • Score: 玩家积分(score)
  2. 分布式支持(可选): 通过分片(sharding)方案,将玩家按 ID 分布到不同 Redis 实例中。
  3. 缓存优化: 热门排行榜(如 Top 100)定期缓存,减少实时查询 Redis 的压力。
  4. 数据持久化: 定期将排行榜数据同步到 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 数据结构
KeyMemberScore
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);
    }
}

运行说明
  1. 启动 MySQL 并创建 player_score 表。
  2. 启动 Redis。
  3. 配置好 application.yml 文件中的 MySQL 和 Redis 地址。
  4. 启动 Spring Boot 项目。
  5. 使用 Postman 或其他工具测试:
    • POST /leaderboard/update?userId=user1&score=100 用于更新积分。
    • GET /leaderboard/rank?userId=user1 用于查询用户积分。
    • GET /leaderboard/top?start=0&end=9 用于查询排行榜前 10 名。

分布式实现(可选)

当玩家规模超过单个 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 或其他工具)

  1. 更新玩家积分

    • 请求方式:POST
    • 接口地址:http://localhost:8080/leaderboard/update
    • 请求参数:userId=user1&score=100
    • 返回结果:积分更新成功
  2. 查询玩家排名和积分

    • 请求方式:GET
    • 接口地址:http://localhost:8080/leaderboard/rank
    • 请求参数:userId=user1
    • 返回结果(示例 JSON):
      {
          "userId": "user1",
          "rank": 1,
          "score": 100.0
      }
      
  3. 获取全服排行榜

    • 请求方式: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);

实际应用中的优化策略

  1. 缓存优化

    • 使用 Guava CacheRedis 缓存 Top 100,减少 Redis 查询次数。
  2. 持久化支持

    • 定期将 Redis 中的积分同步到 MySQL 数据库。
  3. 异步处理

    • 玩家积分更新时,利用消息队列(如 Kafka)异步写 Redis,提升写入效率。

通过以上代码和架构设计,可以有效支持上亿玩家的积分实时排行榜,并满足高并发需求。


六、总结

利用 Redis 的有序集合功能,可以高效实现积分的实时排行。通过分区存储、异步计算、缓存优化等技术,可以在上亿用户的规模下满足实时性和高并发的需求。