【Redis】Redis zset 实现排行榜

文章目录

  • 前言
  • 案例
  • 程序设计
    • score 设计 (相同积分的排序)
    • 缓存数据定时刷新
    • 当心缓存击穿

前言

排行榜是业务开发中常见的一个场景,如何设计一个好的数据结构能够满足高效实时的查询,下面我们结合一个实际例子来讨论一下。

案例

大概需求就是: 排行榜上显示前n个积分最高的用户. 并且相同积分先完成的排在前面. 并且还要能看到自己当前的积分.

看到这个需求的时候就想到可以用redis zset来实现

  • 首先排行榜明显是一个热点数据, 访问频率大, 且计算复杂. 肯定不能直接从数据库中读取计算排名, 否则服务很容易挂掉.
  • 然后就只能用缓存了. 于是看一下redis 的 zset 有序列表.
    • zset 是一个有序列表, 满足排序需求
    • redis 数据存在内存中, 存取效率高
    • zset 使用ziplist 或 skiplist+map实现. 直接查询用户积分时间复杂度低.
      大概就是元素少于128且长度小于64字节时用ziplist, 否则使用 skiplist+map, skiplist 存储score-value, map=value-score

程序设计

对于整个排行榜, 我们用 zset 保存排行榜数据, key 为排行榜信息, member 为用户id, score 存储用户积分.

用户信息(排行榜需要的头像昵称) 再用string或hash结构存储. 这个没啥说的, 就是查用户信息的时候别一个一个查就好了.

score 设计 (相同积分的排序)

zset对于score相同的排序是按照key的字典序排的.
所以我们需要在score里面加入时间信息.

比较简单的一种方式是积分乘以10的n次方, 后面n位用于存储时间信息.
由于时间小的排在前面, 所以可以取一个最大时间减去当前时间.

score = 积分 * 1E10 + 最大时间 - 当前时间

(其实也考虑过把时间信息放在小数位, 但是发现会丢失精度就放弃了)

需要注意两点

  • score支持的最大值为9007199254740992, 所以这个n需要结合具体的业务场景决定. 主要考虑积分可能的最大值生成的score不会越界.
  • 最大时间的取值.
    • 如果排行榜是临时的(超过某个时间就不存在或不保证数据的准确性), 那最大时间直接取排行榜的截止有效时间就可,这种方式时间信息占用的位数较少. (推荐)
    • 如果排行榜一直存在就取一个固定值, 2050年? 这种时间占用位数较多, 但是可以降低时间精度缓解一点压力, 精确到秒? 或者占用几位小数(最好自己测一下精度有没有影响)

然后我这里根据实际业务, 使用的固定的最大时间2050年, 时间精确到秒, 所以n取的10, 此时支持的最大积分为90w, 满足实际业务场景.

这里贴一下相关代码

private static final double STEP = 1E10;
private static final long SECOND_20500101 = 2524579200L;public static Double createScoreWithTimeAsc(Integer value, long timeSecond) {if (value == null) {return 0D;}return value * STEP + SECOND_20500101 - timeSecond;
}/*** 返回的是incrScore, 用于 redisTemplate.opsForZSet().incrementScore*/
public static Double incrScoreWithTimeAsc(Integer increment, Double originScore) {if (increment == null) {return 0D;}if (originScore == null || originScore < 1.0) {return createScoreWithTimeAsc(delta);}double last = originScore % STEP; //上次的时间long now = System.currentTimeMillis() / 1000;return increment * STEP - last + SECOND_20500101 - now;
}public static long getValueFromTimeScore(Double score) {return (long) (score / STEP);
}

然后在加减积分的代码就不贴了, 但是需要注意并发的情况, 对于这种排行榜类的数据也是比较容易出现并发的.

缓存数据定时刷新

虽然我们将排行榜数据存入zset中了, 但这个只是提高了我们的访问效率, 并不能完全保证数据的准确性. 可能会因为各种原因(并发, 网络异常)导致缓存数据不准确, 因此需要定时刷新缓存数据.

然后缓存刷新策略大概有一下几种:

  • 全量刷新: 把所有缓存数据都重新计算一遍, 比如每天刷一遍
  • 增量刷新: 把一定时间内变化的缓存数据刷新, 每个小时刷一次
  • 根据数据变化频率动态刷新: 类似redis持久化策略. 一定时间内变化的频率到达一个阈值就刷新.
    具体采取哪种策略也是看具体的需求, 对数据的准确性实时性需求、性能需求等. 可以同时结合多种策略. 以及时间间隔也取决于产品需求和刷新耗时.

大概就是使用一个定时任务, 查询数据库最近一段时间内积分变化的用户, 对这些用户的积分进行重算, 刷新缓存.

还有就是到了缓存过期的时候, 就别再刷了. 除非打算这个排行榜一直存在缓存中.

当心缓存击穿

