ActiveMQ笔记(7):如何清理无效的延时消息?

简介: ActiveMQ的延时消息是一个让人又爱又恨的功能,具体使用可参考上篇ActiveMQ笔记(6):消息延时投递,在很多需要消息延时投递的业务场景十分有用,但是也有一个缺陷,在一些大访问量的场景,如果瞬间向MQ发送海量的延时消息,超过MQ的调度能力,就会造成很多消息到了该投递的时刻,却没有投递出去,形成积压,一直停留在ActiveMQ web控制台的Scheduled面板中。

ActiveMQ的延时消息是一个让人又爱又恨的功能,具体使用可参考上篇ActiveMQ笔记(6):消息延时投递,在很多需要消息延时投递的业务场景十分有用,但是也有一个缺陷,在一些大访问量的场景,如果瞬间向MQ发送海量的延时消息,超过MQ的调度能力,就会造成很多消息到了该投递的时刻,却没有投递出去,形成积压,一直停留在ActiveMQ web控制台的Scheduled面板中。

下面的代码演示了,如何清理activemq中的延时消息(包括:全部清空及清空指定时间段的延时消息),这也是目前唯一可行的办法。

为了演示方便,先封装一个小工具类:

package cn.mwee.utils.mq;

import cn.mwee.utils.list.ListUtil;
import cn.mwee.utils.log4j2.MwLogger;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessagePostProcessor;

import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.TextMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Created by yangjunming on 6/20/16.
 */
public final class MessageUtil {

    private Logger logger = new MwLogger(MessageUtil.class);//这里就是一个Log4j2的实例,大家可以换成原生的log4j2或类似工具

    private ConnectionFactory connectionFactory;
    private long receiveTimeout;//接收超时时间
    private JmsTemplate jmsTemplate;
    private List<String> destinationQueueNames;
    private final static String BACKUP_QUEUE_SUFFIX = "_B";
    private boolean autoBackup = false;//是否自动将消息备份到_b的队列,方便调试


    public MessageUtil(final ConnectionFactory connectionFactory, final long receiveTimeout, final List<String> destinationQueueNames) {
        this.connectionFactory = connectionFactory;
        this.receiveTimeout = receiveTimeout;
        this.destinationQueueNames = new ArrayList<>();
        this.destinationQueueNames.addAll(destinationQueueNames.stream().collect(Collectors.toList()));
        jmsTemplate = new JmsTemplate(this.connectionFactory);
        jmsTemplate.setReceiveTimeout(this.receiveTimeout);
    }

    public MessageUtil(ConnectionFactory connectionFactory, List<String> destinationQueueNames) {
        this(connectionFactory, 10000, destinationQueueNames);
    }


    public void convertAndSend(Object message) {
        if (ListUtil.isEmpty(destinationQueueNames)) {
            logger.error("目标队列为空,无法发送,请检查配置!message => " + message.toString());
            return;
        }
        for (String dest : destinationQueueNames) {
            jmsTemplate.convertAndSend(dest, message);
            if (autoBackup) {
                jmsTemplate.convertAndSend(dest + BACKUP_QUEUE_SUFFIX, message);
            }
        }
    }

    public void convertAndSend(Object message, MessagePostProcessor messagePostProcessor) {
        if (ListUtil.isEmpty(destinationQueueNames)) {
            logger.error("目标队列为空,无法发送,请检查配置!message => " + message.toString());
            return;
        }
        for (String dest : destinationQueueNames) {
            jmsTemplate.convertAndSend(dest, message, messagePostProcessor);
            if (autoBackup) {
                jmsTemplate.convertAndSend(dest + BACKUP_QUEUE_SUFFIX, message, messagePostProcessor);
            }
        }
    }

    public void convertAndSend(String destinationName, Object message) {
        if (StringUtils.isBlank(destinationName)) {
            logger.error("目标队列为空,无法发送,请检查配置!message => " + message.toString());
            return;
        }
        jmsTemplate.convertAndSend(destinationName, message);
        if (autoBackup) {
            jmsTemplate.convertAndSend(destinationName + BACKUP_QUEUE_SUFFIX, message);
        }
    }


