利用 Redis 实现延迟队列(点赞场景)

🌈点赞场景在前段时间有很多人都在争论,我也看了一些视频和文档,最后觉得b站技术的这篇写得很好

【点个赞吧】 - B站千亿级点赞系统服务架构设计 - 哔哩哔哩

🌈所以我也尝试用 Redis 的延迟队列来写一个点赞处理的 demo(这都是基于高并发情况下),完全没有落地。后续有时间会专门写一个点赞的技术方案

🌈至于为什么选择 Redis 延迟队列而不是用常见的 MQ,因为我还在学习阶段,想更好地学习 Redis 的延迟队列

🌈由于只是个菜只因,接触到和学到的技术并不多,可能还有很多情况没有考虑到

目录

1. Redis 实现延迟队列的方案

2. Redis 过期事件监听实现延时任务

3. Redis 过期事件监听实现延时任务功能有什么缺陷

3.1. 时效性差

3.2. 丢消息

3.3. 多服务实例下存在消息重复消息的问题

4. 为什么选用Redission作为延迟队列

5. 使用 Redis 实现延时任务有什么注意的地方?

6. 如何使用 Redisson 实现延迟队列(点赞场景)

6.1. 基础配置

6.2. 延迟队列实战


1. Redis 实现延迟队列的方案

基于 Redis 实现延时任务的功能无非就下面两种方案:

  1. Redis 过期事件监听
  2. Redisson 内置的延时队列

这里选用的是用 Redission 内置的延迟队列,所以实现的着重点放在 Redission

2. Redis 过期事件监听实现延时任务

Redis 2.0 引入了 发布订阅(Pub/Sub) 功能。在 Pub/Sub 模型中,引入了一个名为 channel(频道) 的概念,类似于消息队列中的 topic(主题)

Pub/Sub 涉及两个主要角色:发布者(Publisher)订阅者(Subscriber,也称为消费者)

  • 发布者 通过 PUBLISH 命令将消息发送到指定的 channel
  • 订阅者 通过 SUBSCRIBE 命令订阅感兴趣的 channel,并且可以同时订阅一个或多个 channel

Pub/Sub 模式 中,生产者需要指定将消息发送到哪个 channel,而消费者通过订阅对应的 channel 来获取消息。Redis 内部也存在一些默认的 channel,这些通道用于 Redis 自身发送消息,而非用户代码生成。只需监听这些 channel,即可获取与 过期 key 相关的通知,从而实现延时任务的功能。

这一特性被 Redis 官方称为 Keyspace Notifications,其主要作用是 实时监控 Redis 中键和值的变化。通过它,开发者能够及时捕捉键的变化(如过期、删除等事件),从而执行相应的处理逻辑

3. Redis 过期事件监听实现延时任务功能有什么缺陷

3.1. 时效性差

官方文档的一段介绍解释了时效性差的原因,地址:

Redis keyspace notifications | Docs

Redis 中的 过期事件消息 只有在 Redis 服务器真正删除 key 时才会发布,而不是在 key 到达过期时间后立即发布。

常见的过期数据删除策略有两种:

  1. 惰性删除:仅当访问 key 时,才会检查其是否过期。这种方式对 CPU 友好,但可能导致大量过期 key 未及时删除,继续占用内存。
  2. 定期删除:Redis 会定期抽取一部分 key,检查并删除过期的 key。为了减少删除操作对 CPU 的影响,Redis 会限制删除操作的执行时长和频率。虽然定期删除更有利于释放内存,但也可能增加 CPU 负载。

Redis 结合了这两种策略,采用 定期删除惰性删除 的方式。定期删除保证了内存的回收,而惰性删除则在取用时保证 CPU 性能。

因此,可能会出现这样一种情况:虽然设置了 key 的过期时间,但当该时间到达时,key 可能尚未被删除,导致 过期事件 未及时发布。

其他的文章测试

请勿过度依赖Redis的过期监听-阿里云开发者社区

3.2. 丢消息

Redis 的 pub/sub 模式中的消息并不支持持久化,这与消息队列不同。在 Redis 的 pub/sub 模式中,发布者将消息发送给指定的频道,订阅者监听相应的频道以接收消息。当没有订阅者时,消息会被直接丢弃,在 Redis 中不会存储该消息。

3.3. 多服务实例下存在消息重复消息的问题

