Spring Boot中使用Redis和Lua脚本实现延时队列

news2024/11/24 15:31:32
码到三十五 : 个人主页

延时队列是一种常见的需求。延时队列允许我们延迟处理某些任务,这在处理需要等待一段时间后才能执行的操作时特别有用,如发送提醒、定时任务等。文中,将介绍如何在Spring Boot环境下使用Redis和Lua脚本来实现一个延时队列。

目录

    • 一、延迟队列的四大使用场景
    • 二、如何利用ZSet实现延迟队列
    • 三、实现步骤
    • 四、实现代码
    • 五、使用ZSet实现延迟队列的缺陷
    • 六、替代实现方案
      • 结语

一、延迟队列的四大使用场景

  1. 订单超时自动处理
    在电商领域,延迟队列对于处理订单超时问题至关重要。一旦用户下单,订单信息便进入延迟队列,并预设超时时长。若用户在此时间内未完成支付,订单信息将由消费者从队列中提取,并执行如取消订单、库存释放等后续操作,高效且自动化。

  2. 优惠券到期温馨提醒
    借助延迟队列,我们可以实现优惠券到期前的温馨提醒服务。将临近过期的优惠券信息入队,并设定精确延迟时间。时间一到,系统自动提醒用户优惠券的到期日,引导他们及时享用优惠,提升用户体验。

  3. 智能消息重试策略
    在处理网络请求失败、数据库异常等情况时,延迟队列提供了智能的消息重试机制。当消息初次处理失败,它会被置入队列并设定重试延时。延时结束后,系统会再次尝试处理,确保消息的可靠传递与处理。

  4. 异步通知与定时提醒
    延迟队列还能用于实现异步通知和定时提醒功能。用户完成操作后,系统将相关通知信息加入队列,并设定发送延时,确保在最佳时机向用户推送通知,既不打扰用户,又能保持信息的时效性。

二、如何利用ZSet实现延迟队列

Redis的ZSet(有序集合)是一个根据分数对唯一字符串成员进行排序的数据结构。在多个成员分数相同时,它们会按照字典顺序进行排列。ZSet不仅常用于排行榜和限速器等场景,还可巧妙用于实现延迟队列。

在这里插入图片描述

基于ZSet的延迟队列实现原理,主要利用了其有序性和按分数排序的特点。以下是具体实现步骤的简要介绍:

  1. 定义延迟消息:在ZSet中,我们将延迟消息作为成员,而其对应的延迟时间则作为该成员的分数。这里的延迟时间通常是一个未来的时间戳,它指明了消息应当被处理的确切时刻。

  2. 消息入队:使用ZADD命令,我们可以轻松地将消息添加到ZSet中,并为其指定相应的延迟时间作为分数。

  3. 定期检查:通过定期轮询ZSet,我们可以利用ZRANGEBYSCORE命令来检索那些分数(即延迟时间)小于或等于当前时间戳的消息,这些消息即为到期的、需要被处理的消息。

  4. 消息处理与出队:一旦找到到期的消息,我们可以使用ZPOPMIN命令将它们从ZSet中移除,并进行相应的处理。在处理过程中,需要考虑并发性和数据一致性问题,确保每条消息都能被正确处理且不会被重复处理。

  5. 后续操作与通知:为了提高系统的性能和可靠性,我们可以结合Redis的Pub/Sub机制。在处理完消息后,发布一个事件来通知其他服务或订阅者进行后续的操作或处理。

通过这种方式,ZSet能够有效地按照消息的延迟时间顺序,逐个取出并处理到期的消息,从而实现了一个高效且可靠的延迟队列系统。

三、实现步骤

在这里插入图片描述

在Spring Boot环境下,实现一个基于Redis和Lua脚本的延时队列,需要以下几个步骤:

  1. 环境准备

    • 安装并启动Redis服务器。
    • 在Spring Boot项目中添加spring-boot-starter-data-redis依赖。
  2. Redis数据结构选择

    • 使用Redis的zset(有序集合)数据结构来存储延时任务。zset中的元素是唯一的,但分数(score)可以相同,可以用作任务的延迟时间戳。
  3. Lua脚本编写

    • 编写一个Lua脚本来处理队列的出队和入队操作,以确保操作的原子性。
  4. Spring Boot应用配置

    • 配置Redis连接工厂和Redis模板。
  5. 实现延时队列服务

    • 提供一个服务来管理延时队列,包括入队、出队、检查并处理到期的任务等。
  6. 定时任务调度

    • 使用Spring的@Scheduled注解或者Redis的键空间通知来定期检查并处理到期的任务。

四、实现代码

下面是一个简化版本的实现:

1. 添加Maven依赖

pom.xml中添加spring-boot-starter-data-redis依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 配置Redis

