基于Rabbitmq和Redis的延迟消息实现

1 基于Rabbitmq延迟消息实现

支付时间设置为30,未支付的消息会积压在mq中,给mq带来巨大压力。我们可以利用Rabbitmq的延迟队列插件实现消息前一分钟尽快处理
在这里插入图片描述
在这里插入图片描述

1.1定义延迟消息实体

由于我们要多次发送延迟消息,因此需要先定义一个记录消息延迟时间的消息体

@Data
public class MultiDelayMessage<T> {
    /**
     * 消息体
     */
    private T data;
    /**
     * 记录延迟时间的集合
     */
    private List<Long> delayMillis;

    public MultiDelayMessage(T data, List<Long> delayMillis) {
        this.data = data;
        this.delayMillis = delayMillis;
    }
    public static <T> MultiDelayMessage<T> of(T data, Long ... delayMillis){
        return new MultiDelayMessage<>(data, CollUtils.newArrayList(delayMillis));
    }

    /**
     * 获取并移除下一个延迟时间
     * @return 队列中的第一个延迟时间
     */
    public Long removeNextDelay(){
        return delayMillis.remove(0);
    }

    /**
     * 是否还有下一个延迟时间
     */
    public boolean hasNextDelay(){
        return !delayMillis.isEmpty();
    }
}

1.2 定义常量,用于记录交换机、队列、RoutingKey等常量

package com.hmall.trade.constants;

public interface MqConstants {
    String DELAY_EXCHANGE = "trade.delay.topic";
    String DELAY_ORDER_QUEUE = "trade.order.delay.queue";
    String DELAY_ORDER_ROUTING_KEY = "order.query";
}

1.3 抽取mq配置到nacos中

spring:
  rabbitmq:
    host: ${hm.mq.host:192.168.150.101} # 主机名
    port: ${hm.mq.port:5672} # 端口
    virtual-host: ${hm.mq.vhost:/hmall} # 虚拟主机
    username: ${hm.mq.un:hmall} # 用户名
    password: ${hm.mq.pw:123} # 密码
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

1.4 定义消息处理器

使用延迟消息处理器发送消息
在这里插入图片描述
在这里插入图片描述

1.3 消息监听与延迟消息再次发送

在这里插入图片描述
在这里插入图片描述

2 延迟消息实现

DelayQueue:基于JVM

Rabbitmq的延迟任务:基于TTL和死信交换机
在这里插入图片描述

2.1 redis的延迟任务:基于zset的去重和排序功能

在这里插入图片描述
1.为什么任务需要存储在数据库中?
延迟任务是一个通用的服务,任何有延迟需求的任务都可以调用该服务,内存数据库的存储是有限的,需要考虑数据持久化的问题,存储数据库中是一种数据安全的考虑

2.为什么使用redis中的两种数据类型,list和zset?

  • 原因一: list存储立即执行的任务,zset存储未来的数据
  • 原因二:任务量过大以后,zset的性能会下降

时间复杂度:执行时间(次数) 随着数据规模增长的变化趋势

  • 操作redis中的list命令LPUSH: 时间复杂度: O(1)
  • 操作redis中的zset命令zadd: 时间复杂度: (Mlog(n))

2.2 设计mybatis映射实体类:

/**
     * 版本号,用乐观锁
     */
    @Version
    private Integer version;

乐观锁支持:
/**
     * mybatis-plus乐观锁支持
     * @return
     */
@Bean
public MybatisPlusInterceptor optimisticLockerInterceptor(){
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
}

2.3 创建task类,用于接收添加任务的参数

@Data
public class Task implements Serializable {

    /**
     * 任务id
     */
    private Long taskId;
    /**
     * 类型
     */
    private Integer taskType;

    /**
     * 优先级
     */
    private Integer priority;

    /**
     * 执行id
     */
    private long executeTime;

    /**
     * task参数
     */
    private byte[] parameters;
    
}

