spring-boot 整合 redisson 实现延时队列(文末有彩蛋)

应用场景

通常在一些需要经历一段时间或者到达某个指定时间节点才会执行的功能,比如以下这些场景:

  • 订单超时提醒
  • 收货自动确认
  • 会议提醒
  • 代办事项提醒

为什么使用延时队列

对于数据量小且实时性要求不高的需求来说,最简单的方法就是定时扫描数据库。

但是,当数量达到数百万、上千万级别且时,定时扫库就显得非常低效且消耗资源,

甚至有些时间间隔小实时性要求高的情况,上一次扫描还没结束,下一次就又开始了,

这时候如果使用延时队列就会比较合适

延时队列的几种方式:

  • Quartz 定时任务实现扫库
  • DelayQueue JDK中提供了一组实现延迟队列的API
  • Redis sorted set
  • Redis 过期键监听回调
  • RabbitMQ 死信队列
  • RabbitMQ 基于插件实现延迟队列
  • Wheel 时间轮训算法

Redisson 实现延时队列

顾名思义 Redis son 就是 Redis 的儿子,举个栗子先:

1.引入 pom

<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>${lastest.version}</version>
</dependency>

2.封装一个 RedissonQueue 类

@Service
public class RedissonQueue {public static final String QUEUE = "delayQueue";// 默认超时时间,30秒public static final Integer DEFAULT_TIMEOUT = 30;@Resourceprivate RedissonClient redissonClient;// 加入任务并设置到期时间public void offer(String taskId, Integer timeout) {RDelayedQueue<String> delayedQueue = delayedQueue();delayedQueue.offer(taskId, Objects.isNull(timeout) ? DEFAULT_TIMEOUT : timeout, TimeUnit.SECONDS);}// 移除任务public void remove(String taskId) {RDelayedQueue<String> delayedQueue = delayedQueue();delayedQueue.removeIf(messageId -> messageId.equals(taskId));}// 任务列表public RDelayedQueue<String> delayedQueue() {RBlockingDeque<String> blockingDeque = blockingDeque();return redissonClient.getDelayedQueue(blockingDeque);}public RBlockingDeque<String> blockingDeque() {return redissonClient.getBlockingDeque(QUEUE);}public boolean isShutdown() {return redissonClient.isShutdown();}public void shutdown() {redissonClient.shutdown();}}

3.交给 Spring 管理

@Slf4j
@Service
public class RedissonService implements ApplicationRunner {@Resourceprivate RedissonQueue redissonQueue;@Resource(name = "threadPoolTaskExecutor")private ThreadPoolTaskExecutor executor;@Overridepublic void run(ApplicationArguments args) {RBlockingDeque<String> blockingDeque = redissonQueue.blockingDeque();executor.execute(() -> {while (true) {if (redissonQueue.isShutdown()) {return;} else {String messageId = null;try {messageId = blockingDeque.take();} catch (InterruptedException e) {log.warn("RedissonConsumer error:{}", e.getMessage());}if (!Objects.isNull(messageId) && !messageId.isEmpty()) {log.warn("timeout messageId : {}", messageId);}}}});}// 初始化,启动服务就执行一次@PostConstructpublic void init() {redissonQueue.delayedQueue();}@PreDestroypublic void shutdown() {redissonQueue.shutdown();}}

4.测试接口

@Operation(summary = "添加任务", description = "添加任务")
@PostMapping
public ResponseEntity<?> add(@RequestParam(value = "taskId", required = false) String taskId,@RequestParam(value = "timeout", required = false) Integer timeout) {taskId = StringUtils.isEmpty(taskId) ? String.valueOf(snowflake.nextId()) : taskId;redissonQueue.offer(taskId, timeout);return ResponseEntity.ok().body(redissonQueue.delayedQueue());
}@Operation(summary = "移除任务", description = "移除任务")
@DeleteMapping(value = "/{taskId}")
public ResponseEntity<?> remove(@PathVariable("taskId") String taskId) {redissonQueue.remove(taskId);return ResponseEntity.ok().body(redissonQueue.delayedQueue());
}

5.测试结果

添加10个任务

在这里插入图片描述

删除第1个任务

在这里插入图片描述

可以看到第一个任务删除后没有被执行(没有设置到期时间,默认为30秒到期)

在这里插入图片描述

实现原理

  • redisson_delay_queue_timeout:delayQueue,sorted set 数据类型,存放所有延迟任务,按延迟任务的到期时间戳(提交任务时间戳 +
    延迟时间)排序,所以列表最前面第一个元素就是整个延迟队列中最早被执行的任务。
  • redisson_delay_queue:delayQueue,list 数据类型,也是存放所有任务。
  • delayQueue,list 数据类型,被称为目标队列,这个里面存放的任务都是已经到延迟时间的,可以被消费者获取的任务,所以上面示例中
    RBlockingQueue 的 take 方法是从此目标队列中获取任务的。
  • redisson_delay_queue_channel:delayQueue,是一个 channel,用来通知客户端开启一个延迟任务
  • 生产者提交任务时将任务放到 redisson_delay_queue_timeout:delayQueue 中,提交任务的时间戳+延迟时间
  • 客户端会有一个延迟任务,这个延迟任务会向 Redis Server 发送一段 lua 脚本,Redis 执行 lua 脚本中的命令,此操作是原子性的

lua 脚本主要干两件事