application.ymlapplication.properties中配置Redis连接信息:

spring:
  redis:
    host: localhost
    port: 6379

3. Lua脚本
定义一个Lua脚本原子性地执行出队操作。脚本使用Redis的有序集合命令来查找并移除到期的任务:

-- KEYS[1] 延时队列的key
-- ARGV[1] 当前时间戳
-- 返回值:任务ID(如果存在)或nil
local key = KEYS[1]
local currentTime = tonumber(ARGV[1])
local task = redis.call('zrangebyscore', key, 0, currentTime, 'LIMIT', 0, 1)
if #task > 0 then
    redis.call('zremrangebyscore', key, 0, currentTime)
    return task[1]
else
    return nil
end

可以稍微优化一下上面的Lua脚本,以减少不必要的操作和提高效率:

-- KEYS[1] 延时队列的key
-- ARGV[1] 当前时间戳
-- 返回值:任务ID(如果存在)或nil

local key = KEYS[1]
local currentTime = tonumber(ARGV[1])

-- 使用zrangebyscore和zrem的组合命令zpopmin,它原子性地返回并移除分数最低的元素
-- zpopmin命令(5.0及以上版本)
local task = redis.call('zpopmin', key, 1, 'BLOCK', 0, 'SCORES')

-- zpopmin返回的是一个包含两个元素的数组,第一个元素是分数,第二个是成员
if task and #task > 0 and task[2] and tonumber(task[1]) <= currentTime then
    return task[2] -- 返回任务ID
else
    return nil
end