Redis 的 pub/sub 模式目前只有广播模式,这意味着当生产者向特定频道发布一条消息时,所有订阅相关频道的消费者都能够收到该消息。

这个时候,我们需要注意多个服务实例重复处理消息的问题,这会增加代码开发量和维护难度。

4. 为什么选用Redission作为延迟队列

Redisson 是一个开源的 Java Redis 客户端,提供了许多开箱即用的功能,包括多种分布式锁的实现和延迟队列。Redisson 内置的延迟队列 RDelayedQueue 利用 Redis 的 SortedSet 实现延时任务功能。

SortedSet 是一个有序集合,每个元素都有一个分数,代表其优先级或时间权重。Redisson 通过将需要延迟执行的任务插入到 SortedSet 中,并为它们设置相应的过期时间作为分数来实现延迟队列。

Redisson 在客户端启动一个定时任务,当时间到达时,它使用 zrangebyscore 命令扫描 SortedSet 中已过期的元素(即分数小于或等于当前时间的元素)。这些过期元素会被从 SortedSet 中移除,并加入到就绪消息列表(List 结构)中。

当任务被移到就绪消息列表时,Redisson 通常还会通过 Redis 的发布/订阅机制(Pub/Sub)通知消费者有新任务到达。就绪消息列表是一个阻塞队列,消费者可以使用阻塞操作(如 BLPOP key 0,其中0表示无限等待)来监听。由于 Redis 的 Pub/Sub 机制是事件驱动的,它避免了轮询开销,只有在有新消息时才会触发处理逻辑。

需要注意的是,Redisson 的定时任务调度器并不是以固定时间间隔频繁调用 zrangebyscore 命令进行扫描,而是根据 SortedSet 中最近的到期时间动态调整下一次检查的时间点。

相比于使用 Redis 过期事件监听实现延时任务,Redisson 延迟队列具有以下优势:

  1. 减少丢失消息的可能性RDelayedQueue 中的消息会被持久化,即使 Redis 宕机,根据持久化机制,可能仅丢失少量消息,影响不大。此外,还可以使用数据库扫描作为补偿机制。
  2. 避免消息重复消费:所有客户端从同一个目标队列获取任务,避免了重复消费的问题。

尽管 Redisson 提供了便利的延迟队列功能,但在实际项目中,如果需要更高的吞吐量和可靠性,通常优先选择使用消息队列的延时消息方案。消息队列可以通过保障消息消费的可靠性和控制生产者与消费者数量来实现更好的性能。

5. 使用 Redis 实现延时任务有什么注意的地方?

在任务时间跨度较大且任务数量众多的场景中,需要特别注意内存管理。大量任务可能会导致内存占用过高,而长时间保存任务则会造成资源浪费。为了解决这些问题,可以结合使用 MySQL 和 Redis 来优化任务管理:

  1. 短期任务:对于延迟时间较短的任务(例如几分钟到几个小时内执行的任务),可以继续存储在 Redis 中,以便快速访问和处理。
  2. 长期任务:对于延迟时间较长的任务(例如几天或几周后执行的任务),则可以存储在 MySQL 中。通过这种方式,可以有效减少 Redis 的内存占用。
  3. 定期扫描:使用定时任务(例如 XXL-JOB 或 Spring Task)定期扫描 MySQL 中即将到期的任务(例如未来 2 小时内到期的任务),并将这些任务推送到 Redis 中进行处理。这种做法可以确保任务在适当的时候被加载到内存中。
  4. 优化查询:在定期扫描 MySQL 时,可能需要处理大量数据。为提高查询效率,可以使用索引或进行分库分表等优化措施。

将 Redis 和 MySQL 结合使用的优势

  1. 节省缓存资源:通过将长期任务存储在 MySQL 中,避免了在 Redis 中存储大量长期任务导致的内存浪费。
  2. 可靠性和成本:MySQL 提供的事务机制可以保证任务数据的可靠性,同时存储成本也相对较低。
  3. 避免大 key 问题:如果仅使用一个 RDelayedQueue,任务数量过大会产生大 key 问题。可以通过将任务按某种逻辑(如时间段、任务类型)分片存储到多个 RDelayedQueue 中来避免这一问题。

通过这种结合使用的方式,既能利用 Redis 的快速访问能力,又能依靠 MySQL 的持久化存储和事务支持,有效地管理大时间跨度和大量的延时任务。

6. 如何使用 Redisson 实现延迟队列