缓存肯定是要设置过期时间的, 过期时间肯定是在缓存数据不经常访问的时候. 那如果缓存过期后用户访问排行榜, 这个时候就需要从数据库中查询相关数据, 重新计算排行榜前n位(没必要全部重算), 显然重算排行榜是一个比较费事费力的操作.

但是假如这个时候是大量用户并发访问, 然后查询排行榜缓存, 发现没有数据, 于是都去查询数据库重算. 这个时候数据库压力就会很大, 很容易挂掉. 即 缓存击穿.

然后解决方式大概有几种

  • 不设置过期时间, 不过期就不会失效.
  • 加锁, 重新加载缓存的时候加锁, 防止所有的请求都去数据库查询重算.

然后结合具体场景, 这里加载缓存时不一定时缓存过期了, 可能还没构建缓存, 就有大量用户访问该排行榜. 并且该排行榜缓存没有必要一直存在, 浪费空间.

然后记得用双重锁检测

// 伪代码// redis 里查排行榜
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeWithScores(zSetKey, 0, endIndex);
// 判断是否为空
if (CollectionUtils.isEmpty(typedTuples)) {// 如果缓存为空, 则加锁加载缓存, 防止缓存击穿lock();try {// 再查一次typedTuples = stringRedisTemplate.opsForZSet().reverseRangeWithScores(zSetKey, 0, endIndex);if (CollectionUtils.isEmpty(typedTuples)) { // 还是空, 重新加载缓存result = computeDataFromDB();// empty returnif (CollectionUtils.isEmpty(result)) {return Collections.emptyList();}typedTuples = loadDataToCache(result);}} finally {unlock();}
}

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

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

相关文章

数据库监控工具-PIGOSS BSM

PIGOSS BSM 运维监控系统的重要功能之一是数据库监控&#xff0c;它能够帮助数据库管理员(DBA)和系统管理员监控包含Oracle、SQL Server、MySQL、DB2、PostgreSql、MongoDB、达梦、南大通用、人大金仓、神州通用等多种类异构型的数据库环境。PIGOSS BSM通过执行数据库查询来采集…

DSSAT模型教程

详情点击链接&#xff1a;R语言与作物模型&#xff08;DSSAT模型&#xff09;教程 前言 随着基于过程的作物生长模型&#xff08;Process-based Crop Growth Simulation Model&#xff09;的发展&#xff0c;R语言在作物生长模型和数据分析、挖掘和可视化中发挥着越来越重要的…

Python基础-列表(list)和元组(tuple)

Python包含6种内建的序列&#xff1a;列表&#xff0c;元组&#xff0c;字符串&#xff0c;Unicode字符串&#xff0c;buffer对象&#xff0c;xrange对象&#xff0c;本文讨论列表和元组。 1.列表可以修改&#xff0c;元组则不能修改。 2.几乎在所有的情况下&#xff0c;列表…

详细解析python视频选择--【思维导图知识范围】

C ,JAVA JAVAWEB ,微信小程序等 都有视频选择的分析。 语言视频选择收录专辑链接C张雪峰推荐选择了计算机专业之后-在大学期间卷起来-【大学生活篇】JAVA黑马B站视频JAVA部分的知识范围、学习步骤详解JAVAWEB黑马B站视频JAVAWEB部分的知识范围、学习步骤详解SpringBootSpringB…

Cesium:加载geojson面贴地和显示边界问题

1.背景 cesium加载geojson面数据后&#xff0c;有部分数据在地形下面显示不全&#xff0c; 加了clampToGround: true&#xff0c;设置贴地后&#xff0c;边界又不见了 this.viewer.dataSources.add(GeoJsonDataSource.load(http://xx/xzbj.geojson, {stroke: Color.BLACK.with…

保护隐私与安全的防关联、多开浏览器

随着互联网的不断发展&#xff0c;我们越来越离不开浏览器这个工具&#xff0c;它为我们提供了便捷的网络浏览体验。然而&#xff0c;随着我们在互联网上的活动越来越多&#xff0c;我们的个人信息和隐私也日益暴露在网络风险之下。在这种背景下&#xff0c;为了保护个人隐私和…

阿里Java开发手册~ORM 映射

1. 【强制】在表查询中&#xff0c;一律不要使用 * 作为查询的字段列表&#xff0c;需要哪些字段必须明确写明。 说明&#xff1a; 1 &#xff09; 增加查询分析器解析成本。 2 &#xff09; 增减字段容易与 resultMap 配置不一致。 2. 【强制】 POJO 类的 布尔 属性不…

PDF添加水印以及防止被删除、防止编辑与打印

方法记录如下&#xff1a; 1、添加水印&#xff1b; 2、打印输出成一个新的pdf&#xff1b; 3、将pdf页面输出成一张张的图片&#xff1a;&#xff08;福昕pdf操作步骤如下&#xff09; 4、将图片组装成一个新的pdf&#xff1a;&#xff08;福昕pdf操作步骤如下&#xff09;…