    public void convertAndSend(String destinationName, Object message, MessagePostProcessor messagePostProcessor) {
        if (StringUtils.isBlank(destinationName)) {
            logger.error("目标队列为空,无法发送,请检查配置!message => " + message.toString());
            return;
        }
        jmsTemplate.convertAndSend(destinationName, message, messagePostProcessor);
        if (autoBackup) {
            jmsTemplate.convertAndSend(destinationName + BACKUP_QUEUE_SUFFIX, message, messagePostProcessor);
        }
    }

    public static String getText(javax.jms.Message message) {
        if (message instanceof TextMessage) {
            try {
                return ((TextMessage) message).getText();
            } catch (JMSException e) {
                return message.toString();
            }
        }
        return message.toString();
    }

    public String getFirstDestination() {
        if (ListUtil.isEmpty(destinationQueueNames)) {
            return null;
        }
        return destinationQueueNames.get(0);
    }


    public boolean isAutoBackup() {
        return autoBackup;
    }

    public void setAutoBackup(boolean autoBackup) {
        this.autoBackup = autoBackup;
    }
}

其中主要就用到了convertAndSend(Object message, MessagePostProcessor messagePostProcessor) 这个方法,其它代码可以无视。

先来模拟瞬间向MQ发送大量延时消息:

    /**
     * 发送延时消息
     *
     * @param messageUtil
     */
    private static void sendScheduleMessage(MessageUtil messageUtil) {
        for (int i = 0; i < 10000; i++) {
            Object obj = "test:" + i;
            messageUtil.convertAndSend(obj, new ScheduleMessagePostProcessor(1000 + i * 1000));
        }
    }

这里向MQ发送了1w条延时消息,每条消息延时1秒*i,上面代码中的ScheduleMessagePostProcessor类可在上篇中找到。

运行完之后,MQ中应该堆积着了很多消息了:

下面的代码可以清空所有延时消息:

    /**
     * 删除所有延时消息
     *
     * @param connectionFactory
     * @throws JMSException
     */
    private static void deleteAllScheduleMessage(final ConnectionFactory connectionFactory) throws JMSException {
        Connection conn = connectionFactory.createConnection();
        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Destination management = session.createTopic(ScheduledMessage.AMQ_SCHEDULER_MANAGEMENT_DESTINATION);
        MessageProducer producer = session.createProducer(management);
        Message request = session.createMessage();
        request.setStringProperty(ScheduledMessage.AMQ_SCHEDULER_ACTION, ScheduledMessage.AMQ_SCHEDULER_ACTION_REMOVEALL);
        producer.send(request);
    }

清空所有延时消息,有些用力过猛了,很多时候,我们只需要清理掉过期的延时消息(即:本来计划是8:00投递出去的消息,结果过了8点还没投递出去) 

    /**
     * 删除过期的延时消息
     *
     * @param connectionFactory
     * @throws JMSException
     */
    private static void deleteExpiredScheduleMessage(final ConnectionFactory connectionFactory) throws JMSException {
        long start = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(12);//删除:当前时间前12小时范围的延时消息
        long end = System.currentTimeMillis();
        Connection conn = connectionFactory.createConnection();
        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Destination management = session.createTopic(ScheduledMessage.AMQ_SCHEDULER_MANAGEMENT_DESTINATION);
        MessageProducer producer = session.createProducer(management);
        Message request = session.createMessage();
        request.setStringProperty(ScheduledMessage.AMQ_SCHEDULER_ACTION, ScheduledMessage.AMQ_SCHEDULER_ACTION_REMOVEALL);
        request.setStringProperty(ScheduledMessage.AMQ_SCHEDULER_ACTION_START_TIME, Long.toString(start));
        request.setStringProperty(ScheduledMessage.AMQ_SCHEDULER_ACTION_END_TIME, Long.toString(end));
        producer.send(request);
    }

与上一段代码基本相似,只是多指定了删除消息的起止时间段。  

最后贴一段spring的配置文件及main函数入口

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
 5 
 6     <bean id="jmsFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
 7         <property name="connectionFactory">
 8             <bean class="org.apache.activemq.ActiveMQConnectionFactory">
 9                 <property name="brokerURL"