6.1. 基础配置

maven 依赖:

<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.16.2</version>
</dependency>

基础 Redission 配置文件

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://localhost:6379");return Redisson.create(config);}
}

6.2. 延迟队列实战

这里就随便写一个需要频繁修改数据的场景,就例如一个点赞的场景

点赞功能虽然看起来简单,但如果系统流量大,用户频繁点赞,尤其是针对热门内容时,后台需要处理的大量并发请求就会成为性能瓶颈

点赞计数并发处理

  • 并发场景:在点赞的场景中,可能会有很多用户同时对同一个帖子进行点赞。为了避免计数错误(比如两个人同时点赞,只增加了一次的情况),就需要使用分布式锁或原子操作来确保计数的正确性。
  • 解决方案:使用 RAtomicLong 是一个比较好的选择,它提供原子操作,可以确保多个线程或多个分布式节点对同一个点赞计数进行安全的增加或覆盖操作。

缓存

  • 高频访问优化:在点赞场景中,某个内容可能会在短时间内被大量用户点赞。将点赞数保存在 Redis 这样的缓存系统中,可以极大地减轻数据库的压力,提高系统的响应速度。

延迟持久化

  • 持久化问题:每次点赞都立即写入数据库会对数据库产生巨大的压力,尤其是在高并发情况下。因此,通常的策略是将点赞数暂时缓存在 Redis 中,等待合适的时间再批量持久化到数据库。
  • 解决方案:通过 RDelayedQueue 实现延迟处理的功能。在点赞操作发生时,不立即持久化,而是将操作推迟 15 分钟再处理。这一做法既能确保点赞数不丢失,又减少了频繁持久化操作的开销。

使用 RBlockingQueue 代替 RQueue 的好处

  • 避免频繁轮询:在原本的代码中使用了 RQueue,这种队列会需要不断地去轮询,判断是否有新的任务需要处理,这对资源是一种浪费。
  • 阻塞队列的优化:RBlockingQueue 提供了阻塞机制,只有在有新元素到来时才会唤醒队列进行处理,节省了系统资源的消耗,减少不必要的 CPU 轮询开销。

public Long likeIncrementCount(String postId, Long directLikeNum, int countStrategy) {String key = LikeCacheKey.Like_COUNT.getKey(postId);RAtomicLong rAtomicLong = redissonClient.getAtomicLong(key);// 初始化操作数,如果 Redis 数据不存if (!rAtomicLong.isExists()) {getLikeNum(postId);}long likeCount;// 根据策略计数switch (countStrategy) {case ACCUMULATION.getType(): // 累加if (directLikeNum == null) {likeCount = rAtomicLong.incrementAndGet();} else {likeCount = rAtomicLong.addAndGet(directLikeNum);}break;case COVER.getType(): // 覆盖if (directLikeNum == null) {throw new IllegalArgumentException("Direct like number cannot be null when using override strategy");}rAtomicLong.set(directLikeNum);likeCount = directLikeNum;break;default: // 默认返回当前值likeCount = rAtomicLong.get();break;}// 设置过期时间rAtomicLong.expire(60, TimeUnit.MINUTES);// 使用RBlockingQueue避免频繁轮询RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(LikeCacheKey.Like_DYNAMIC.getKey());RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);// 如果队列中不包含当前key,则添加到延迟队列中if (!delayedQueue.contains(key)) {// 延迟 10 分钟统计delayedQueue.offerAsync(key, 10, TimeUnit.MINUTES);}return likeCount;
}

接下来就是处理延迟队列

