【系统架构核心服务设计】使用 Redis ZSET 实现排行榜服务

目录

一、排行榜的应用场景

二、排行榜技术的特点

[三、使用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的安装和使用请参考:(在文章末尾)

Spring框架学习 有这一篇就够!_spring 学习-CSDN博客

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 欢迎大家与我交流!

相关推荐
Channing Lewis17 分钟前
flask常见问答题
后端·python·flask
蘑菇丁18 分钟前
ansible批量生产kerberos票据,并批量分发到所有其他主机脚本
java·ide·eclipse
Channing Lewis18 分钟前
如何保护 Flask API 的安全性?
后端·python·flask
呼啦啦啦啦啦啦啦啦1 小时前
【Redis】持久化机制
java·redis·mybatis
怪小庄吖1 小时前
翻译:How do I reset my FPGA?
经验分享·嵌入式硬件·fpga开发·硬件架构·硬件工程·信息与通信·信号处理
C嘎嘎嵌入式开发1 小时前
什么是僵尸进程
服务器·数据库·c++
我想学LINUX2 小时前
【2024年华为OD机试】 (A卷,100分)- 微服务的集成测试(JavaScript&Java & Python&C/C++)
java·c语言·javascript·python·华为od·微服务·集成测试
梅见十柒2 小时前
计算机系统原理:一些断言
经验分享·笔记
Yeats_Liao3 小时前
Navicat 导出表结构后运行查询失败ERROR 1064 (42000): You have an error in your SQL syntax;
数据库·sql