【Redis】Redis zset 实现排行榜

news/2024/10/23 7:27:29/

文章目录

  • 前言
  • 案例
  • 程序设计
    • 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.ppmy.cn/news/979669.html

相关文章

20款奔驰S350商务型加装原厂前排座椅通风系统,夏天必备的功能

通风座椅的主动通风功能可以迅速将座椅表面温度降至适宜程度&#xff0c;从而确保最佳座椅舒适性。该功能启用后&#xff0c;车内空气透过打孔皮饰座套被吸入座椅内部&#xff0c;持续时间为 8 分钟。然后&#xff0c;风扇会自动改变旋转方向&#xff0c;将更凉爽的环境空气从座…

16K个大语言模型的进化树;81个在线可玩的AI游戏;AI提示工程的终极指南;音频Transformers课程 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f916; LLM 进化树升级版&#xff01;清晰展示 15821 个大语言模型的关系 这张进化图来自于论文 「On the Origin of LLMs: An Evolutionary …

【C++ 进阶】继承

一.继承的定义格式 基类又叫父类&#xff0c;派生类又叫子类&#xff1b; 二.继承方式 继承方式分为三种&#xff1a; 1.public继承 2.protected继承 3.private继承 基类成员与继承方式的关系共有9种&#xff0c;见下表&#xff1a; 虽然说是有9种&#xff0c;但其实最常用的还…

无涯教程-jQuery - stopPropagation()方法函数

stopPropagation()方法停止将事件冒泡到父元素&#xff0c;从而防止任何父处理程序收到该事件的通知。 您可以使用方法 event.isPropagationStopped()来了解是否曾经在该事件对象上调用过此方法。 stopPropagation() - 语法 event.stopPropagation() stopPropagation() - …

网络安全进阶学习第七课——文件包含漏洞

文章目录 一、文件包含概念二、文件包含漏洞原理三、文件包含分类文件包含分为两种&#xff1a; 四、与文件包含相关的配置文件&#xff1a;&#xff08;php.ini文件&#xff09;五、与文件包含有关的函数1、include()2、include_once()3、require()4、require_once() 六、文件…

FCPX插件-15组金色华丽粒子特效闪耀动画 Awards Backgrounds

Awards Backgrounds是fcpx上一个很棒的电影级效果插件&#xff0c;Awards Backgrounds 包含15组金色华丽粒子特效闪耀动画&#xff0c;可以为您的作品创建豪华的背景或叠加特效&#xff01;包含各种带有可编辑颜色的下落闪闪发光粒子的场景。用于展示奖项提名者、优雅的表演、祝…

探索NE555:一款经典的集成电路(超详细)

NE555是一款经典的集成电路&#xff0c;它在电子领域被广泛应用于定时器、脉冲发生器、电压控制振荡器等各种应用场景。它的设计简单、易于使用&#xff0c;并且具备稳定可靠的性能&#xff0c;因此深受电子爱好者和工程师的青睐。本篇博客将详细介绍NE555的原理、工作模式和常…

下拉框可筛选可树状多选组件

实际效果图片 父页面 <el-form-item label"转发&#xff1a;" :label-width"formLabelWidth" class"formflex_item"><el-select ref"select" :clearable"true" clear"clearSelect" remove-tag"r…