  • 将到了延迟时间的任务从 redisson_delay_queue_timeout:delayQueue 中移除,存到 delayQueue 这个目标队列
  • 获取到 redisson_delay_queue_timeout:delayQueue 中最早到期时间的任务的到期时间戳,发布到 redisson_delay_queue_channel:
    delayQueue channel 中

当客户端监听到 redisson_delay_queue_channel:delayQueue 这个 channel 的消息时,会再次提交一个客户端延迟任务,延迟时间就是消息(最早到期时间任务的到期时间戳)当前时间戳
这个时间其实也就是 redisson_delay_queue_channel:delayQueue 中最早到期时间的任务的剩余的延迟时间。
一旦时间来到最早到期时间任务的到期时间戳,redisson_delay_queue_timeout:delayQueue 中最早到期时间的任务已经到期,客户端的延迟任务也同时到期,
于是开始执行 lua 脚本操作,及时将到期任务放到目标队列中。然后再次发布剩余的延迟任务中最早到期任务的到期时间戳到 channel
中,
如此循环运行下去,保证 redisson_delay_queue_timeout:delayQueue 中到期数据能及时放到目标队列中。
这里存在一个特殊情况,需要项目启动时就执行一次延时队列。因为由于没有客户端延迟任务的执行,
可能会出现 redisson_delay_queue_timeout:delayQueue 队列中有到期但是没有被放到目标队列的可能,启动就执行一次是为了保证到期的数据能被及时放到目标队列中。

结论

  • Redisson 方案理论上没有延迟,但当消息数量剧增,消费者消费缓慢这种情况下,可能会导致延迟任务消费的延迟。

  • 消息丢失问题 Redisson 方案最大程度上减轻消息丢失的可能性,因为所有任务都是存在 list 和 sorted set 两种数据类型中,Redis
    有持久化机制。除非整个 redis 集群宕机,可能丢失一小部分数据。

