【Redis实践】使用zset实现实时排行榜以及一些优化思考

文章目录

  • 1.概述
  • 2.zset的基本概念说明
    • 2.1.数据结构说明
    • 2.2.zset做排行榜的指令
  • 3. 项目中的实践
    • 3.1.RedisTemplate实现排行榜
    • 3.2.可能存在的问题及解决方案
      • 3.2.1. 限制成员的数量
      • 3.2.2.保留当前分数与最高分数
      • 3.2.3.批量操作成员分数,减少并发
  • 4.总结

1.概述

我们在做互联网项目的时候会遇到一些排行版的需求,如果排行榜的时效性不高,比如日榜,周榜这种,可以考虑通过定时任务统计、聚合数据并落库,需要查询的时候直接查询这个统计好的数据就好了。但有时候我们遇到的需求时效性会高一点,比如小时榜、分钟榜、甚至实时排行榜,这种情况下再使用定时任务统计的方式就不太合适了。
在Redis中有个叫zset的数据结构,非常适合用来做排名,它的数据结构中有一个score分数,我们可以直接使用Redis的指令,让里面的数据的按分数的大小进行排序。所以zset往往是我们做高时效性排行榜的解决方案。

2.zset的基本概念说明

2.1.数据结构说明

下面列举zset的操作指令,有一定经验的同学看到这些指令就应该知道大概可以如何使用了。

指令详细指令说明
zaddzadd key score member添加成员和分数,也可以替换成员分数
zincrbyzincrby key score member为某个成员累加分数,如果成员不存在则创建成员
zremzrem key member删除某个成员
zscorezscore key member返回某个成员的分数
zrangezrange key 0 -1 withscores按分值从小到大排
zrevrangezrevrange key 0 -1 withscores按分值从大到小排

这里需要说明一下的两个range方法,0 -1 是零和负一,中间用空格隔开,意思是获取所有的分数,如果是想获取指定数量的分数,例如top10,这里可以使用 0 9,最后一个withscores的意思的Redis会返回每个成员的分数。在没有这个选项的情况下,ZRANGE只会返回成员的名称,而不包括其对应的分数。

下面可以看看zset的使用方法。

2.2.zset做排行榜的指令

以一个例子来说明,假设现在有3个用户和对应的分数分别如下:

user1: 100
user2: 200
user3: 150

现在就通过Redis的指令,来试一下排行榜功能,依次键入以下指令:

zadd leaderboard 100 user1
zadd leaderboard 200 user2
zadd leaderboard 150 user3zrange leaderboard 0 -1 WITHSCORES
zrevrange leaderboard 0 -1 WITHSCORES

在这里插入图片描述
可以看到的是,返回的结果的是一行member,一行分数的结构,按照分数的高低进行排序的。

3. 项目中的实践

下面通过在通过RedisTemplate来封装一下排行榜的demo,然后会列出一些思考,考虑实际存在的问题及其解决方案。

3.1.RedisTemplate实现排行榜

