Redis有序集合(ZSet):排行榜功能的最优解,原理与实战
1. Redis有序集合概述
1.1 什么是Redis有序集合
Redis有序集合(Sorted Set,简称ZSet)是Redis中最复杂也是最强大的数据结构之一。它结合了Set的唯一性 和按分数排序的特性,每个成员都关联一个分数(score),Redis根据分数对成员进行排序。
1.2 有序集合的特点
特性 | 描述 | 优势 |
---|---|---|
有序性 | 按分数自动排序 | 天然的排序功能 |
唯一性 | 成员不重复 | 自动去重 |
双索引 | 支持按成员和分数查询 | 查询灵活高效 |
范围操作 | 支持分数和排名范围查询 | 适合排行榜场景 |
2. 底层实现原理
2.1 编码方式
编码方式 | 使用条件 | 特点 |
---|---|---|
ziplist | 元素数≤128且成员长度≤64字节 | 内存紧凑 |
skiplist+hashtable | 其他情况 | 查询高效 |
2.2 跳表(Skip List)原理
跳表是ZSet的核心数据结构:
- 多层链表结构,支持O(log N)查询
- 每层都是有序链表
- 通过概率性算法维护层次结构
3. 基本操作
3.1 添加和更新
bash
# 添加成员
127.0.0.1:6379> ZADD leaderboard 100 "player1" 200 "player2" 150 "player3"
(integer) 3
# 更新分数
127.0.0.1:6379> ZADD leaderboard 250 "player1"
(integer) 0 # 成员已存在,返回0
3.2 查询操作
bash
# 按排名查询(分数从小到大)
127.0.0.1:6379> ZRANGE leaderboard 0 -1 WITHSCORES
1) "player3"
2) "150"
3) "player2"
4) "200"
5) "player1"
6) "250"
# 按排名查询(分数从大到小)
127.0.0.1:6379> ZREVRANGE leaderboard 0 2 WITHSCORES
1) "player1"
2) "250"
3) "player2"
4) "200"
5) "player3"
6) "150"
# 按分数范围查询
127.0.0.1:6379> ZRANGEBYSCORE leaderboard 150 200
1) "player3"
2) "player2"
# 获取成员分数
127.0.0.1:6379> ZSCORE leaderboard "player1"
"250"
# 获取成员排名
127.0.0.1:6379> ZRANK leaderboard "player1"
(integer) 2
127.0.0.1:6379> ZREVRANK leaderboard "player1"
(integer) 0 # 从大到小排名
# 获取集合大小
127.0.0.1:6379> ZCARD leaderboard
(integer) 3
4. 高级操作
4.1 分数操作
bash
# 增加分数
127.0.0.1:6379> ZINCRBY leaderboard 50 "player2"
"250"
# 按分数范围计数
127.0.0.1:6379> ZCOUNT leaderboard 200 300
(integer) 2
4.2 删除操作
bash
# 删除成员
127.0.0.1:6379> ZREM leaderboard "player3"
(integer) 1
# 按排名删除
127.0.0.1:6379> ZREMRANGEBYRANK leaderboard 0 0
(integer) 1
# 按分数删除
127.0.0.1:6379> ZREMRANGEBYSCORE leaderboard 200 250
(integer) 1
5. 实战应用场景
5.1 游戏排行榜系统
java
@Service
public class GameLeaderboardService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LEADERBOARD_KEY = "game:leaderboard:global";
/**
* 更新玩家分数
*/
public void updatePlayerScore(String playerId, long score) {
redisTemplate.opsForZSet().add(LEADERBOARD_KEY, playerId, score);
}
/**
* 增加玩家分数
*/
public Double addPlayerScore(String playerId, long deltaScore) {
return redisTemplate.opsForZSet().incrementScore(LEADERBOARD_KEY, playerId, deltaScore);
}
/**
* 获取排行榜TOP N
*/
public List<PlayerRank> getTopPlayers(int topN) {
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(LEADERBOARD_KEY, 0, topN - 1);
List<PlayerRank> rankings = new ArrayList<>();
int rank = 1;
for (ZSetOperations.TypedTuple<String> tuple : tuples) {
PlayerRank playerRank = new PlayerRank();
playerRank.setRank(rank++);
playerRank.setPlayerId(tuple.getValue());
playerRank.setScore(tuple.getScore().longValue());
rankings.add(playerRank);
}
return rankings;
}
/**
* 获取玩家排名和分数
*/
public PlayerRank getPlayerRank(String playerId) {
Double score = redisTemplate.opsForZSet().score(LEADERBOARD_KEY, playerId);
if (score == null) {
return null;
}
Long rank = redisTemplate.opsForZSet().reverseRank(LEADERBOARD_KEY, playerId);
PlayerRank playerRank = new PlayerRank();
playerRank.setPlayerId(playerId);
playerRank.setScore(score.longValue());
playerRank.setRank(rank != null ? rank.intValue() + 1 : -1);
return playerRank;
}
/**
* 获取玩家周围排名
*/
public List<PlayerRank> getPlayersAround(String playerId, int range) {
Long playerRank = redisTemplate.opsForZSet().reverseRank(LEADERBOARD_KEY, playerId);
if (playerRank == null) {
return new ArrayList<>();
}
long start = Math.max(0, playerRank - range);
long end = playerRank + range;
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(LEADERBOARD_KEY, start, end);
List<PlayerRank> rankings = new ArrayList<>();
int rank = (int) start + 1;
for (ZSetOperations.TypedTuple<String> tuple : tuples) {
PlayerRank pr = new PlayerRank();
pr.setRank(rank++);
pr.setPlayerId(tuple.getValue());
pr.setScore(tuple.getScore().longValue());
rankings.add(pr);
}
return rankings;
}
}
@Data
class PlayerRank {
private String playerId;
private Integer rank;
private Long score;
}
5.2 热门内容排行
java
@Service
public class HotContentService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 更新内容热度分数
*/
public void updateContentScore(String contentId, String category) {
String key = "hot:content:" + category;
// 计算热度分数(浏览量 + 点赞数 + 评论数 + 时间衰减)
double score = calculateHotScore(contentId);
redisTemplate.opsForZSet().add(key, contentId, score);
redisTemplate.expire(key, 1, TimeUnit.DAYS);
}
/**
* 获取热门内容
*/
public List<String> getHotContents(String category, int count) {
String key = "hot:content:" + category;
Set<String> contentIds = redisTemplate.opsForZSet()
.reverseRange(key, 0, count - 1);
return new ArrayList<>(contentIds);
}
/**
* 计算热度分数
*/
private double calculateHotScore(String contentId) {
// 简化的热度计算公式
long views = getViews(contentId);
long likes = getLikes(contentId);
long comments = getComments(contentId);
long timeDecay = getTimeDecay(contentId);
return (views * 1.0 + likes * 2.0 + comments * 3.0) * timeDecay;
}
private long getViews(String contentId) { return 100; }
private long getLikes(String contentId) { return 50; }
private long getComments(String contentId) { return 20; }
private long getTimeDecay(String contentId) { return 1; }
}
5.3 延时队列实现
java
@Service
public class DelayedQueueService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String DELAY_QUEUE_KEY = "delay:queue";
/**
* 添加延时任务
*/
public void addDelayedTask(String taskId, String taskData, long delaySeconds) {
long executeTime = System.currentTimeMillis() + delaySeconds * 1000;
String taskInfo = taskId + ":" + taskData;
redisTemplate.opsForZSet().add(DELAY_QUEUE_KEY, taskInfo, executeTime);
}
/**
* 获取到期任务
*/
public List<String> getExpiredTasks(int batchSize) {
long currentTime = System.currentTimeMillis();
Set<String> expiredTasks = redisTemplate.opsForZSet()
.rangeByScore(DELAY_QUEUE_KEY, 0, currentTime, 0, batchSize);
if (!expiredTasks.isEmpty()) {
// 删除已获取的任务
redisTemplate.opsForZSet().removeRangeByScore(DELAY_QUEUE_KEY, 0, currentTime);
}
return new ArrayList<>(expiredTasks);
}
/**
* 取消延时任务
*/
public boolean cancelDelayedTask(String taskId) {
// 需要根据taskId模糊匹配删除
Set<String> allTasks = redisTemplate.opsForZSet().range(DELAY_QUEUE_KEY, 0, -1);
for (String task : allTasks) {
if (task.startsWith(taskId + ":")) {
return redisTemplate.opsForZSet().remove(DELAY_QUEUE_KEY, task) > 0;
}
}
return false;
}
/**
* 获取队列统计信息
*/
public Map<String, Object> getQueueStats() {
Map<String, Object> stats = new HashMap<>();
Long totalTasks = redisTemplate.opsForZSet().zCard(DELAY_QUEUE_KEY);
Long expiredTasks = redisTemplate.opsForZSet()
.count(DELAY_QUEUE_KEY, 0, System.currentTimeMillis());
stats.put("total_tasks", totalTasks);
stats.put("expired_tasks", expiredTasks);
stats.put("pending_tasks", totalTasks - expiredTasks);
return stats;
}
}
5.4 用户积分系统
java
@Service
public class UserPointsService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String POINTS_LEADERBOARD = "user:points:leaderboard";
private static final String MONTHLY_POINTS = "user:points:monthly:";
/**
* 增加用户积分
*/
public void addUserPoints(String userId, long points, String reason) {
// 更新总积分排行榜
redisTemplate.opsForZSet().incrementScore(POINTS_LEADERBOARD, userId, points);
// 更新月度积分排行榜
String monthlyKey = MONTHLY_POINTS + getCurrentMonth();
redisTemplate.opsForZSet().incrementScore(monthlyKey, userId, points);
redisTemplate.expire(monthlyKey, 35, TimeUnit.DAYS);
// 记录积分变动历史
recordPointsHistory(userId, points, reason);
}
/**
* 获取用户总积分和排名
*/
public UserPointsInfo getUserPointsInfo(String userId) {
Double totalPoints = redisTemplate.opsForZSet().score(POINTS_LEADERBOARD, userId);
Long totalRank = redisTemplate.opsForZSet().reverseRank(POINTS_LEADERBOARD, userId);
String monthlyKey = MONTHLY_POINTS + getCurrentMonth();
Double monthlyPoints = redisTemplate.opsForZSet().score(monthlyKey, userId);
Long monthlyRank = redisTemplate.opsForZSet().reverseRank(monthlyKey, userId);
UserPointsInfo info = new UserPointsInfo();
info.setUserId(userId);
info.setTotalPoints(totalPoints != null ? totalPoints.longValue() : 0);
info.setTotalRank(totalRank != null ? totalRank.intValue() + 1 : -1);
info.setMonthlyPoints(monthlyPoints != null ? monthlyPoints.longValue() : 0);
info.setMonthlyRank(monthlyRank != null ? monthlyRank.intValue() + 1 : -1);
return info;
}
/**
* 获取积分排行榜
*/
public List<UserPointsRank> getPointsLeaderboard(String type, int topN) {
String key = "total".equals(type) ? POINTS_LEADERBOARD : MONTHLY_POINTS + getCurrentMonth();
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, topN - 1);
List<UserPointsRank> rankings = new ArrayList<>();
int rank = 1;
for (ZSetOperations.TypedTuple<String> tuple : tuples) {
UserPointsRank userRank = new UserPointsRank();
userRank.setRank(rank++);
userRank.setUserId(tuple.getValue());
userRank.setPoints(tuple.getScore().longValue());
rankings.add(userRank);
}
return rankings;
}
private String getCurrentMonth() {
return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
}
private void recordPointsHistory(String userId, long points, String reason) {
// 实现积分历史记录逻辑
}
}
@Data
class UserPointsInfo {
private String userId;
private Long totalPoints;
private Integer totalRank;
private Long monthlyPoints;
private Integer monthlyRank;
}
@Data
class UserPointsRank {
private Integer rank;
private String userId;
private Long points;
}
6. 性能优化与最佳实践
6.1 性能特点
操作类型 | 时间复杂度 | 性能说明 |
---|---|---|
ZADD | O(log N) | 插入效率高 |
ZRANGE | O(log N + M) | M为返回元素数 |
ZSCORE | O(1) | 获取分数很快 |
ZRANK | O(log N) | 排名查询高效 |
ZREMRANGEBYSCORE | O(log N + M) | 范围删除 |
6.2 最佳实践
6.2.1 控制集合大小
java
@Service
public class OptimizedZSetService {
private static final int MAX_LEADERBOARD_SIZE = 10000;
/**
* 安全的排行榜更新
*/
public void safeUpdateLeaderboard(String key, String member, double score) {
redisTemplate.opsForZSet().add(key, member, score);
// 保持排行榜大小
Long size = redisTemplate.opsForZSet().zCard(key);
if (size > MAX_LEADERBOARD_SIZE) {
redisTemplate.opsForZSet().removeRange(key, 0, size - MAX_LEADERBOARD_SIZE - 1);
}
}
}
6.2.2 使用Pipeline优化
java
/**
* 批量更新排行榜
*/
public void batchUpdateLeaderboard(String key, Map<String, Double> playerScores) {
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) {
for (Map.Entry<String, Double> entry : playerScores.entrySet()) {
connection.zAdd(key.getBytes(), entry.getValue(), entry.getKey().getBytes());
}
return null;
}
});
}
总结
Redis有序集合是排行榜和排序场景的最佳选择:
核心知识点
- 底层原理:跳表+哈希表的双重数据结构
- 基本操作:添加、查询、删除、范围操作
- 高级功能:分数操作、排名查询、范围删除
- 实战应用:游戏排行榜、热门内容、延时队列、积分系统
关键要点
- 双重索引:同时支持按成员和分数的高效查询
- 自动排序:插入时自动按分数排序,无需手动维护
- 范围操作:强大的分数和排名范围查询能力
- 高性能:O(log N)的查询性能,适合大数据量
实战建议
- 合理设计分数:根据业务需求设计合适的分数计算逻辑
- 控制数据规模:定期清理或限制ZSet大小
- 优化查询范围:避免大范围查询,使用分页
- 监控性能:关注内存使用和操作耗时
- 善用范围操作:充分利用ZSet的范围查询能力
通过本文学习,你应该能够熟练使用Redis有序集合实现各种排序和排行榜功能。
下一篇预告 :《Redis Stream:Redis 5.0+新数据结构详解,替代消息队列的新选择》