目录
[三、使用Redis ZSET实现排行榜](#三、使用Redis ZSET实现排行榜)
[3.1 引入依赖](#3.1 引入依赖)
[3.2 配置Redis连接](#3.2 配置Redis连接)
[3.3 创建实体类(可选)](#3.3 创建实体类(可选))
[3.4 编写 Redis 操作服务层](#3.4 编写 Redis 操作服务层)
[3.5 编写控制器层](#3.5 编写控制器层)
[3.6 测试](#3.6 测试)
[3.6.1 测试 addMovieScore 接口](#3.6.1 测试 addMovieScore 接口)
[3.6.2 测试 getTopNRankings 接口](#3.6.2 测试 getTopNRankings 接口)
[3.6.3 测试 getMovieRank接口](#3.6.3 测试 getMovieRank接口)
[3.6.4 测试 getMovieScore接口](#3.6.4 测试 getMovieScore接口)
一、排行榜的应用场景
排行榜服务是一个看似简单但又复杂的设计,其在互联网产品中应用非常广泛:
- 游戏排行榜
- 商品排行榜
- 视频排行榜
- 社交排行榜
互联网应用提供排行榜功能可以对关键信息起到增强曝光的作用,并且可以在一定程度上提供用户的活跃度、参与度,从而促进互联网产品的发展。
二、排行榜技术的特点
与现实生活中的排行榜不同,互联网应用中的排行榜一般具有如下特点。
- 曝光量大
- 竞争激烈
- 实时变化
- 周期滚动
所以,在排行榜的技术实现方面,要重点考虑高并发读/写、实时展示最新排名,以及可以轻松支持周期滚动的能力。
在设计排行榜服务时,首先要考虑的问题是使用什么存储系统来维护排行榜。假如使用关系型数据库的话,因为它对高并发读/写的支持较弱面且为了支持按照评分排序,在关系型数据库中需要根据分数/积分字段,使用SELECT语句的ORDER BY子句来实现。而该方式具有如下**++缺点++**。
- **性能开销:**在有大量数据的情况下,排序操作会耗费大量的系统资源和处理时间,尤其是当需要进行多字段排序或者排序字段的数据类型不同时,查询效率更低。
- **磁盘I/O:**当需要对大量数据进行排序时,可能要使用临时表或者磁盘存储技术,使排序操作不再全部运行在内存中,而这需要进行大量的磁盘读/写操作,从面导致性能降低,查询的响应时间变长。
所以,实现排行榜不太适合使用关系型数据库。排行榜是按照积分排序的,因此很容易让人想到Redis的ZSET数据结构。ZSET是一种有序集合形式,该集合由Member组成,每个Member都有一个Score(积分),集合会按照Score自动排序。所以,目前Redis ZSET便成为实现排行榜的首选。
补充:ZSET底层数据结构是通过压缩列表和跳表实现的:
三、使用Redis ZSET实现排行榜
3.1 引入依赖
XML
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version> <!-- 使用合适的版本 -->
</dependency>
关于 Redis 客户端库 --- Jedis :
在 Java 项目中,如果需要通过代码去连接 Redis 数据库、执行如设置键值对、获取数据、进行列表操作、发布订阅等各种 Redis 支持的操作时,就需要引入 Jedis 库。
这段依赖配置就是为了方便在 Java 项目中引入 Jedis 这个强大的 Redis 客户端库,从而能在代码层面和 Redis 数据库进行交互操作,实现各种基于 Redis 存储和缓存等功能需求。
3.2 配置Redis连接
在 **application.yml
**文件中配置 Redis 连接信息:
python
redis:
host: localhost # 修改为实际Redis主机地址
port: 6379 # 修改为实际Redis端口
password: 123 # 修改为实际Redis密码
database: 0 # 选择使用的数据库,默认为0
3.3 创建实体类(可选)
这一步根据业务需求选择,如果需要存储更复杂的电影相关信息用于排行榜,可以创建对应的实体类,这里简单以电影 ID 和评分为例:
java
@Data
@Component
@AllArgsConstructor
@NoArgsConstructor
public class MovieScore {
private String movieId;
private double rating;
}
关于Lombok的安装和使用请参考:(在文章末尾)
3.4 编写 Redis 操作服务层
java
package com.snut.selltickets.service;
import com.snut.selltickets.model.MovieScore;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class RankService {
private static final String RANKING_KEY = "movie_ranking";
@Resource
private RedisTemplate<String, String> redisTemplate;
// 添加电影评分到排行榜
public void addMovieScore(String movieId, double rating) {
ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add(RANKING_KEY, movieId, rating);
}
// 获取排行榜前N名的电影(这里返回电影ID和对应评分)
public Set<MovieScore> getTopNRankings(int n) {
ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
return zSetOperations.reverseRangeWithScores(RANKING_KEY, 0, n - 1).stream()
.map(tuple -> new MovieScore(tuple.getValue(), tuple.getScore()))
.collect(Collectors.toSet());
}
// 获取电影在排行榜中的排名(从高到低排序,排名从0开始)
public Long getMovieRank(String movieId) {
ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
return zSetOperations.reverseRank(RANKING_KEY, movieId);
}
// 获取电影的评分
public Double getMovieScore(String movieId) {
ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
return zSetOperations.score(RANKING_KEY, movieId);
}
}
3.5 编写控制器层
用于对外提供接口,可测试调用
java
import com.snut.selltickets.model.MovieScore;
import com.snut.selltickets.model.Result;
import com.snut.selltickets.service.RankService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Set;
@RestController
@RequestMapping("/userApi/RankCtl")
public class RankCtl {
@Autowired
RankService rankService;
// 添加电影评分接口
@PostMapping("/add")
public ResponseEntity<String> addMovieScore(@RequestBody MovieScore movieScore) {
rankService.addMovieScore(movieScore.getMovieId(),movieScore.getRating());
return ResponseEntity.ok("电影评分成功");
}
// 获取排行榜前N名接口
@GetMapping("/topN")
public ResponseEntity<Set<MovieScore>> getTopNRankings(@RequestParam("n") int n) {
Set<MovieScore> topNRankings = rankService.getTopNRankings(n);
return ResponseEntity.ok(topNRankings);
}
// 获取电影排名接口
@GetMapping("/movieRank")
public ResponseEntity<Long> getMovieRank(@RequestParam("movieId") String movieId) {
Long userRank = rankService.getMovieRank(movieId);
return ResponseEntity.ok(userRank);
}
// 获取电影积分接口
@GetMapping("/movieRating")
public ResponseEntity<Double> getMovieScore(@RequestParam("movieId") String movieId) {
Double userScore = rankService.getMovieScore(movieId);
return ResponseEntity.ok(userScore);
}
}
3.6 测试
我这里是在前端简单模拟了一个测试器:
3.6.1 测试 addMovieScore 接口
3.6.2 测试 getTopNRankings 接口
这里返回排行榜前topN(3)的电影信息
3.6.3 测试 getMovieRank接口
3.6.4 测试 getMovieScore接口
前端测试代码
javascript
<template>
<div id="app">
<div style="height: 120px;"></div>
movieId:<input type="text" v-model="form.movieId"/>
rating:<input type="text" v-model="form.rating"/>
topN:<input type="text" v-model="n"/>
<div style="margin-bottom: 30px;"></div>
<button style="width: 150px; height: 100px;" @click="add()">add</button>
<button style="width: 150px; height: 100px;" @click="topN()">topN</button>
<button style="width: 150px; height: 100px;" @click="getMovieRank()">getMovieRank</button>
<button style="width: 150px; height: 100px;" @click="getMovieScore()">getMovieScore</button>
</div>
</template>
<script>
export default {
data() {
return {
form:{
movieId:"",
rating:""
},
n:""
}
},
methods: {
add() {
this.$http.post("userApi/RankCtl/add",this.form).then((resp) => {
this.$message({
message: resp.data,
type: 'success'
});
this.$router.go(0); //更新当前的路由 组件
});
},
topN() {
this.$http.get("userApi/RankCtl/topN?n="+this.n).then((resp) => {
this.$message({
message: resp.data,
type: 'success'
});
console.log(resp.data);
});
},
getMovieRank() {
this.$http.get("userApi/RankCtl/movieRank?movieId="+this.form.movieId).then((resp) => {
this.$message({
message: resp.data,
type: 'success'
});
console.log(resp.data);
});
},
getMovieScore() {
this.$http.get("userApi/RankCtl/movieRating?movieId="+this.form.movieId).then((resp) => {
this.$message({
message: resp.data,
type: 'success'
});
console.log(resp.data);
});
}
},
mounted() {
}
}
</script>
<style scoped>
#app{
width: 150px;
margin: 0 auto;
}
</style>
🌸🌸🌸 完结撒花🌸🌸🌸
博主WX:g2279605572 欢迎大家与我交流!