2.4 添加任务

2.4.1 添加任务到数据库中

addTaskToDb(task);修改任务表和日志表

@Autowired
    private TaskinfoMapper taskinfoMapper;

    @Autowired
    private TaskinfoLogsMapper taskinfoLogsMapper;

    /**
     * 添加任务到数据库中
     *
     * @param task
     * @return
     */
    private boolean addTaskToDb(Task task) {

        boolean flag = false;

        try {
            //保存任务表
            Taskinfo taskinfo = new Taskinfo();
            BeanUtils.copyProperties(task, taskinfo);
            taskinfo.setExecuteTime(new Date(task.getExecuteTime()));
            taskinfoMapper.insert(taskinfo);

            //设置taskID
            task.setTaskId(taskinfo.getTaskId());

            //保存任务日志数据
            TaskinfoLogs taskinfoLogs = new TaskinfoLogs();
            BeanUtils.copyProperties(taskinfo, taskinfoLogs);
            taskinfoLogs.setVersion(1);
            taskinfoLogs.setStatus(ScheduleConstants.SCHEDULED);
            taskinfoLogsMapper.insert(taskinfoLogs);

            flag = true;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return flag;
    }

2.4.2 添加任务到redis

addTaskToCache(task);判断任务执行之间是否在现在还是未来五分钟内

@Autowired
    private CacheService cacheService;

    /**
     * 把任务添加到redis中
     *
     * @param task
     */
    private void addTaskToCache(Task task) {

        String key = task.getTaskType() + "_" + task.getPriority();

        //获取5分钟之后的时间  毫秒值
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 5);
        long nextScheduleTime = calendar.getTimeInMillis();

        //2.1 如果任务的执行时间小于等于当前时间,存入list
        if (task.getExecuteTime() <= System.currentTimeMillis()) {
            cacheService.lLeftPush(ScheduleConstants.TOPIC + key, JSON.toJSONString(task));
        } else if (task.getExecuteTime() <= nextScheduleTime) {
            //2.2 如果任务的执行时间大于当前时间 && 小于等于预设时间(未来5分钟) 存入zset中
            cacheService.zAdd(ScheduleConstants.FUTURE + key, JSON.toJSONString(task), task.getExecuteTime());
        }


    }

2.4.3 删除任务

1、删除数据库任务表,更改日志表任务状态
2、删除list或者zset中的任务

在TaskService中添加方法

/**
     * 取消任务
     * @param taskId        任务id
     * @return              取消结果
     */
public boolean cancelTask(long taskId);

/**
     * 取消任务
     * @param taskId
     * @return
     */
@Override
public boolean cancelTask(long taskId) {

    boolean flag = false;

    //删除任务,更新日志
    Task task = updateDb(taskId,ScheduleConstants.EXECUTED);

    //删除redis的数据
    if(task != null){
        removeTaskFromCache(task);
        flag = true;
    }



    return false;
}

/**
     * 删除redis中的任务数据
     * @param task
     */
private void removeTaskFromCache(Task task) {

    String key = task.getTaskType()+"_"+task.getPriority();

    if(task.getExecuteTime()<=System.currentTimeMillis()){
        cacheService.lRemove(ScheduleConstants.TOPIC+key,0,JSON.toJSONString(task));
    }else {
        cacheService.zRemove(ScheduleConstants.FUTURE+key, JSON.toJSONString(task));
    }
}

/**
     * 删除任务,更新任务日志状态
     * @param taskId
     * @param status
     * @return
     */
private Task updateDb(long taskId, int status) {
    Task task = null;
    try {
        //删除任务
        taskinfoMapper.deleteById(taskId);

        TaskinfoLogs taskinfoLogs = taskinfoLogsMapper.selectById(taskId);
        taskinfoLogs.setStatus(status);
        taskinfoLogsMapper.updateById(taskinfoLogs);

        task = new Task();
        BeanUtils.copyProperties(taskinfoLogs,task);
        task.setExecuteTime(taskinfoLogs.getExecuteTime().getTime());
    }catch (Exception e){
        log.error("task cancel exception taskid={}",taskId);
    }

    return task;

}

