Redis用ZSet实现实时积分排行榜

背景

需求:实时积分排行榜
要求实时性,有序性

为什么要用Redis的ZSet

Redis的ZSet就像一个巨型的的电子记分牌,上面会有所有用户的积分和排名情况
ZSet是由Redis提供的一种数据结构。他存储一组唯一的成员,每个成员关联一个分数。成员按分数自动排序。

  • 极致的实时性
  • 天然排序
  • 简单易用
  • 高性能、高并发

而使用数据库(如MySQL),一是实时性低,二是复杂度高,需要写sql,最后就是高并发能力差。

核心操作

  • ZADD key score member : 添加或更新成员及其分数
  • ZINCRBY key increment member : 给指定成员分数增加(可正可负)分数。(最常用)
  • ZREVRANGE key start stop : 获取分数从高到底(REV是reverse)排名在[start,stop]区间内的成员
  • ZREVRANK key member : 获取指定成员在从高到低排名中的名次(0表示第一名)
  • ZSCORE key member : 获取指定成员的分数
  • ZCARD key : 获取ZSet的成员数量

具体实现

  1. 配置RedisTemplate
@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

// 使用String序列化器序列化Key (重要!)
template.setKeySerializer(new StringRedisSerializer());

// 使用GenericJackson2JsonRedisSerializer序列化Value (包含ZSet的Value)
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

// 设置HashKey和HashValue的序列化器 (如果用到Hash结构)
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

// !!! 关键:为ZSet操作设置序列化器 !!!
template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer()); // 默认值序列化器
template.setEnableDefaultSerializer(true); // 确保启用默认序列化器

return template;
}
}
  1. 核心服务实现:
@Service
public class RankingService {

private final RedisTemplate<String, Object> redisTemplate;
private static final String RANKING_KEY = "game:leaderboard"; // 排行榜的Redis Key

public RankingService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}

// 获取操作ZSet的便捷工具
private ZSetOperations<String, Object> zSetOps() {
return redisTemplate.opsForZSet();
}

// 1. 添加或更新玩家积分 (核心操作)
public void addOrUpdateScore(String userId, double score) {
// 使用 incrementScore 是最佳实践!如果用户不存在则添加,存在则增加分数
zSetOps().incrementScore(RANKING_KEY, userId, score);
// 注意:也可以使用 add, 但 add 会覆盖原有分数,而不是累加!
// zSetOps().add(RANKING_KEY, userId, score); // 慎用!通常用于初始化或重置分数
}

// 2. 查询玩家当前积分
public Double getScore(String userId) {
return zSetOps().score(RANKING_KEY, userId);
}

// 3. 查询玩家排名 (从高到低, 0表示第一名)
public Long getRank(String userId) {
// ZRevRank: 在降序排列的集合中获取排名 (0-based)
return zSetOps().reverseRank(RANKING_KEY, userId);
}

// 4. 查询玩家排名 (带分数, 从高到低, 0表示第一名)
public RankData getRankWithScore(String userId) {
Long rank = zSetOps().reverseRank(RANKING_KEY, userId);
Double score = zSetOps().score(RANKING_KEY, userId);
if (rank == null || score == null) {
return null; // 用户不存在于排行榜
}
return new RankData(userId, rank + 1, score); // 通常排名习惯从1开始,所以+1
}

// 5. 获取排行榜片段 (Top N, 从高到低)
public Set<ZSetOperations.TypedTuple<Object>> getTopNRanks(int n) {
// ZRevRangeWithScores: 获取降序排列中 [0, n-1] 范围的成员及其分数
return zSetOps().reverseRangeWithScores(RANKING_KEY, 0, n - 1);
}

// 6. 获取排行榜片段 (指定名次范围, 从高到低)
public Set<ZSetOperations.TypedTuple<Object>> getRanks(long start, long end) {
// ZRevRangeWithScores: 获取降序排列中 [start, end] 范围的成员及其分数
return zSetOps().reverseRangeWithScores(RANKING_KEY, start, end);
}

// 7. (可选) 按分数范围查询玩家 (例如: 查询积分在1000到2000之间的玩家)
public Set<ZSetOperations.TypedTuple<Object>> getUsersByScoreRange(double minScore, double maxScore) {
return zSetOps().rangeByScoreWithScores(RANKING_KEY, minScore, maxScore);
}

// 辅助类,用于封装排名信息
@Data
@AllArgsConstructor
public static class RankData {
private String userId;
private Long rank; // 从1开始的名次
private Double score;
}
}

关于Jedis的拓展(老式)

是什么?

Jedis是最常用的Redis Java客户端之一

Redis服务器就相当于移动/联通的基站。这是一提供通信服务的核心设备,但它本身不能直接打电话
Java程序就相当于一个智能手机,想要使用基站的服务
Jedis就是手机里面的SIM卡和通信模块

简单来说,Jedis就是可以让Java程序说Redis语言,并与Redis服务器握手、会话的关键桥梁

具体使用

  • Jedis类:代表一个到Redis服务器的单个链接。
  • JedisPool类:管理多个Jedis实例,提供连接池功能。

关于RedisTemplate的拓展

RedisTemplate的使用