@Slf4j
@Component
@RequiredArgsConstructor
public class LikePersistenceTask implements ApplicationRunner {private final ScheduledExecutorService executorService = Executors.newSingleThreadExecutor();private final LikeService likeService;  private final RedissonClient redissonClient;@Overridepublic void run(ApplicationArguments args) {executorService.submit(this::processLikeData);log.info("启动一个后台线程,用于处理 Redis 点赞统计数据持久化。");}private void processLikeData() {RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(LikeCacheKey.LIKE_DYNAMIC.getKey());while (!Thread.currentThread().isInterrupted()) {try {String key = blockingQueue.take();  // 阻塞直到有元素if (StringUtils.isNotBlank(key)) {processKey(key);}} catch (InterruptedException e) {log.error("处理 Redis 点赞统计数据持久化线程被中断", e);Thread.currentThread().interrupt();  // 恢复中断状态} catch (Exception e) {log.error("处理 Redis 点赞统计数据持久化时发生错误", e);}}}private void processKey(String key) {String[] objs = LikeCacheKey.LIKE_COUNT.parseKeyArg(key);String postId = objs[0];Integer actionType = Integer.valueOf(objs[1]);likeService.persistenceLikeData(postId, actionType);}@PreDestroypublic void shutdown() {log.info("正在关闭 LikePersistenceTask 线程池...");executorService.shutdown();try {if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {executorService.shutdownNow();if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {log.error("线程池未能在指定时间内终止");}}} catch (InterruptedException ie) {executorService.shutdownNow();Thread.currentThread().interrupt();}}
}

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

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

相关文章

『功能项目』Unity本地数据库读取进入游戏【29】

本章项目成果展示 打开上一篇28Unity连接读取本地数据库的项目&#xff0c; 本章要做的事情是通过读取本地数据库登录进入游戏场景 首先创建一个脚本文件夹&#xff1a; 新建脚本&#xff1a;MySqlAccess.cs 编写脚本&#xff1a;MySqlAccess.cs using UnityEngine; using MyS…

Java | Leetcode Java题解之第390题消除游戏

题目&#xff1a; 题解&#xff1a; class Solution {public int lastRemaining(int n) {int a1 1;int k 0, cnt n, step 1;while (cnt > 1) {if (k % 2 0) { // 正向a1 a1 step;} else { // 反向a1 (cnt % 2 0) ? a1 : a1 step;}k;cnt cnt >> 1;step s…

【二等奖成品论文】2024年数学建模国赛B题25页成品论文+完整matlab代码、python代码等(后续会更新)

您的点赞收藏是我继续更新的最大动力&#xff01; 一定要点击如下的卡片&#xff0c;那是获取资料的入口&#xff01; 【全网最全】2024年数学建模国赛B题31页完整建模过程25页成品论文matlab/python代码等&#xff08;后续会更新「首先来看看目前已有的资料&#xff0c;还会…

python画图|并列直方图绘制

前述学习过程中&#xff0c;已经知晓普通直方图绘制和堆叠直方图绘制&#xff0c;参考链接如下&#xff1a; 西猫雷婶-CSDN博客 有时候&#xff0c;我们还会遇到并列直方图绘制的需求&#xff0c;今天就探索一下。 【1】官网教程 按照惯例&#xff0c;我们先来到官网&#…

MySQL数据库的介绍

目录 1.什么是MySQL数据库 2.MySQL数据库的设计 MySQL的进一步认识 MySQL的客户端 —— mysql MySQL的服务端 —— mysqld 3.MySQL数据库的架构 MySQL架构图 连接层 服务层 存储引擎层 文件系统层 4.MySQL的存储引擎 认识存储引擎 MySQL中的存储引擎 存储引擎之…

JWT生成、解析token

目录 1. 导入JWT相关依赖2. JWT生成token3. JWT解析token4. 测试结果5. JWT加密、解密工具类 1. 导入JWT相关依赖 <!-- jwt认证模块--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><versio…

Docker中部署nacos 开启鉴权springboot连接配置

nacos开启鉴权后发现各种连不上。 按道理说所有的东西都是采用同一个docker网络连接的&#xff0c;连接的时候可以采用容器名连接。 下面是刚开始springboot中的链接配置。增加了用户名和密码 这里nacos我们用到了注册中心和配置中心。启动项目的时候配置中心没有问题&#x…

企业选ETL还是ELT架构?

作为数据处理的重要工具&#xff0c;ETL工具被广泛使用&#xff0c;同时ETL也是数据仓库中的重要环节。本文将从解释ETL工具是怎么处理数据&#xff0c;同时介绍ELT和ETL工具在企业搭建数据仓库的重要优势。 一、什么是ETL? ETL是Extract-Transform-Load的缩写&#xff0c;将…

RabbitMQ 应用

文章目录 前言1. Simple 简单模式2. Work Queue 工作队列模式3. Pubulish/Subscribe 发布/订阅模式Exchange 的类型 4. Routing 路由模式5. Topics 通配符模式6. RPC RPC通信7. Publisher Confirms 发布确认1. 单独确认2. 批量确认3. 异步确认 前言 前面我们学习了 RabbitMQ 的…

数据结构--串的模式匹配算法

文章目录 串的模式匹配算法1.朴素算法&#xff08;Brute-Force(BF)暴力算法&#xff09;BF算法分析 2.KMP算法字符串的最长公共前后缀部分匹配表&#xff08;前缀表&#xff09;Next 串的模式匹配算法 查找子串&#xff08;模式串&#xff09;在主串中的位置的操作通常称为串的…

《OpenCV计算机视觉》—— 图像形态学(腐蚀、膨胀等)

文章目录 一、图像形态学基本概念二、基本运算1.简单介绍2.代码实现 三、高级运算1.简单介绍2.代码实现 一、图像形态学基本概念 图像形态学是图像处理科学的一个独立分支&#xff0c;它基于集合论和数学形态学的理论&#xff0c;专门用于分析和处理图像中的形状和结构。图像形…

基于YOLOv10的垃圾检测系统

基于YOLOv10的垃圾检测系统 (价格90) 包含 [CardBoard, Glass, Metal, Paper, Plastic] 5个类 [纸板, 玻璃, 金属, 纸张, 塑料] 通过PYQT构建UI界面&#xff0c;包含图片检测&#xff0c;视频检测&#xff0c;摄像头实时检测。 &#xff08;该系统可以根据数据训练出的…

Spring之Bean的生命周期 2024-9-6 19:47

目录 什么是Bean的生命周期为什么要知道Bean的生命周期Bean的生命周期之5步Bean生命周期之7步Bean生命周期之10步 声明&#xff1a;本章博客内容采自老杜2022spring6 语雀文档 什么是Bean的生命周期 Spring其实就是一个管理Bean对象的工厂。它负责对象的创建&#xff0c;对象的…

webpack+lite-server 构建项目示例

首先安装以下库 npm install --save-dev webpack webpack-cli lite-server npm install --save-dev babel-loader babel/core babel/preset-env项目结构 webpack.config.js 配置 const path require("path");module.exports {entry: "./src/index.js",…

数据分析-12-多个时间序列数据的时间戳对齐以及不同的方式补点

参考python时间序列数据的对齐和数据库的分批查询 1 问题场景与分析 1.1 场景 在医院的ICU里,须要持续观察病人的各项生命指标。这些指标的采集频率每每是不一样的(例如有些指标隔几秒采集一个,有些几个小时采集一个,有些一天采集一个),并且有些是按期的,有些是不按期的…

SenseGlove机器臂遥操作控制:技术优势与高危作业安全保障

在追求高效与安全的工业时代&#xff0c;高危作业任务始终是行业发展的一大障碍。SenseGlove力反馈手套机器臂遥操作应用案例的出现&#xff0c;凭借其独特的技术优势&#xff0c;为解决这一难题提供了创新性解决方案。 一、技术优势 高精度的力反馈技术&#xff1a;SenseGlove…

传统CV算法——特征匹配算法

Brute-Force蛮力匹配 Brute-Force蛮力匹配是一种简单直接的模式识别方法&#xff0c;经常用于计算机视觉和数字图像处理领域中的特征匹配。该方法通过逐一比较目标图像中的所有特征点与源图像中的特征点来寻找最佳匹配。这种方法的主要步骤包括&#xff1a; 特征提取&#xff…

设计模式之装饰器模式:让对象功能扩展更优雅的艺术

一、什么是装饰器模式 装饰器模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff08;Structural Pattern&#xff09;&#xff0c;它允许用户通过一种灵活的方式来动态地给一个对象添加一些额外的职责。就增加功能来说&#xff0c;装饰器模式相比使用…

使用html+css+layui实现动态表格组件

1、概述 需求&#xff0c;表格第一列指标可配置通过后端api传进来&#xff0c;表格显示数据以及鼠标触摸后气泡弹出层提示信息都是从后端传过来&#xff0c;实现动态表格的组件&#xff01;&#xff01;实现效果如下&#xff1a; 接口标准数据格式如下&#xff1a; {"da…

自动驾驶---什么是Frenet坐标系?

1 背景 为什么提出Frenet坐标系&#xff1f;Frenet坐标系的提出主要是为了解决自动驾驶系统在路径规划的问题&#xff0c;它基于以下几个原因&#xff1a; 符合人类的驾驶习惯&#xff1a; 人类驾驶员在驾驶过程中&#xff0c;通常不会关心自己距离起点的横向和纵向距离&#x…