2.4.3 消费任务

1、删除list中的数据
2、使用updateDB删除和跟新日志表

在TaskService中添加方法

/**
 * 按照类型和优先级来拉取任务
 * @param type
 * @param priority
 * @return
 */
public Task poll(int type,int priority);

实现

/**
     * 按照类型和优先级拉取任务
     * @return
     */
@Override
public Task poll(int type,int priority) {
    Task task = null;
    try {
        String key = type+"_"+priority;
        String task_json = cacheService.lRightPop(ScheduleConstants.TOPIC + key);
        if(StringUtils.isNotBlank(task_json)){
            task = JSON.parseObject(task_json, Task.class);
            //更新数据库信息
            updateDb(task.getTaskId(),ScheduleConstants.EXECUTED);
        }
    }catch (Exception e){
        e.printStackTrace();
        log.error("poll task exception");
    }

    return task;
}

2.4.4 未来定时任务更新-reids管道

减少与redis的交互次数
1、在引导类中添加开启任务调度注解:@EnableScheduling
2、在service中添加定时任务 @Scheduled(cron = “0 */1 * * * ?”),每分钟一次

@Scheduled(cron = "0 */1 * * * ?")
public void refresh() {
    System.out.println(System.currentTimeMillis() / 1000 + "执行了定时任务");

    // 获取所有未来数据集合的key值
    Set<String> futureKeys = cacheService.scan(ScheduleConstants.FUTURE + "*");// future_*
    for (String futureKey : futureKeys) { // future_250_250

        String topicKey = ScheduleConstants.TOPIC + futureKey.split(ScheduleConstants.FUTURE)[1];
        //获取该组key下当前需要消费的任务数据
        Set<String> tasks = cacheService.zRangeByScore(futureKey, 0, System.currentTimeMillis());
        if (!tasks.isEmpty()) {
            //将这些任务数据添加到消费者队列中
            cacheService.refreshWithPipeline(futureKey, topicKey, tasks);
            System.out.println("成功的将" + futureKey + "下的当前需要执行的任务数据刷新到" + topicKey + "下");
        }
    }
}
public List<Object> refreshWithPipeline(String future_key,String topic_key,Collection<String> values){

        List<Object> objects = stringRedisTemplate.executePipelined(new RedisCallback<Object>() {
            @Nullable
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                StringRedisConnection stringRedisConnection = (StringRedisConnection)redisConnection;
                String[] strings = values.toArray(new String[values.size()]);
                stringRedisConnection.rPush(topic_key,strings);
                stringRedisConnection.zRem(future_key,strings);
                return null;
            }
        });
        return objects;
    }

总结

1、使用rebbitmq使用的场景是在支付和订单微服务中,用于实现消息可以延迟30分钟付款的功能。并借用该中间件的插件实现支付的异步下单功能,并可以快速处理前几分钟,防止消息堆积
2、使用redis是基于zset的去重和排序功能,相当于将一定数据的保存在数据库,使用定时任务同步数据库符合五分钟的任务到zset中,然后,在在zest中定时更新可以运行的任务到list集合中,相当于实现了延迟功能和缓存功能。
3、第二种还可以扩展为将rabbitmq中时间较长的数据存到redis中,然后定时的去同步redis中的数据到数据库中,防止消息堆积。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Jenkins 构建CICD

GitLab GitLab安装 https://gitlab.cn/install/?versionce CentOS 下安装 1. 安装和配置必须的依赖项 在 CentOS 7上&#xff0c;下面的命令也会在系统防火墙中打开 HTTP、HTTPS 和 SSH 访问。这是一个可选步骤&#xff0c;如果您打算仅从本地网络访问极狐GitLab&#xf…