注意:

  1. zpopmin命令是一个原子性的操作,它返回并删除分数最低的元素。避免了先查询后删除可能带来的并发问题。zpopmin`命令在Redis 5.0及以上版本中可用。

  2. zpopmin命令可以设置阻塞时间,这里设置为0,表示不阻塞。如果希望在没有可用元素时阻塞等待一段时间,可以调整这个值。

  3. 脚本检查了返回的分数是否小于等于当前时间戳,以确保只处理到期的任务。

  4. 如果Redis版本低于5.0zpopmin将不可用,可以使用zrangebyscorezrem的组合,但需要注意并发问题。

4. 实现延时队列服务

@Service
public class DelayQueueService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private static final String DELAY_QUEUE_KEY = "delay_queue";

    // 入队操作
    public void enqueue(String taskId, long delayInSeconds) {
        long score = System.currentTimeMillis() / 1000 + delayInSeconds;
        stringRedisTemplate.opsForZSet().add(DELAY_QUEUE_KEY, taskId, score);
    }

    // 出队操作,使用Lua脚本确保原子性
    public String dequeue() {
        String luaScript = "..."; // 上面定义的Lua脚本内容
        RedisScript<String> script = RedisScript.of(luaScript, String.class);
        long currentTime = System.currentTimeMillis() / 1000;
        return stringRedisTemplate.execute(script, Collections.singletonList(DELAY_QUEUE_KEY), String.valueOf(currentTime));
    }
}

5. 定时任务调度

@Component
public class DelayQueueScheduler {
    @Autowired
    private DelayQueueService delayQueueService;
    private static final long POLLING_INTERVAL = 1000; // 检查间隔1秒

    @Scheduled(fixedRate = POLLING_INTERVAL)
    public void pollAndProcess() {
        String taskId = delayQueueService.dequeue();
        if (taskId != null) {
            // 处理任务逻辑,例如调用某个服务或者方法等。
            System.out.println("Processing task: " + taskId);
        }
    }
}

五、使用ZSet实现延迟队列的缺陷

虽然Redis的ZSet能满足一些简单场景的延迟队列需求,但也存在一些明显的缺陷。

  1. 资源空转问题
    延迟任务的时间分布往往是不均匀的。在某些时段,可能会有大量的任务需要处理,而在其他时段则可能几乎没有任务。这种情况下,如果系统持续检查ZSet以寻找到期任务,那么在任务稀少或无任务的时段,系统会处于空转状态,这无疑是对计算资源的浪费。

  2. 性能瓶颈
    当延迟消息数量众多时,不断地轮询整个ZSet以查找到期消息会对性能产生显著影响。特别是当任务数量庞大且到期时间分散时,范围查询的开销会变得尤为突出。此外,如果多个任务同时到期且回调函数执行效率低下,还可能导致延迟处理中心的性能下降,进而引发连锁反应,影响到后续任务的及时处理。

  3. 时间精度问题
    ZSet使用浮点数作为分数来排序元素,这在某些需要高精度时间控制的场景中可能不够用。同时,Redis实例的故障、重启或时钟回拨等问题都可能影响到延迟事件处理的准确性。

六、替代实现方案

  1. 状态即时校验
    在某些业务流程中,可以通过即时校验当前状态与应有状态的方式来替代延迟队列。但这种方法更适用于工单等可以持续校验的业务场景,对于一次性的延迟通知任务则不太适用。

  2. 利用消息中间件的延迟消息功能
    像RocketMQ和RabbitMQ这样的消息中间件提供了延迟消息的功能。例如,RocketMQ在商业版本中支持自定义时长的延迟消息。

  3. 数据库轮询
    通过定期轮询数据库中的业务单据表或专门的延迟事件表来处理过期任务。但这种方法可能会对业务数据库和服务造成性能负担,且轮询的时间间隔难以精确把控。

  4. 时间轮算法
    时间轮算法是一种有效的处理定时任务的方法。但为了实现持久化和避免任务丢失,需要结合Redis或关系数据库来存储延迟任务。在服务启动时,需要将存储的延迟任务加载到时间轮中,并在任务过期后更新任务状态,以防止重复执行或加载。

结语

通过使用Redis和Lua脚本,可以在Spring Boot环境中实现一个高效且可靠的延时队列系统。这种方法利用了Redis的有序集合数据结构和Lua脚本的原子性操作来确保任务的正确性和一致性。通过定期调度任务来处理到期的任务,可以实现各种需要延迟执行的操作,如发送提醒、执行定时任务等。



听说...关注下面公众号的人都变牛了,纯技术,纯干货 !

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

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

相关文章

【JAVA基础之反射】反射详解

&#x1f525;作者主页&#xff1a;小林同学的学习笔录 &#x1f525;mysql专栏&#xff1a;小林同学的专栏 1.反射 1.1 概述 是在运行状态中&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所有属性和方法&#xff1b; 对于任意一个对象&#xff0c;都能够调用它…

Edge 浏览器键入时如何关闭显示搜索和站点建议

Edge 浏览器键入时如何关闭显示搜索和站点建议

ssm104园区停车管理系统+jsp

园区停车管理系统的设计与实现 摘 要 网络技术和计算机技术发展至今&#xff0c;已经拥有了深厚的理论基础&#xff0c;并在现实中进行了充分运用&#xff0c;尤其是基于计算机运行的软件更是受到各界的关注。加上现在人们已经步入信息时代&#xff0c;所以对于信息的宣传和管…

基于SSM SpringBoot vue宾馆网上预订综合业务服务系统

基于SSM SpringBoot vue宾馆网上预订综合业务服务系统 系统功能 首页 图片轮播 宾馆信息 饮食美食 休闲娱乐 新闻资讯 论坛 留言板 登录注册 个人中心 后台管理 登录注册 个人中心 用户管理 客房登记管理 客房调整管理 休闲娱乐管理 类型信息管理 论坛管理 系统管理 新闻资讯…

【VueUse】超越基本功能的高级 Vue 元素操作

在vue开发中我们经常需要操作DOM元素&#xff0c;从简单的添加类到动态创建元素&#xff0c;这些操作都是不可避免的。而在VueUse库中&#xff0c;Elements相关API函数为我们提供了一系列强大而灵活的工具&#xff0c;帮助我们更轻松地处理DOM元素。无论是优雅地处理元素、动态…

25计算机考研院校数据分析 | 哈尔滨工业大学

哈尔滨工业大学&#xff08;Harbin Institute of Technology&#xff09;&#xff0c;简称哈工大&#xff0c; 校本部位于黑龙江省哈尔滨市&#xff0c;是由工业和信息化部直属的全国重点大学&#xff0c;位列国家“双一流”、“985工程”、“211工程”&#xff0c;九校联盟 、…

数据结构与算法之经典排序算法

一、简单排序 在我们的程序中&#xff0c;排序是非常常见的一种需求&#xff0c;提供一些数据元素&#xff0c;把这些数据元素按照一定的规则进行排序。比如查询一些订单按照订单的日期进行排序&#xff0c;再比如查询一些商品&#xff0c;按照商品的价格进行排序等等。所以&a…

ServiceNow 研究:通过RAG减少结构化输出中的幻觉

论文地址&#xff1a;https://arxiv.org/pdf/2404.08189 原文地址&#xff1a;rag-hallucination-structure-research-by-servicenow 在灾难性遗忘和模型漂移中&#xff0c;幻觉仍然是一个挑战。 2024 年 4 月 18 日 灾难性遗忘&#xff1a; 这是在序列学习或连续学习环境中出现…

Costas-Barker序列模糊函数仿真

文章目录 前言一、Costas 序列二、Barker 码三、Costas-Barker 序列模糊函数仿真1、MATLAB 核心代码2、仿真结果①、Costas-Barker 模糊函数图②、Costas-Barker 距离模糊函数图③、Costas-Barker 速度模糊函数图 四、资源自取 前言 Costas 码是一种用于载波同步的频率调制序列…

20232810 2023-2024-2 《网络攻防实践》实验七

一、实践内容 &#xff08;1&#xff09;使用Metasploit进行Linux远程渗透攻击 任务&#xff1a;使用Metasploit渗透测试软件&#xff0c;攻击Linux靶机上的Samba服务Usermap_script安全漏洞&#xff0c;获取目标Linux靶机的主机访问权限。实践步骤如下&#xff1a; ①启动Met…

字节跳动发起AI战争 寻找下一个TikTok

现如今在字节跳动&#xff0c;已近乎隐退的张一鸣&#xff0c;只重点关注两件事&#xff1a;其一&#xff0c;是风暴中的TikTok&#xff1b;其二&#xff0c;就是字节跳动正在全力追赶的AI战略业务。 提及字节的AI战略远望,多个接近字节的人士均认为,以Flow部门出品最为“正统…

缩小COCO数据集

在运行YOLOS模型的过程中&#xff0c;需要使用到COCO2017这个数据集&#xff0c;但从实验运行来看&#xff0c;其所需时间无疑是相当漫长&#xff0c;预计可能需要近几十天才能完成&#xff0c;因此便考虑缩小COCO数据集大小&#xff0c;即尽可能在遵循其分布的情况下&#xff…

Unity开发一个FPS游戏之四

在前面的系列中&#xff0c;我已介绍了如何实现一个基本的FPS游戏&#xff0c;这里将继续进行完善&#xff0c;主要是增加更换武器以及更多动作动画的功能。 之前我是采用了网上一个免费的3D模型来构建角色&#xff0c;这个模型自带了一把AR自动步枪&#xff0c;并且自带了一些…

Unity开发微信小游戏(2)分享

目录 1.概述 2.代码 3.示例 4.个人作品 1.概述 这里我们能做有两件事&#xff1a; 1&#xff09;主动发起分享 2&#xff09;监听右上角分享&#xff08;...按钮&#xff0c;发朋友圈也在这里&#xff09; API&#xff1a;官方文档 2.代码 1&#xff09;主动发起分享&…

DHCPv4_CLIENT_ALLOCATING_01: 在其本地物理子网上广播DHCPDISCOVER消息

测试目的&#xff1a; 确保客户端能够在其本地物理子网上广播DHCPDISCOVER消息。 描述&#xff1a; 该测试用例旨在验证DHCP客户端是否能够正确地在其本地物理子网上广播DHCPDISCOVER消息&#xff0c;以便进行IP地址的自动分配。 测试拓扑&#xff1a; 测试步骤&#xff1a…

生产看板:最直观的车间管理方式之一,是马是马户牵出来溜溜。

可视化生产看板在组织工业生产中扮演着重要的角色&#xff0c;它可以提供实时的信息和可视化的数据&#xff0c;帮助团队和管理层更好地监控和管理生产过程。 以下是可视化生产看板在组织工业生产中的作用&#xff1a; 实时监控&#xff1a;可视化生产看板可以显示实时的生产数…

JavaEE初阶-多线程易忘点总结

文章目录 1.PCBPID文件描述符表内存指针状态上下文优先级记账信息tgid 2.线程与进程的区别3.sleep和interrupt方法的关系变量终止线程interrupt方法终止线程 4.线程状态5.出现线程不安全的原因线程在系统中是随即调度&#xff0c;抢占式执行的。多个线程修改同一个变量线程针对…

小白也能微调大模型:LLaMA-Factory使用心得

大模型火了之后&#xff0c;相信不少人都在尝试将预训练大模型应用到自己的场景上&#xff0c;希望得到一个垂类专家&#xff0c;而不是通用大模型。 目前的思路&#xff0c;一是RAG(retrieval augmented generation)&#xff0c;在模型的输入prompt中加入尽可能多的“目标领域…

Linux-管道通信

1. 管道概念 管道&#xff0c;是进程间通信的一种方式&#xff0c;在Linux命令中“ | ”就是一种管道&#xff0c;它可以&#xff0c;连接前一条命令&#xff0c;和后一条命令&#xff0c;把前面命令处理完的内容交给后面&#xff0c;例如 cat filename | grep hello …

富文本编辑器CKEditor4简单使用-07(处理浏览器不支持通过工具栏粘贴问题 和 首行缩进的问题)

富文本编辑器CKEditor4简单使用-07&#xff08;处理浏览器不支持通过工具栏粘贴问题 和 首行缩进的问题&#xff09; 1. 前言——CKEditor4快速入门2. 默认情况下的粘贴2.1 先看控制粘贴的3个按钮2.1.1 工具栏粘贴按钮2.1.2 存在的问题 2.2 不解决按钮问题的情况下2.2.1 使用ct…