10                           value="failover:(tcp://localhost:61616,tcp://localhost:61626)?randomize=false&amp;backup=true"/>
11                 <property name="maxThreadPoolSize" value="100"/>
12             </bean>
13         </property>
14     </bean>
15 
16     <bean id="messageUtil" class="cn.mwee.utils.mq.MessageUtil">
17         <constructor-arg index="0" ref="jmsFactory"/>
18         <constructor-arg index="1" value="10000"/>
19         <constructor-arg index="2">
20             <list>
21                 <value>dest1</value>
22                 <value>dest2</value>
23             </list>
24         </constructor-arg>
25         <property name="autoBackup" value="true"/>
26     </bean>
27 
28 </beans>
View Code

main函数:

    public static void main(String[] args) throws InterruptedException, JMSException {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-sender.xml");
        ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class, "jmsFactory");
        MessageUtil messageUtil = context.getBean(MessageUtil.class);
//        sendScheduleMessage(messageUtil);
//        deleteAllScheduleMessage(connectionFactory);
        deleteExpiredScheduleMessage(connectionFactory);
    }

参考文章:

Enhanced JMS Scheduler in ActiveMQ

相关实践学习
消息队列RocketMQ版:基础消息收发功能体验
本实验场景介绍消息队列RocketMQ版的基础消息收发功能,涵盖实例创建、Topic、Group资源创建以及消息收发体验等基础功能模块。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
目录
相关文章
|
消息中间件 NoSQL Java
Springboot 指定重发的次数和延迟时间,定时异步执行 重发任务
Springboot 指定重发的次数和延迟时间,定时异步执行 重发任务
836 0
Springboot 指定重发的次数和延迟时间,定时异步执行 重发任务
|
1月前
|
消息中间件 存储 监控
如何修改 Kafka 消息保留时长:经验总结
如何修改 Kafka 消息保留时长:经验总结
263 4
|
22天前
|
消息中间件 Java 物联网
消息队列 MQ操作报错合集之建立连接时发生了超时错误,该如何解决
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
消息队列 MQ操作报错合集之建立连接时发生了超时错误,该如何解决
|
22天前
|
消息中间件 JavaScript RocketMQ
消息队列 MQ使用问题之过期删除机制的触发条件是什么
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
消息队列 MQ使用问题之过期删除机制的触发条件是什么
|
22天前
|
消息中间件 JavaScript Linux
消息队列 MQ操作报错合集之客户端在启动时遇到了连接错误,是什么原因
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
|
2月前
|
消息中间件 Arthas 监控
消息队列 MQ产品使用合集之每次重置reconsumeTimes就无法达到死信阈值,重试次数是否就要应用方控制
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
消息队列 MQ产品使用合集之每次重置reconsumeTimes就无法达到死信阈值,重试次数是否就要应用方控制
|
2月前
|
消息中间件 Java 测试技术
消息队列 MQ操作报错合集之设置了setKeepAliveInterval(1)但仍然出现客户端未连接,该怎么解决
在使用消息队列MQ时,可能会遇到各种报错情况。以下是一些常见的错误场景、可能的原因以及解决建议的汇总:1.连接错误、2.消息发送失败、3.消息消费报错、4.消息重试与死信处理、5.资源与权限问题、6.配置错误、7.系统资源限制、8.版本兼容性问题。
|
3月前
|
消息中间件 Docker 微服务
RabbitMQ入门指南(十一):延迟消息-延迟消息插件
RabbitMQ是一个高效、可靠的开源消息队列系统,广泛用于软件开发、数据传输、微服务等领域。本文主要介绍了DelayExchange插件、延迟消息插件实现延迟消息等内容。
620 0
|
消息中间件 关系型数据库 MySQL
SpringBoot-Kafka(生产者事务、手动提交offset、定时消费、消息转发、过滤消息内容、自定义分区器、提高吞吐量)
SpringBoot-Kafka(生产者事务、手动提交offset、定时消费、消息转发、过滤消息内容、自定义分区器、提高吞吐量)
SpringBoot-Kafka(生产者事务、手动提交offset、定时消费、消息转发、过滤消息内容、自定义分区器、提高吞吐量)
|
消息中间件 存储 缓存
RabbitMQ (HelloWord 消息应答 持久化 不公平分发 预取值)2
RabbitMQ (HelloWord 消息应答 持久化 不公平分发 预取值)2
57 0