由于Redis在SpringBoot中的配置不是本章的重点,以下忽略配置。提供了几个简单的方法,分别是:

  • 添加或替换用户分数
  • 添加或更新用户分数
  • 获取排行榜前N名
  • 获取某个用户的排名
  • 删除指定用户
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;import java.util.Set;@Service
public class LeaderboardService {@Autowiredprivate RedisTemplate<String, String> redisTemplate;private static final String LEADERBOARD_KEY = "leaderboard";/*** 添加或替换用户分数*/public void addOrReplaceScore(String userId, double score) {ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();zSetOps.add(LEADERBOARD_KEY, userId, score);}/*** 添加或更新用户分数*/public void addOrUpdateScore(String userId, double score) {ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();zSetOps.incrementScore(LEADERBOARD_KEY, userId, score);}/*** 获取排行榜前N名*/public Set<ZSetOperations.TypedTuple<String>> getTopRanks(int topN) {ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();return zSetOps.reverseRangeWithScores(LEADERBOARD_KEY, 0, topN - 1);}/*** 获取用户排名*/public Long getUserRank(String userId) {ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();Long rank = zSetOps.reverseRank(LEADERBOARD_KEY, userId);// 排名从1开始return rank != null ? rank + 1 : null; }/*** 删除指定用户*/public void removeUser(String userId) {redisTemplate.opsForZSet().remove(LEADERBOARD_KEY, userId);}
}

方法封装好了之后,通过controller提供一个用户访问入口就可以了。下面讲一讲可能遇到的问题以及处理方案。

3.2.可能存在的问题及解决方案

3.2.1. 限制成员的数量

一个活动如果参与的人数多,就可能出来成员一直不断膨胀的情况,但实际上我们对排行榜的需求往往只是需要前xx名的数据,例如前10名、前100名、前10000名等等。根据实际的需求,我们可以限制zset中的数量。假如现在保留一万名,就可以提供一个方法,清理排名一万以后的数据:

// 限制排行榜最大长度
private static final int MAX_RANKING_SIZE = 10000; /*** 清理低活跃数据*/
public void cleanUpInactiveUsers() {ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();Long memberCount = Optional.ofNullable(zSetOps.zCard(LEADERBOARD_KEY)).orElse(0L);if (memberCount > MAX_RANKING_SIZE) {zSetOps.removeRange(LEADERBOARD_KEY, 0, -MAX_RANKING_SIZE - 1);}
}

这个方法可以在插入新的成员时调用,但是由于会多次操作Redis,其实是不建议在保存排行榜分数的时候执行的,可以考虑通过定时任务来处理,例如:

@Component
public class ScheduledTasks {@Autowiredprivate LeaderboardService leaderboardService;// 每天凌晨2点清理@Scheduled(cron = "0 0 2 * * ?")public void cleanInactiveUsersTask() {leaderboardService.cleanUpInactiveUsers();}
}

这里的每天凌晨两点,可以根据需要调整为每小时清理一次,每10分钟清理一次等等。

3.2.2.保留当前分数与最高分数

zset中针对同一个用户只能保存一个分数,如果要实现保存当前分数和最高分数,可考虑用两个zset来处理,处理方式也比较简单,按照:获取当前分数比较分数更新历史最高分数的顺序做就好了,下面是一个简单的代码:

public void updateScore(String userId, double newScore) {// 1. 获取当前分数Double currentScore = redisTemplate.opsForZSet().score("currentLeaderboard", userId);// 2. 更新当前分数redisTemplate.opsForZSet().add("currentLeaderboard", userId, newScore);// 3. 更新历史最高分数if (currentScore == null || newScore > currentScore) {redisTemplate.opsForZSet().add("highestLeaderboard", userId, newScore);}
}

同样的,历史最高分数的zset也需要考虑限制成员数量的问题。此外,如果要考虑原子性,可以通过将上述的代码封装到lua脚本中执行。

3.2.3.批量操作成员分数,减少并发

在并发较高的情况下,如果想减少Redis插入请求,我们可以在内存中先保存一部分的请求,等达到某个阈值的时候,再做Redis的插入操作。这里阈值可以是积累了多少个成员做批量更新,也可以是积累到了一定的时间,例如积累了一分钟的数据。

RedisTemplate中的add()有一个重载方法,可以传入一个set进行批量操作:
在这里插入图片描述
在这里插入图片描述
这是一个interface,我们可以先实现一下:

public class MemberValue<T> implements ZSetOperations.TypedTuple<T> {private T value;private Double score;@Overridepublic T getValue() {return value;}public void setValue(T value) {this.value = value;}@Overridepublic Double getScore() {return score;}public void setScore(Double score) {this.score = score;}@Overridepublic int compareTo(ZSetOperations.TypedTuple<T> o) {return 0;}
}

然后以每50个成员更新一次为例,代码如下:

private Set<ZSetOperations.TypedTuple<String>> memberSet = new HashSet<>();@Async
public void asyncBatchSetScore(String userId, double score) {MemberValue<String> memberValue = new MemberValue<>();memberValue.setScore(score);memberValue.setValue(userId);synchronized (LeaderboardService.class) {memberSet.add(memberValue);if (memberSet.size() >= 50) {ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();zSetOps.add(LEADERBOARD_KEY, memberSet);memberSet.clear();}}
}

如果要修改阈值为时间,可以维护一个时间窗口,并修改判断条件即可,这里不展开了。

4.总结

本章先讲解了zset的数据结构以及使用方式,然后通过RedisTemplate做了一个Demo,演示如何实现排行榜,并对一些可能遇见的问题做了思考了解决方案。在开发中,可以选择其中的一些方案来解决实际的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/57895.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++_STL_xx_番外01_关于STL的总结(常见容器的总结;关联式容器分类及特点;二叉树、二叉搜索树、AVL树(平衡二叉搜索树)、B树、红黑树)

文章目录 1. 常用容器总结2. 关联式容器分类3. 二叉树、二叉搜索树、AVL树、B树、红黑树 1. 常用容器总结 针对常用容器的一些总结&#xff1a; 2. 关联式容器分类 关联式容器分为两大类&#xff1a; 基于红黑树的set和map&#xff1b;基于hash表的unorder_set和unorder_ma…

【LwIP源码学习4】主线程tcpip_thread

前言 本文对lwip的主要线程tcpip_thread进行分析。 正文 tcpip_thread是lwip最主要的线程&#xff0c;其创建在tcpip_init函数中 sys_thread_new(TCPIP_THREAD_NAME, tcpip_thread, NULL, TCPIP_THREAD_STACKSIZE, TCPIP_THREAD_PRIO);tcpip_init函数被TCPIP_Init函数调用。…

光圈,感光度,感光器件

光圈&#xff08;通光孔&#xff09;&#xff0c;是一个用来控制光线透过镜头进入机身内感光面光量的装置&#xff0c;通常设置在镜头内。通常&#xff0c;我们用f值来表达光圈大小。通俗来说&#xff0c;摄像机镜头拍照时&#xff0c;不可能随意改变镜头直径&#xff0c;但可以…

Llama 3.2 Vision Molmo:多模态开源生态系统基础

编者按&#xff1a; 视觉功能的融入对模型能力和推理方式的影响如何&#xff1f;当我们需要一个既能看懂图像、又能生成文本的 AI 助手时&#xff0c;是否只能依赖于 GPT-4V 这样的闭源解决方案&#xff1f; 我们今天为大家分享的这篇文章&#xff0c;作者的核心观点是&#xf…

高效视频制作大提速,视频剪辑软件的高级自定义命令功能批量调整视频的色调、饱和度和亮度,轻松驾驭视频编辑技巧

在浩瀚的数字海洋中&#xff0c;视频如同璀璨的星辰&#xff0c;而每一颗星辰都渴望被精心雕琢&#xff0c;闪耀出最独特的光芒。想象一下&#xff0c;你手握一把神奇的钥匙&#xff0c;能够轻松解锁批量视频剪辑的奥秘&#xff0c;让每一帧画面都跃动着你的创意与激情。这把钥…

[RootersCTF2019]ImgXweb

审题 看到robots.txt,看到里面的文件&#xff0c;打开看到 you-will-never-guess这个字符串 进行注册登录 可以看到典型的jwt加密的Cookie 想到之前的字符串可能是密匙&#xff0c;更改为admin&#xff0c;进行登录。 成功后可以看到flag.png。 发现图形打不开 使用curl进…

32单片机HAL库的引脚初始化

在使用HAL库时&#xff0c;GPIO初始化函数定义在stm32f4xx_hal_gpio.c文件中&#xff0c;如下&#xff1a; void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init); 由这个函数可以看出&#xff0c;在初始化GPIO时&#xff0c;需要向函数传入2个结构体&…

ubuntu【桌面】 配置NAT模式固定IP

DHCP分配导致虚拟机IP老变&#xff0c;SSH老要重新配置&#xff0c;设成静态方便些 一、设NAT模式 1、设为NAT模式 2、看模式对应的虚拟网卡 - VMnet8 3、共享主机网卡网络到虚拟网卡 - VMnet8 二、为虚拟网卡设置静态IP 记住这个IP 三、设置ubuntu固定IP 1、关闭DHCP并…

确保企业架构与业务的一致性与合规性:数字化转型中的关键要素与战略实施

在现代企业的数字化转型过程中&#xff0c;确保企业架构&#xff08;Enterprise Architecture, EA&#xff09;与企业业务的紧密一致性与合规性至关重要。无论是在战略层面还是运营层面&#xff0c;EA都为企业的未来发展提供了清晰的蓝图&#xff0c;确保企业在应对复杂的业务环…

Pinctrl子需要中client端使用pinctrl过程的驱动分析

往期内容 本专栏往期内容&#xff1a; Pinctrl子系统和其主要结构体引入Pinctrl子系统pinctrl_desc结构体进一步介绍Pinctrl子系统中client端设备树相关数据结构介绍和解析inctrl子系统中Pincontroller构造过程驱动分析&#xff1a;imx_pinctrl_soc_info结构体 input子系统专栏…

Failed to search for file: Cannot update read-only repo

今天在读《Linux就该这么学》并上机操作RedHat Linux 8。结果在执行指令时却出现了问题: 我明明已经是root权限了&#xff0c;我于是上网去找&#xff0c;但也没看到合适的解答。为什么会和书上的操作结果不一样。 后来我突然意识到是不是我打了不该打的空格&#xff0c;于是…

SpringBoot实现验证码案例

目录 实现逻辑前后端交互接口前端代码后端代码 实现逻辑 1、后端功能&#xff1a;随机生成验证码图片&#xff0c;并把交给前端、接收用户输入&#xff0c;验证用户输入的验证码是否正确、 2、前端功能&#xff1a;显示验证码&#xff0c;提供输入框供用户输入他们看到的验证…

OpenCV基本操作(python开发)——(7)实现图像校正

OpenCV基本操作&#xff08;python开发&#xff09;——&#xff08;1&#xff09; 读取图像、保存图像 OpenCV基本操作&#xff08;python开发&#xff09;——&#xff08;2&#xff09;图像色彩操作 OpenCV基本操作&#xff08;python开发&#xff09;——&#xff08;3&…

记录新建wordpress站的实践踩坑:wordpress 上传源码新建站因权限问题导致无法访问、配置新站建站向导以及插件主题上传配置的解决办法

官方文档&#xff1a;How to install WordPress – Advanced Administration Handbook | Developer.WordPress.org 但是没写权限问题&#xff0c;可以下载到 wordpress官方包。 把下载的wordpresscn的包解压并上传到服务器目录下&#xff0c;但是因为是root上传导致了权限问题…

qt QBrush详解

1、概述 QBrush是Qt框架中的一个基本图形对象类&#xff0c;它主要用于定义图形的填充模式。QBrush可以用于填充如矩形、椭圆形、多边形等形状&#xff0c;也可以用于绘制背景等。通过QBrush&#xff0c;可以设置填充的颜色、样式&#xff08;如实心、渐变、纹理等&#xff09…

练习LabVIEW第三十四题

学习目标&#xff1a; 刚学了LabVIEW&#xff0c;在网上找了些题&#xff0c;练习一下LabVIEW&#xff0c;有不对不好不足的地方欢迎指正&#xff01; 第三十四题&#xff1a; 在一个波形表中显示三条随机数组成的曲线&#xff0c;分别用红&#xff0c;绿&#xff0c;蓝三种…

彻底理解链表(LinkedList)结构

目录 比较操作结构封装单向链表实现面试题 循环链表实现 双向链表实现 链表&#xff08;Linked List&#xff09;是一种线性数据结构&#xff0c;由一组节点&#xff08;Node&#xff09;组成&#xff0c;每个节点包含两个部分&#xff1a;数据域&#xff08;存储数据&#xff…

【MySQL系列】字符集设置

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

02- 模块化编程-001 ADC采样与显示

周末把单片机的开发环境理顺了,开始模块化编程的实践&#xff0c;先从外围模块开始&#xff0c;先从独立的模块&#xff0c;然后构建复杂一些的综合应用&#xff0c;条件所限&#xff0c;以protues的仿真为主。 1、单片机内置ADC采样与显示电路 2、电路原理简介 该电路主要由…

VScode设置系统界面字体

现象&#xff1a; 系统界面字体太大&#xff0c;导致菜单栏字体显示不全&#xff0c;每次使用都要先点然后才能打开终端和帮助 缩小字体应该就可以实现全部都看到的效果 解决步骤 1. “齿轮形状”设置中心---->设置 2.输入zoom 3.用户—>窗口—>修改“Window: Zoom…