【极客时间-系列教程】Vim 实用技巧必知必会-基本概念和基础命令:应对简单的编辑任务

vim很强大&#xff0c;但它的入门确实是比较高&#xff0c;对于初学者来说&#xff0c;怎么退出都是一件很难的事情&#xff0c; 不管你有没有遇到过&#xff0c;反正我是遇到过退出比较难的问题 首先介绍几个常用的命令和按键 :q! 退出不保存:w 写入不退出:r 读文件:wq 写入…

Android面试官の小抄,可能是东半球最好的

面试官的小抄&#xff0c;Android面试&进阶一网打尽&#xff0c;让一部分人先学起来 背景 作为一名客户端开发者&#xff0c;能够明显的感觉到小程序这些年对原生市场带来的压迫感&#xff0c;比如现在的创业公司都是小程序探路&#xff0c;成熟了再推进客户端&#xff0…

用递归解饮料换购

乐羊羊饮料厂正在举办一次促销优惠活动。乐羊羊C型饮料&#xff0c;凭3个瓶盖可以再换一瓶C型饮料&#xff0c;并且可以一直循环下去&#xff0c;但不允许赊账。 请你计算一下&#xff0c;如果小明不浪费瓶盖&#xff0c;尽量地参加活动&#xff0c;那么&#xff0c;对于他初始…

小程序与公众号下发统一消息接口返回45109

根据微信官方通告&#xff0c;自 2023 年 9 月 20 日起&#xff0c;下发统一消息接口将被收回&#xff0c;返回45109。链接见 小程序与公众号下发统一消息接口调整通知 | 微信开放社区各位开发者&#xff1a;下发统一消息 接口曾支持小程序与公众号统一的模板消息下发。由于小程…

揭密,这个微信群机器人的所有秘密在这里

技术长久不用就废了&#xff0c;我想把软件开发技术重新捡拾起来。 咱们“一起学英语”群已有三年时光&#xff0c;群里很多朋友互帮互助走到了今天。可是&#xff0c;即使再好玩的英语话题&#xff0c;也有谈腻的时候。 群里是不是应该引入一点好玩的东西&#xff1f; 人工智能…

51单片机+DS1302设计一个电子钟(LCD1602显示时间)

一、前言 电子钟是一种能够准确显示时间的设备&#xff0c;广泛应用于家庭、办公场所和公共场所&#xff0c;为人们提供了方便和准确的时间信息。本项目设计一个基于51单片机的电子钟&#xff0c;使用DS1302作为RTC时钟芯片&#xff0c;LCD1602作为显示屏&#xff0c;并通过串…

亚马逊、Shein、lazada自养号测评如何解决支付和环境问题?

年底旺季&#xff0c;平台风控都会大规模持续升级&#xff0c;针对风控升级如果测评环境没有进行相对应的更新可能会导致大批量砍单&#xff0c;或者F号&#xff0c;严重的店铺还会被关联。有人以为是支付卡的问题&#xff0c;也有人觉得是IP被关联了。其实他们讲的也没错&…

【万字长文】前端性能优化实践 | 京东云技术团队

一、引言 从一个假死页面引发的思考&#xff1a; 作为前端开发&#xff0c;除了要攻克页面难点&#xff0c;也要有更深的自我目标&#xff0c;性能优化是自我提升中很重要的一环&#xff1b; 在前端开发中&#xff0c;会偶遇到页面假死的现象&#xff0c; 是因为当js有大量计算…

mysql 中with的用法(2)

