使用 Spring Boot 和 Redisson 实现 Redis Sorted Set (ZSET) 排行榜
在构建高性能、实时更新的排行榜(如游戏积分榜、用户活跃度排名等)时,Redis 的 Sorted Set(ZSET)数据结构是一个理想的选择。结合 Redisson 作为 Redis 客户端,并在 Spring Boot 环境中集成,可以高效地实现这一功能。本文将详细介绍如何在 Spring Boot 项目中使用 Redisson 操作 Redis ZSET 来实现排行榜。
Redis Sorted Set (ZSET) 简介
Sorted Set(有序集合) 是 Redis 提供的一种数据结构,它结合了集合的唯一性和按分数排序的特性。每个成员(member)在 ZSET 中都是唯一的,并且每个成员都关联一个分数(score),Redis 会根据分数自动为成员排序。
ZSET 的主要操作包括:
- 添加/更新成员及其分数:
ZADD key score member
- 获取成员的排名:
ZRANK key member
(从低到高排序),ZREVRANK key member
(从高到低排序) - 获取指定范围的成员:
ZRANGE key start stop [WITHSCORES]
- 获取排行榜前 N 名:
ZREVRANGE key 0 N-1 WITHSCORES
使用场景:
- 游戏积分排行榜
- 用户活跃度排名
- 商品评分排名
- 实时热点分析等
Redisson 简介
Redisson 是一个功能丰富的 Redis 客户端,为 Java 提供了各种分布式数据结构和服务。它不仅封装了 Redis 的基础命令,还提供了高级功能,如分布式锁、布隆过滤器、消息队列等。对于操作 Redis 的 Sorted Set,Redisson 提供了便捷的 API,使得开发者可以更高效地实现排行榜等功能。
Redisson 的主要特点:
- 简洁的 API:易于使用,降低开发复杂度。
- 高性能:基于 Netty 的高性能网络通信框架。
- 丰富的功能:支持多种 Redis 数据结构和分布式服务。
- Spring Boot 集成:方便与 Spring Boot 项目无缝集成。
项目环境搭建
1. 创建 Spring Boot 项目
使用 Spring Initializr 创建一个新的 Spring Boot 项目,选择以下依赖:
- Spring Web
- Spring Data Redis
2. 添加 Redisson 依赖
在 pom.xml
中添加 Redisson 的依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.6</version> <!-- 请根据最新版本进行调整 -->
</dependency>
注意:请确保使用与 Redis 服务器版本兼容的 Redisson 版本。
配置 Redisson
在 application.yml
或 application.properties
中配置 Redis 连接信息。以下是使用 application.yml
的示例:
spring:
redis:
host: localhost
port: 6379
password: yourpassword # 如果有密码的话
database: 0
Redisson 会自动读取 Spring Boot 的 Redis 配置,无需额外配置。如果需要更复杂的配置(如集群模式),可以通过自定义 Redisson 配置来实现。
排行榜功能实现
假设我们要实现一个简单的用户积分排行榜,功能包括:
- 添加或更新用户分数
- 获取用户排名
- 获取排行榜前 N 名
- 获取指定范围内的用户
1. 添加或更新用户分数
使用 ZADD
命令将用户及其分数添加到 Sorted Set 中。如果用户已经存在,则会更新其分数。
import org.redisson.api.RScoredSortedSet;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LeaderboardService {
private static final String LEADERBOARD_KEY = "game_leaderboard";
@Autowired
private RedissonClient redissonClient;
/**
* 添加或更新用户分数
*
* @param userId 用户ID
* @param score 用户分数
*/
public void addOrUpdateUserScore(String userId, double score) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
sortedSet.add(score, userId);
}
}
2. 获取用户排名
获取用户在排行榜中的排名,注意 Redis 的排名是从 0 开始的。
/**
* 获取用户排名(从高到低)
*
* @param userId 用户ID
* @return 用户排名(1 表示第一名),如果用户不存在则返回 null
*/
public Long getUserRank(String userId) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
Long rank = sortedSet.revRank(userId);
return rank != null ? rank + 1 : null; // 转换为从1开始的排名
}
3. 获取排行榜前 N 名
获取排行榜中分数最高的前 N 名用户。
import org.redisson.api.ScoredEntry;
import java.util.Set;
/**
* 获取排行榜前 N 名
*
* @param topN 前N名
* @return 前N名用户及其分数
*/
public Set<ScoredEntry<String>> getTopN(int topN) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
return sortedSet.entryRangeReversed(0, topN - 1);
}
4. 获取指定范围内的用户
例如,获取第 10 到第 20 名的用户。
/**
* 获取指定范围内的用户
*
* @param start 起始排名(从1开始)
* @param end 结束排名(从1开始)
* @return 指定范围内的用户及其分数
*/
public Set<ScoredEntry<String>> getUsersInRange(int start, int end) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
return sortedSet.entryRangeReversed(start - 1, end - 1);
}
完整示例代码
以下是一个完整的 Spring Boot 示例,展示如何使用 Redisson 操作 Redis ZSET 来实现排行榜功能。
1. LeaderboardService.java
package com.example.leaderboard.service;
import org.redisson.api.RScoredSortedSet;
import org.redisson.api.ScoredEntry;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class LeaderboardService {
private static final String LEADERBOARD_KEY = "game_leaderboard";
@Autowired
private RedissonClient redissonClient;
/**
* 添加或更新用户分数
*
* @param userId 用户ID
* @param score 用户分数
*/
public void addOrUpdateUserScore(String userId, double score) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
sortedSet.add(score, userId);
}
/**
* 获取用户排名(从高到低)
*
* @param userId 用户ID
* @return 用户排名(1 表示第一名),如果用户不存在则返回 null
*/
public Long getUserRank(String userId) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
Long rank = sortedSet.revRank(userId);
return rank != null ? rank + 1 : null; // 转换为从1开始的排名
}
/**
* 获取排行榜前 N 名
*
* @param topN 前N名
* @return 前N名用户及其分数
*/
public Set<ScoredEntry<String>> getTopN(int topN) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
return sortedSet.entryRangeReversed(0, topN - 1);
}
/**
* 获取指定范围内的用户
*
* @param start 起始排名(从1开始)
* @param end 结束排名(从1开始)
* @return 指定范围内的用户及其分数
*/
public Set<ScoredEntry<String>> getUsersInRange(int start, int end) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
return sortedSet.entryRangeReversed(start - 1, end - 1);
}
}
2. LeaderboardController.java
package com.example.leaderboard.controller;
import com.example.leaderboard.service.LeaderboardService;
import org.redisson.api.ScoredEntry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Set;
@RestController
@RequestMapping("/leaderboard")
public class LeaderboardController {
@Autowired
private LeaderboardService leaderboardService;
/**
* 添加或更新用户分数
*/
@PostMapping("/update")
public String updateUserScore(@RequestParam String userId, @RequestParam double score) {
leaderboardService.addOrUpdateUserScore(userId, score);
return "用户分数已更新";
}
/**
* 获取用户排名
*/
@GetMapping("/rank/{userId}")
public String getUserRank(@PathVariable String userId) {
Long rank = leaderboardService.getUserRank(userId);
if (rank != null) {
return "用户 " + userId + " 的排名是第 " + rank + " 名";
} else {
return "用户未上榜";
}
}
/**
* 获取排行榜前 N 名
*/
@GetMapping("/top/{n}")
public String getTopN(@PathVariable int n) {
Set<ScoredEntry<String>> topN = leaderboardService.getTopN(n);
StringBuilder sb = new StringBuilder();
int rank = 1;
for (ScoredEntry<String> entry : topN) {
sb.append(rank++)
.append(". 用户ID: ")
.append(entry.getValue())
.append(", 分数: ")
.append(entry.getScore())
.append("<br>");
}
return sb.toString();
}
/**
* 获取指定范围内的用户
*/
@GetMapping("/range")
public String getUsersInRange(@RequestParam int start, @RequestParam int end) {
Set<ScoredEntry<String>> range = leaderboardService.getUsersInRange(start, end);
StringBuilder sb = new StringBuilder();
int rank = start;
for (ScoredEntry<String> entry : range) {
sb.append(rank++)
.append(". 用户ID: ")
.append(entry.getValue())
.append(", 分数: ")
.append(entry.getScore())
.append("<br>");
}
return sb.toString();
}
}
3. RedissonConfig.java
(可选)
如果需要自定义 Redisson 配置,如连接池、集群模式等,可以创建一个配置类:
package com.example.leaderboard.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
// 单节点模式
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setPassword("yourpassword"); // 如果有密码的话
// 集群模式示例
/*
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
.setPassword("yourpassword");
*/
return Redisson.create(config);
}
}
注意:如果使用
redisson-spring-boot-starter
,Redisson 会自动配置,通常无需手动创建RedissonClient
Bean。上述配置类仅供需要自定义配置时参考。
最佳实践与注意事项
- 排行榜 Key 的设计:使用有意义的 Key 名称,如
game_leaderboard
,避免与其他业务数据混淆。 - 分数的数据类型:Redis 的 ZSET 分数是
double
类型,可以精确到小数点后多位,确保分数的精确性。 - 分布式环境下的 Redisson:
- 确保 Redisson 的配置与 Redis 集群或主从架构相匹配。
- 使用连接池和合理的超时设置,提升性能和稳定性。
- 性能优化:
- 对于高并发的更新操作,Redisson 已经做了优化,但仍需根据实际负载调整 Redis 服务器的配置。
- 使用管道(Pipeline)或批量操作来减少网络延迟。
- 异常处理:在生产环境中,确保对 Redis 连接异常、操作失败等情况进行妥善处理,避免影响整个应用的稳定性。
- 数据持久化:配置 Redis 的持久化策略(RDB 或 AOF),确保数据在 Redis 重启或故障后能够恢复。
- 安全性:
- 设置强密码,防止未授权访问。
- 使用防火墙或其他安全措施,限制 Redis 的访问范围。
完整示例代码
以下是一个完整的 Spring Boot 项目示例,包含了上述所有内容。
1. pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>leaderboard</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>leaderboard</name>
<description>Redis ZSET Leaderboard Example</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>11</java.version>
<redisson.version>3.23.6</redisson.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson Spring Boot Starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- Lombok (可选,用于简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test (可选,用于测试) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. application.yml
spring:
redis:
host: localhost
port: 6379
password: yourpassword # 如果有密码的话
database: 0
3. RedissonConfig.java
(可选)
package com.example.leaderboard.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
// 单节点模式
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setPassword("yourpassword"); // 如果有密码的话
// 集群模式示例
/*
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
.setPassword("yourpassword");
*/
return Redisson.create(config);
}
}
4. LeaderboardService.java
package com.example.leaderboard.service;
import org.redisson.api.RScoredSortedSet;
import org.redisson.api.ScoredEntry;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class LeaderboardService {
private static final String LEADERBOARD_KEY = "game_leaderboard";
@Autowired
private RedissonClient redissonClient;
/**
* 添加或更新用户分数
*
* @param userId 用户ID
* @param score 用户分数
*/
public void addOrUpdateUserScore(String userId, double score) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
sortedSet.add(score, userId);
}
/**
* 获取用户排名(从高到低)
*
* @param userId 用户ID
* @return 用户排名(1 表示第一名),如果用户不存在则返回 null
*/
public Long getUserRank(String userId) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
Long rank = sortedSet.revRank(userId);
return rank != null ? rank + 1 : null; // 转换为从1开始的排名
}
/**
* 获取排行榜前 N 名
*
* @param topN 前N名
* @return 前N名用户及其分数
*/
public Set<ScoredEntry<String>> getTopN(int topN) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
return sortedSet.entryRangeReversed(0, topN - 1);
}
/**
* 获取指定范围内的用户
*
* @param start 起始排名(从1开始)
* @param end 结束排名(从1开始)
* @return 指定范围内的用户及其分数
*/
public Set<ScoredEntry<String>> getUsersInRange(int start, int end) {
RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet(LEADERBOARD_KEY);
return sortedSet.entryRangeReversed(start - 1, end - 1);
}
}
5. LeaderboardController.java
package com.example.leaderboard.controller;
import com.example.leaderboard.service.LeaderboardService;
import org.redisson.api.ScoredEntry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Set;
@RestController
@RequestMapping("/leaderboard")
public class LeaderboardController {
@Autowired
private LeaderboardService leaderboardService;
/**
* 添加或更新用户分数
*/
@PostMapping("/update")
public String updateUserScore(@RequestParam String userId, @RequestParam double score) {
leaderboardService.addOrUpdateUserScore(userId, score);
return "用户分数已更新";
}
/**
* 获取用户排名
*/
@GetMapping("/rank/{userId}")
public String getUserRank(@PathVariable String userId) {
Long rank = leaderboardService.getUserRank(userId);
if (rank != null) {
return "用户 " + userId + " 的排名是第 " + rank + " 名";
} else {
return "用户未上榜";
}
}
/**
* 获取排行榜前 N 名
*/
@GetMapping("/top/{n}")
public String getTopN(@PathVariable int n) {
Set<ScoredEntry<String>> topN = leaderboardService.getTopN(n);
StringBuilder sb = new StringBuilder();
int rank = 1;
for (ScoredEntry<String> entry : topN) {
sb.append(rank++)
.append(". 用户ID: ")
.append(entry.getValue())
.append(", 分数: ")
.append(entry.getScore())
.append("<br>");
}
return sb.toString();
}
/**
* 获取指定范围内的用户
*/
@GetMapping("/range")
public String getUsersInRange(@RequestParam int start, @RequestParam int end) {
Set<ScoredEntry<String>> range = leaderboardService.getUsersInRange(start, end);
StringBuilder sb = new StringBuilder();
int rank = start;
for (ScoredEntry<String> entry : range) {
sb.append(rank++)
.append(". 用户ID: ")
.append(entry.getValue())
.append(", 分数: ")
.append(entry.getScore())
.append("<br>");
}
return sb.toString();
}
}
6. LeaderboardApplication.java
package com.example.leaderboard;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LeaderboardApplication {
public static void main(String[] args) {
SpringApplication.run(LeaderboardApplication.class, args);
}
}
测试接口
启动 Spring Boot 应用后,可以通过以下方式测试排行榜功能:
1. 添加或更新用户分数
请求:
POST /leaderboard/update?userId=user1&score=1500
响应:
用户分数已更新
2. 获取用户排名
请求:
GET /leaderboard/rank/user1
响应:
用户 user1 的排名是第 1 名
3. 获取排行榜前 N 名
请求:
GET /leaderboard/top/10
响应:
1. 用户ID: user1, 分数: 1500.0<br>
2. 用户ID: user2, 分数: 1200.0<br>
...
4. 获取指定范围内的用户
请求:
GET /leaderboard/range?start=5&end=15
响应:
5. 用户ID: user5, 分数: 900.0<br>
6. 用户ID: user6, 分数: 850.0<br>
...
15. 用户ID: user15, 分数: 500.0<br>
最佳实践与注意事项
- 分数更新策略:
- 确保分数的更新是幂等的,避免重复添加导致排名混乱。
- 根据业务需求选择合适的分数类型(如积分、时间戳等)。
- 数据持久化:
- 配置 Redis 的持久化机制(RDB 或 AOF),确保排行榜数据在 Redis 重启后不丢失。
- 定期备份 Redis 数据,防止意外数据丢失。
- 性能优化:
- 对于高并发的更新和查询操作,确保 Redis 服务器性能足够。
- 使用集群模式扩展 Redis,提升吞吐量和可用性。
- 利用 Redisson 的连接池和线程池配置,优化网络资源使用。
- 安全性:
- 设置强密码,防止未授权访问 Redis。
- 使用防火墙或安全组限制 Redis 的访问范围,仅允许应用服务器访问。
- 错误处理:
- 在服务中加入 Redis 操作的异常处理机制,保证在 Redis 连接失败或操作异常时,能够优雅降级或重试。
- 监控与报警:
- 监控 Redis 的性能指标(如内存使用、命中率、延迟等)。
- 配置 Redis 相关的监控工具(如 Redis Exporter 配合 Prometheus),及时发现和处理问题。
总结
通过结合 Spring Boot 和 Redisson,可以高效地利用 Redis 的 Sorted Set(ZSET)数据结构,实现实时更新、快速查询的排行榜功能。Redisson 提供了简洁且功能强大的 API,降低了开发复杂度,同时保证了操作的高性能和可靠性。本文介绍了从项目搭建、配置 Redisson 到具体实现排行榜功能的各个步骤,希望能够帮助你快速上手并应用于实际项目中。
在实际生产环境中,根据具体业务需求,还可以进一步扩展排行榜功能,如支持多维度排序、分区域排行榜、缓存优化等。结合 Redis 的高性能和 Redisson 的丰富功能,能够满足大多数实时排行榜的需求。