Android 任务调度 WorkManager 和 JobScheduler 的使用

在过去&#xff0c;常常使用后台Service来执行定时任务。虽然Service是执行后台任务的一种方式&#xff0c;但自Android 8.0&#xff08;API级别26&#xff09;以后&#xff0c;Google推荐使用更高效和系统友好的方式来执行定时任务&#xff0c;例如JobScheduler和WorkManager。…

flask实现一个登录界面

flask实现一个登录界面 基础的Flask项目结构 forms.py&#xff1a;定义登录表单和表单字段的文件。templates/login.html&#xff1a;用于渲染登录表单的 HTML 模板文件。routes.py&#xff1a;定义应用的路由和视图函数的文件。__init__.py&#xff1a;创建并初始化 Flask 应…

Java Spring和Spring集成Mybatis

0目录 1.Spring 2.Spring集成Mybatis 1.Spring 特性 IOC&#xff1a;控制反转 AOP&#xff1a;面向切面 Spring组成部分 在SMM中起到的作用&#xff08;粘合剂&#xff09; Spring理念 OOP核心思想【万物皆对象】 Spring核心思想【万物皆Bean组件】 Spring优势 低侵入式 …

MySQL学习笔记 ------ 排序查询

一、语法 SELECT 查询列表 FROM 表名 【WHERE 筛选条件】 ORDER BY 排序列表 【ASC}DESC】&#xff1b;#支持多个排序条件&#xff0c;以逗号分隔 二、特点 1、ASC &#xff1a;升序&#xff0c;如果不写默认升序 DESC&#xff1a;降序 2、排序列表 支持 单个字段…

基于新浪微博海量用户行为数据、博文数据数据分析:包括综合指数、移动指数、PC指数三个指数

基于新浪微博海量用户行为数据、博文数据数据分析&#xff1a;包括综合指数、移动指数、PC指数三个指数 项目介绍 微指数是基于海量用户行为数据、博文数据&#xff0c;采用科学计算方法统计得出的反映不同事件领域发展状况的指数产品。微指数对于收录的关键词&#xff0c;在指…

Java运算符

大体上&#xff0c;与C语言差不多&#xff0c;不同的地方&#xff0c;我用红色字体标注了 算术运算符 1. 基本四则运算符&#xff1a;加减乘除模 ( - * / %) int a 10 ; int b 20 ; System . out . println ( a b ); // 30 System . out . println ( a - b…

数据结构--线性表2-1

目录 一、线性结构的定义 二、线性表的表示 三、顺序表的实现&#xff08;或操作&#xff09; 1、修改&#xff1a; 2、插入&#xff1a; 四、顺序表的运算效率分析&#xff1a;时间效率分析&#xff1a; 一、线性结构的定义 若结构时非空有限集&#xff0c;则有且仅有一个…

8 个线程池最佳实践和坑!使用不当直接生产事故!!

这篇文章我会简单总结一下我了解的使用线程池的时候应该注意的坑以及一些优秀的实践。拿来即用&#xff0c;美滋滋&#xff01; 1、正确声明线程池 线程池必须手动通过 ThreadPoolExecutor 的构造函数来声明&#xff0c;避免使用Executors 类创建线程池&#xff0c;会有 OOM …

线性代数的学习和整理2:用EXCEL进行矩阵计算

目录 矩阵的各种概念 矩阵的维数 矩阵的基底 矩阵的列向量 矩阵的平直概念 矩阵的乘法的映射图 矩阵的秩 矩阵的乘法具有不可交换性 矩阵的模 矩阵的各种概念 矩阵的维数 &#xff08;a1,a2&#xff09;是2维的&#xff08;a1,a2,a3&#xff09;是3维的&#xff08;a…

一文复习Java基础面试知识

申明&#xff1a;本人于公众号Java筑基期&#xff0c;CSDN先后发当前文章&#xff0c;标明原创&#xff0c;转载二次发文请注明转载公众号&#xff0c;另外请不要再标原创 &#xff0c;注意违规 Java基础知识 1、基本数据类型 在Java中&#xff0c;共有八种基本数据类型&…

基于深度学习淡水鱼体重智能识别模型研究

工作原理为&#xff1a;首先对大众淡水鱼图片进行数据清洗并做标签分类&#xff0c;之后基于残差网络ResNet50模型进行有监督的分类识别训练&#xff0c;获取识别模型。其次通过搭建回归模型设计出体重模型&#xff0c;对每一类淡水鱼分别拟合出对应的回归方程&#xff0c;将获…

Ubuntu ImportError: No module named ‘_tkinter‘问题解决方式

尝试安装tkinter模块.出现以下问题 sudo pip3 install tkinter [sudo] password for Ns3: DEPRECATION: Python 3.5 reached the end of its life on September 13th, 2020. Please upgrade your Python as Python 3.5 is no longer maintained. pip 21.0 will drop support …