with递归练习主要用于表里面包含父节点id之类的 查询出对应的省份和市。 建表 CREATE TABLE tb(id VARCHAR(3), pid VARCHAR(3), name VARCHAR(64));INSERT INTO tb VALUES(002, 0, 浙江省); INSERT INTO tb VALUES(001, 0, 广东省); INSERT INTO tb VALUES(003, 002, 衢州市…

史上最强AI芯片!英伟达H200震撼来袭!141 GB 超大显存,Llama2推理性能翻倍,老黄赢麻了!

原创 作者 | 王二狗英伟达又一次打了所有人措手不及&#xff01; 就在昨晚&#xff0c;老黄发布了新一代史上最强 AI芯片 NVIDIA HGX™ H200 。 141 GB 超大显存&#xff01;带宽增加 2.4 倍 H200 拥有141GB 显存&#xff01;相比之前的 H100和A100&#xff0c;容量几乎翻倍&…

SpringBoot整合Activiti7——定时器事件(九)

文章目录 定时器事件时间定义时间固定时间段时间周期 1.开始事件2.中间事件3.边界事件代码实现xml文件自定义服务任务监听器自定义用户任务监听器测试流程流程执行步骤 定时器事件 可以用在开始事件、中间事件、边界事件上&#xff0c;边界事件可以是中断和非中断边界事件 需要…

css实现元素四周阴影

前言 首先确定的是需要使用box-shadow这一属性 语法如下&#xff1a; box-shadow: h-shadow v-shadow blur spread color inset; h-shadow&#xff1a;表示水平方向上的阴影偏移量&#xff0c;必须指明&#xff0c;可以是正数、负数、0&#xff0c;如果为正数左方有阴影&…

安装MinGW并在codeblocks下使用

一、下载安装MinGW 1.下载MinGw安装器&#xff0c;下载地址 2. 安装 下载下来的知识一个安装器&#xff0c;我们双击安装会帮我们自动下载好相关文件 安装完成后会打开一个安装管理工具&#xff0c;在这个工具中我们选中想要安装的软件包然后安装到本地 选好以后在菜单栏选…

多区域LCC-HVDC一次调频二次调频

多区域LCC-HVDC高压直流互联一次二次调频&#xff0c;后续可加入双馈永磁风机调频&#xff0c;储能调频等。多个四机两区域模型互联。 目前实现的主要是区域之间的调频。

灯饰行业ERP有什么用?有哪些模块

在我们的生活当中会遇到各种类型的灯饰产品&#xff0c;这些商品有不同的设计特点和用料以及选型&#xff0c;并且在销售策略和价格策略等方面也比较灵活。 随着近些年灯饰行业同质化竞争愈演愈烈&#xff0c;国内很多的灯饰企业也遭遇较大的运营压力&#xff0c;尤其是采用传…

MySQL最新2023年面试题及答案,汇总版(7)【MySQL最新2023年面试题及答案,汇总版-第三十七刊】

文章目录 MySQL最新2023年面试题及答案&#xff0c;汇总版(7)01、NOW() 和 CURRENT_DATE() 有什么区别&#xff1f;02、CHAR和VARCHAR的区别&#xff1f;03、主键索引与唯一索引的区别&#xff1f;04、MySQL中有哪些不同的表格&#xff1f;05、SQL的生命周期&#xff1f;06、你…

浪潮服务器安装操作系统

文章目录 一、准备工作1.1 制作启动 U 盘 一、准备工作 一开始认为将镜像文件导入U盘即可&#xff0c;但实操中并不行&#xff0c;得首先将U盘制作成启动盘才行。   首先需要确定安装的版本&#xff0c;我这里要安装的是 CentOS 7.9 版本&#xff0c;镜像文件为 CentOS-7-x86…

Git企业开发级讲解(一)

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、Git初识1、提出问题2、如何解决--版本控制器3、注意事项 二、Git 安装1、Linux-centos2、…

【图的定义和术语,图的类型定义】

文章目录 图的定义和术语图的类型定义 图的定义和术语 图&#xff1a;G&#xff08;V,E&#xff09;V:顶点&#xff08;数据元素&#xff09;的有穷非空集合。 E&#xff1a;边的有穷集合。 无向图&#xff1a;每条边都是无方向的。 有向图&#xff1a;每条边都是有方向的。 …