  • 广播任务问题,是不会出现的,因为每个客户端都是从同一个目标队列中获取任务。

Redisson 这种实现方案是比较合适且靠谱的,一般中小型项目建议用 Redisson 实现延迟队列,规模较大的项目直接上 MQ。

整合DEMO仓库地址

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

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

相关文章

使用Pandas读取Excel文件将特定列转成str格式方法汇总

文章目录 读取Excel文件并确保列为字符串类型使用 dtype 参数使用 converters 参数 读取Excel文件的正确拼写示例&#xff1a;读取Excel文件并过滤包含特定值的行详细解释 读取Excel文件并确保列为字符串类型 正确的方法是使用 pd.read_excel 函数&#xff0c;并指定 dtype 或…

Webserver笔记

代码随想录的项目 LogStream.h // 返回data_ char数组的数据末尾地址 const char* end() const { return data_ sizeof data_; }Logging.cpp //定义一个 struct timeval 类型的变量 tv&#xff0c;用于存储当前的时间信息。 //定义一个 time_t 类型的变量 time&#xff0c;用…

语音合成-TTS文字转语音(专业版)

语音合成-TTS文字转语音(专业版) 一、工具简介 *使用强大的智能AI语音库&#xff0c;合成独具特色接近真人语音的朗读音频。 *使用极具表现力和类似人类的声音&#xff0c;使文本阅读器和已启用语音的助理等方案栩栩如生。 *用途&#xff1a;这个语音工具&#xff0c;不仅可…

【C语言初阶】C语言数组基础:从定义到遍历的全面指南

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;C语言 “ 登神长阶 ” &#x1f921;往期回顾&#x1f921;&#xff1a;C语言函数 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀数组 &#x1f4d2;1. 什么是数组…

HTTP状态码(HTTP Status Code)讲解

HTTP状态码&#xff08;HTTP Status Code&#xff09;是用以表示网页服务器超文本传输协议响应状态的3位数字代码。它由RFC 2616规范定义&#xff0c;并得到多个RFC规范的扩展。状态码告知客户端请求的处理结果及状态&#xff0c;有助于开发者定位和解决问题。 HTTP状态码分为…

HTTP请求与响应:Python爬虫技术解析

引言 在Web开发和数据抓取中&#xff0c;理解HTTP协议是至关重要的。HTTP&#xff08;超文本传输协议&#xff09;是用于从网络传输超文本到本地浏览器的标准协议。它定义了客户端与服务器之间请求和响应的格式。本文将从HTTP请求和响应的基本结构开始&#xff0c;逐步深入到如…

【C++】学习笔记——AVL树

文章目录 十六、AVL树1. AVL树的概念2. AVL树节点的定义3. AVL树的插入4. AVL树的旋转5. AVL树的验证6. 完整代码测试7. AVL树的性能 未完待续 十六、AVL树 1. AVL树的概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&…

【机器学习】无监督学习和自监督学习

1. 什么是机器学习 机器学习是一种使计算机系统能够从数据中学习并做出预测或决策的技术和科学领域。它不需要显式地编程来执行特定任务&#xff0c;而是通过使用算法来分析数据和识别模式&#xff0c;以此“学习”如何做出准确的预测或决策。 以下是机器学习的几个关键点&…

【JS逆向课件:第七课:模块与包】

模块与包 模块 模块介绍 在计算机程序的开发过程中&#xff0c;随着程序代码越写越多&#xff0c;在一个文件里代码就会越来越长&#xff0c;越来越不容易维护。 为了编写可维护的代码&#xff0c;我们把很多函数分组&#xff0c;分别放到不同的文件里&#xff0c;这样&…

前端基础之JavaScript学习——函数的使用

大家好我是来自CSDN的前端寄术区博主PleaSure乐事&#xff0c;今天我们继续有关JavaScript的学习&#xff0c;使用的编译器为vscode&#xff0c;浏览器为谷歌浏览器。 函数的声明与使用 声明 在JavaScript当中函数的声明和其他语言类似&#xff0c;使用如下格式即可声明&…

实战篇(十):使用Processing创建可爱花朵:实现随机位置、大小和颜色的花朵

使用Processing创建可爱花朵 0.效果预览1. 引言2. 设置Processing环境3. 创建花朵类4. 实现花瓣绘制5. 绘制可爱的笑脸6. 鼠标点击生成花朵7. 完整代码8. 总结与扩展0.效果预览 在本教程中,我们将使用Processing编程语言来创建一个可爱的花朵生成器。通过封装花朵为一个类,并…

大语言模型-检索测评指标

1. MRR &#xff08;Mean Reciprocal Rank&#xff09;平均倒数排名&#xff1a; 衡量检索结果排序质量的指标。 计算方式&#xff1a; 对于每个查询&#xff0c;计算被正确检索的文档的最高排名的倒数的平均值&#xff0c;再对所有查询的平均值取均值。 意义&#xff1a; 衡量…

Context使用

Context API 是 React 提供的一种用于跨组件层级共享数据的方法&#xff0c;它可以用来实现兄弟组件之间的通信。通常情况下&#xff0c;兄弟组件之间的通信需要通过它们的共同父组件来实现&#xff0c;而 Context API 则可以帮助我们避免将数据逐层传递到每一个中间组件。 实…

京准:GPS北斗卫星授时信号安全隔离防护装置

京准&#xff1a;GPS北斗卫星授时信号安全隔离防护装置 京准&#xff1a;GPS北斗卫星授时信号安全隔离防护装置 1、主要特点 ★信号加固功能&#xff1a; GPS/BDS单系统信号拒止情况下&#xff08;包含受到GPS L1欺骗干扰、GPS L1压制干扰、BDS B1欺骗干扰、BDS B1压制干扰&…

【C++】类和对象(下):初始化列表、类型转换、友元

目录 一.初始化列表 二.类型转换 三.static成员 四.友元 五.内部类 六.匿名对象 一.初始化列表 之前在实现构造函数的时候&#xff0c;初始化成员变量主要是使用函数体内赋值的方法&#xff0c;构造函数初始化还有另外一种方式&#xff1a;初始化列表。使用方式是以一个…

【STM32】按键控制LED光敏传感器控制蜂鸣器(江科大)

一、按键控制LED LED.c #include "stm32f10x.h" // Device header/*** 函 数&#xff1a;LED初始化* 参 数&#xff1a;无* 返 回 值&#xff1a;无*/ void LED_Init(void) {/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENAB…

C语言习题~day32

请问该程序的输出是多少&#xff08;&#xff09; #include<stdio.h> int main(){ unsigned char i 7; int j 0; for(;i > 0;i - 3){ j; } printf("%d\n", j); return 0; }A.2 B.死循环 C.173 D.172 无符号字符型的取值范围是 0 到 255。 第一次循环…

199.二叉树的右视图(DFS)

给定一个二叉树的根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 示例 1: 输入: [1,2,3,null,5,null,4] 输出: [1,3,4] 示例 2: 输入: [1,null,3] 输出: [1,3] 示例 3: 输入: [] 输出: [] 解题…

Flutter 中的基本数据类型:num、int 和 double

在 Dart 编程语言中&#xff0c;数值类型的基础是 num&#xff0c;而 int 和 double 则是 num 的子类型。在开发 Flutter 应用时&#xff0c;理解这三者的区别和使用场景是非常重要的。本文将详细介绍 num、int 和 double 的定义及其使用区别。 num num 是 Dart 中的数值类型…

实战:springboot用LocalDateTime快速替换Date

概叙 实战&#xff1a;早点用JDK8中的java.time来替换java.util.Date-CSDN博客 在Spring Boot项目中大家从Date升级到LocalDateTime最关心以下两个问题&#xff1a; 使用LocalDateTime类型字段作为接口出入参数&#xff0c;能正常映射转换前端传入的参数吗&#xff1f;返回参…