springboot集成rabbitmq死信队列的延时队列使用

目录

        1、自动分列延时队列

        2、应答失败自动转储延时再通知机制

-------------------------------------------------------------------------------------------------------------------------------

一、自动分列延时队列

根据消息延时时间自动按D(天)、H(时)、M(分)、S(秒)各粒度进行自动建立队列及发送到对应的延时队列中,降低不同时间长度的延时队列做队列再延时的频率。

例如38天的延时消息会自动发送到20天时长队列(D20)中,20天后自动发送到10天时长队列(D10)中,10天后还剩8天就会自动发到1天时长队列中,一天后继续发到1天时长队列中,直到小于1天再按时、分、秒的粒度一次转发到对应的延时队列。

这样的动态粒度控制可以

1、避免粒度太小转发频率变大,队列转发损耗太大,单队列累积数量太多

2、粒度太大,最后的接收处理消息时间节点出现较大误差

当前设置的粒度为D20,D10,D1,H20,H10,H1,M40,M20,M10,S40,S20,S10,S9,S8,S7,S6,S5,S4,S3,S2,S1。可以看到这里10秒以后的延时都是每个一秒有一个对应的时长队列,主要是考虑到如果设置的延时消息是秒级,那么说明消息对延时时间敏感,那么每一次消息交换消耗的时间都是影响较大的,这里如果S10后就设置一个S1的那么如果8秒的就要转换8次,损耗太大,而有了S8那么就直接入S8等待8s后直接消费就行了。

其中的粒度可以根据实际情况自己配置。

代码如下:

1、固定队列配置类

package com.tb.app.configurer.mq.rabbit.producer;

import java.util.HashMap;
import java.util.Map;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 延时消息队列-扩展
 * @author tangbin
 * @date 2021年6月18日
 */
@Configuration
public class RabbitmqProducerDelayExtendsConfig {
	
	/**
	 * =========中转消费队列
	 */
	public final static String DELAY_EXTENDS_XF_TRANS_QUEUE = "delay_extends_xf_trans_queue";
	public final static String DELAY_EXTENDS_XF_TRANS_EXCHANGE = "delay_extends_xf_trans_exchange";
    public final static String DELAY_EXTENDS_XF_TRANS_ROUTING = "delay_extends_xf_trans_routing";
	/**
	 * =========最终消费队列
	 */
	public final static String DELAY_EXTENDS_XF_FINAL_QUEUE = "delay_extends_xf_final_queue";
	public final static String DELAY_EXTENDS_XF_FINAL_EXCHANGE = "delay_extends_xf_final_exchange";
    public final static String DELAY_EXTENDS_XF_FINAL_ROUTING = "delay_extends_xf_final_routing";
    
	/**
	 * =========死信队列
	 */
	public final static String DELAY_EXTENDS_DEAD_QUEUE = "delay_extends_dead_queue";
	public final static String DELAY_EXTENDS_DEAD_EXCHANGE = "delay_extends_dead_exchange";
    public final static String DELAY_EXTENDS_DEAD_ROUTING = "delay_extends_dead_routing";
 
    /**
     * =========最终消费队列中应答失败消息转储队列
     */
	public final static String DELAY_EXTENDS_XF_FINAL_ACKFAIL_QUEUE = "delay_extends_xf_final_ackfail_queue";
    public final static String DELAY_EXTENDS_XF_FINAL_ACKFAIL_ROUTING = "delay_extends_xf_final_ackfail_routing";
    public final static String DELAY_EXTENDS_XF_FINAL_ACKFAIL_EXCHANGE = "delay_extends_xf_final_ackfail_exchange";
	

    /**
     * 中转消费队列
     * @return
     */
    @Bean
    public Queue xfTransQueue(){
        return new Queue(DELAY_EXTENDS_XF_TRANS_QUEUE);
    }

    /**
     * 中转消费交换机
     * @return
     */
    @Bean
    public DirectExchange xfTransExchange(){
        return new DirectExchange(DELAY_EXTENDS_XF_TRANS_EXCHANGE);
    }
 
    /**
     * 绑定 中转消费队列和交换机
     * @return
     */
    @Bean
    public Binding bindingXfTrans(){
        return BindingBuilder.bind(xfTransQueue()).to(xfTransExchange()).with(DELAY_EXTENDS_XF_TRANS_ROUTING);
    }

    //====================================================================================================================
    
    /**
     * 最终消费队列
     * 
     * 如果应答失败转储到 “最终消费应答失败转储队列”
     * 
     * @return
     */
    @Bean
    public Queue xfFinalQueue(){
    	
        Map<String,Object> args = new HashMap<>();
        //指定过期消息跳转的交换器名称
        args.put("x-dead-letter-exchange",DELAY_EXTENDS_XF_FINAL_ACKFAIL_EXCHANGE);
        //指定跳转消息携带的roting-key
        args.put("x-dead-letter-routing-key",DELAY_EXTENDS_XF_FINAL_ACKFAIL_ROUTING);
        
        return QueueBuilder.durable(DELAY_EXTENDS_XF_FINAL_QUEUE).withArguments(args).build();
    }

    /**
     * 最终消费交换机
     * @return
     */
    @Bean
    public DirectExchange xfFinalExchange(){
        return new DirectExchange(DELAY_EXTENDS_XF_FINAL_EXCHANGE);
    }
 
    /**
     * 绑定 最终消费队列和交换机
     * @return
     */
    @Bean
    public Binding bindingXfFinal(){
        return BindingBuilder.bind(xfFinalQueue()).to(xfFinalExchange()).with(DELAY_EXTENDS_XF_FINAL_ROUTING);
    }
    
    //==============================================================================================================
    
    /**
     * 最终消费应答失败转储队列
     * 
     * 超过5分钟后自动再次下发到最终消费队列进行处理
     * 
     * @return
     */
    @Bean
    public Queue xfFinalACKFailQueue(){
    	
        Map<String,Object> args = new HashMap<>();
        //指定过期时间
        args.put("x-message-ttl", 5*60*1000);
        //指定过期消息跳转的交换器名称
        args.put("x-dead-letter-exchange",DELAY_EXTENDS_XF_FINAL_EXCHANGE);
        //指定跳转消息携带的roting-key
        args.put("x-dead-letter-routing-key",DELAY_EXTENDS_XF_FINAL_ROUTING);
        
        return QueueBuilder.durable(DELAY_EXTENDS_XF_FINAL_ACKFAIL_QUEUE).withArguments(args).build();
    	
    }

    /**
     * 最终消费应答失败转储交换机
     * @return
     */
    @Bean
    public DirectExchange xfFinalACKFailExchange(){
        return new DirectExchange(DELAY_EXTENDS_XF_FINAL_ACKFAIL_EXCHANGE);
    }
 
    /**
     * 绑定 最终消费应答失败转储 队列和交换机
     * @return
     */
    @Bean
    public Binding bindingXfFinalACKFail(){
        return BindingBuilder.bind(xfFinalACKFailQueue()).to(xfFinalACKFailExchange()).with(DELAY_EXTENDS_XF_FINAL_ACKFAIL_ROUTING);
    }
}

这个类中定义了

1、中转队列(对阶段性延时队列到期做再延时中转的队列)

2、最终消费队列(最终延时结束,即到达设置的消息延时时间后的消息队列)

3、最终消费应答失败转储队列(用于应答失败消息的转储,避免消息丢失及消息堵塞)

4、动态创建死信队列(这个类只是定义了死信队列的共同名字属性,队列的创建在另一个类)

2、动态死信队列消息发送及创建类

package com.tb.app.configurer.mq.rabbit.producer;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.google.common.collect.Maps;
import com.rabbitmq.client.Channel;
import com.tb.app.common.exception.ServiceException;

import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;


/**
 * 延时消息队列-扩展版
 * @author tangbin
 * @date 2021年6月18日
 */
@Component
@Slf4j
public class RabbitmqProducerDelayExtendsFactory{

	@Autowired
    RabbitTemplate rabbitTemplate;

	/**
	 * 
	 * 发送延时消息
	 * 
	 * @param sendDate 发消息的时间点的时间戳
	 * @param timeDelay 延时时间,单位毫秒,最大不能超过4294967295(49.710269618055555天)
	 * @param message 消息内容
	 * @param sendName 发送者描述
	 * @throws Exception
	 */
	public void sendDelayMessage(Long sendDateTimestamp,Long timeDelay,String sendName,JSONObject message){
		
		//计算过时时间节点
		sendDateTimestamp = Optional.ofNullable(sendDateTimestamp).orElseGet(()->new Date().getTime());
		Long endDateTime = sendDateTimestamp+timeDelay;
		
		//计算当前还剩余的过时时间
		Long newTimeDelay = endDateTime - new Date().getTime();
		/**
		 * 消费队列指定
		 */
		//默认中转消费队列
		String DEXE = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_EXCHANGE;
		String DEXR = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_ROUTING;
		//判断时间
		if (newTimeDelay < 10000) {
			//时间在10秒以内,指定为最终消费队列
			DEXR = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_FINAL_ROUTING;
			DEXE = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_FINAL_EXCHANGE;
		}
		
		/**
		 * 延时队列指定
		 */
		Map<String, Object> resMap = queLabel(newTimeDelay);
		String label = (String) resMap.get("label");
		Long queueDelay = (Long) resMap.get("queueDelay");
		
		String DEDE = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_DEAD_EXCHANGE+label;
		String DEDQ = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_DEAD_QUEUE+label;
		String DEDR = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_DEAD_ROUTING+label;
		
		//创建连接连接到MabbitMQ
		ConnectionFactory factory = rabbitTemplate.getConnectionFactory();
		Connection connection =factory.createConnection();
		//创建一个频道
		Channel channel = connection.createChannel(false);  
		
		//创建delay queue
		HashMap<String, Object> arguments = new HashMap<String, Object>();
		arguments.put("x-message-ttl", queueDelay);
		arguments.put("x-dead-letter-exchange", DEXE);
		arguments.put("x-dead-letter-routing-key", DEXR);   
		
		try {

			channel.exchangeDeclare(DEDE,"direct",true);
			channel.queueDeclare(DEDQ, true, false, false, arguments);
			channel.queueBind(DEDQ,DEDE,DEDR);    
			
		} catch (Exception e) {
			e.printStackTrace();
			throw new ServiceException("队列创建失败!");
		}
		
		//往队列中发出一条消息
		String sendPath = sendName+"->"+DEDQ;
		JSONObject sendDataJsonObject = new JSONObject().element("data",message)
    			.element("timeDelay", timeDelay)
    			.element("newTimeDelay", newTimeDelay)
    			.element("endDateTime", endDateTime)
    			.element("sendDateTimestamp", sendDateTimestamp)
    			.element("sendPath", sendPath);
		log.info("发送消息  : " + sendDataJsonObject.toString());
		rabbitTemplate.convertAndSend(DEDQ, sendDataJsonObject);
		
		//关闭连接
		try {

			channel.close();
			
		} catch (Exception e) {
			e.printStackTrace();
			throw new ServiceException("通的关闭失败!");
		}
		connection.close();
		
	}

	/**
	 * 获取队列标签
	 * @return
	 */
	private static Map<String, Object> queLabel(Long timeDelay) {
		
		Map<String, Object> resMap = Maps.newHashMap();
		
		//延时队列标签
		String label = "S1";
		//延时队列延时时间
		Long queueDelay = 1000l;
		
		if (timeDelay < 10*1000) {
			//10秒以内
			for (int i = 9; i >= 0; i--) {
				
				if (timeDelay >= i*1000) {
					//找到最小临界值
					label = "S"+i;
					queueDelay = i*1000l;
					break;
				}
				
			}
		}else if (Math.floor(timeDelay/(1*24*60*60*1000*1.0)) >= 20) {
			
			//大于20天
			label = "D"+20;
			queueDelay = 24*60*60*1000*20l;
			
		}else if (Math.floor(timeDelay/(1*24*60*60*1000*1.0)) >= 10) {
			
			//大于10天
			label = "D"+10;
			queueDelay = 24*60*60*1000*10l;
			
		}else if (Math.floor(timeDelay/(1*24*60*60*1000*1.0)) >= 1) {
			
			//大于1天
			label = "D"+1;
			queueDelay = 24*60*60*1000*1l;
			
		}else if (Math.floor(timeDelay/(60*60*1000*1.0)) >= 20) {
			
			//大于20小时
			label = "H"+20;
			queueDelay = 60*60*1000*20l;
			
		}else if (Math.floor(timeDelay/(60*60*1000*1.0)) >= 10) {
			
			//大于10小时
			label = "H"+10;
			queueDelay = 60*60*1000*10l;
			
		}else if (Math.floor(timeDelay/(60*60*1000*1.0)) >= 1) {
			
			//大于1小时
			label = "H"+1;
			queueDelay = 60*60*1000*1l;
			
		}else if (Math.floor(timeDelay/(60*1000*1.0)) >= 40) {
			
			//大于40分钟
			label = "M"+40;
			queueDelay = 60*1000*40l;
			
		}else if (Math.floor(timeDelay/(60*1000*1.0)) >= 20) {
			
			//大于20分钟
			label = "M"+20;
			queueDelay = 60*1000*20l;
			
		}else if (Math.floor(timeDelay/(60*1000*1.0)) >= 10) {
			
			//大于10分钟
			label = "M"+10;
			queueDelay = 60*1000*10l;
			
		}else if (Math.floor(timeDelay/(60*1000*1.0)) >= 1) {
			
			//大于1分钟
			label = "M"+1;
			queueDelay = 60*1000*1l;
			
		}else if (Math.floor(timeDelay/(1000*1.0)) >= 40) {
			
			//大于40秒
			label = "S"+40;
			queueDelay = 1000*40l;
			
		}else if (Math.floor(timeDelay/(1000*1.0)) >= 20) {
			
			//大于20秒
			label = "S"+20;
			queueDelay = 1000*20l;
			
		}else if (Math.floor(timeDelay/(1000*1.0)) >= 10) {
			
			//大于10秒
			label = "S"+10;
			queueDelay = 1000*10l;
			
		}
		
		resMap.put("label", label);
		resMap.put("queueDelay", queueDelay);
		
		return resMap;
	}
    
}

这个类中定义了

1、时间粒度控制方法queLabel,可以自己定义粒度

2、动态创建延时队列及动态指定转发到的再延时队列

3、消费者中转消息再延时监听类

@Component
@RabbitListener(queues = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_QUEUE)//监听的队列名称 
@Slf4j
public class RabbitmqDelayComsumerExtendsTransComsumer {
	
	@Autowired
	RabbitmqProducerDelayExtendsFactory rabbitmqProducerDelayExtendsFactory;

	//@RabbitHandler
    public void process(JSONObject result) {
		
		log.info("消费者收到"+RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_QUEUE+"消息  : " + result.toString());
		Long timeDelay = result.getLong("timeDelay");
		Long sendDateTimestamp = result.getLong("sendDateTimestamp");
		JSONObject data = result.getJSONObject("data");
		
		//重新发到延时队列中
		rabbitmqProducerDelayExtendsFactory.sendDelayMessage(sendDateTimestamp, timeDelay,RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_QUEUE, data);
    }
    
	
	@RabbitHandler
    public void receiveObjectDel(Channel channel, JSONObject json, Message message){

        log.info("接收到"+RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_QUEUE+"的消息:"+json);

        //手动ACK
        //默认情况下如果一个消息被消费者所正确接收则会被从队列中移除
        //如果一个队列没被任何消费者订阅,那么这个队列中的消息会被 Cache(缓存),
        //当有消费者订阅时则会立即发送,当消息被消费者正确接收时,就会被从队列中移除
        try {
            //手动ack应答
        	//最后一个参数是:是否重回队列
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
            process(json);
            //拒绝消息
            //channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            //消息被丢失
            //channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            //消息被重新发送
            //channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            //多条消息被重新发送
            //channel.basicNack(message.getMessageProperties().getDeliveryTag(), true, true);
            //log.info("消息消费成功:id:{}",message.getMessageProperties().getDeliveryTag());
        } catch (IOException e) {
            e.printStackTrace();
            log.info("消息消费失败:id:{}",message.getMessageProperties().getDeliveryTag());
        }
    }
}

这个类是一个消费者类,主要是接收需要再延时的消息进行消息的再延时。

4、最终消费者类

import org.springframework.stereotype.Component;

import com.rabbitmq.client.Channel;
import com.tb.app.configurer.mq.rabbit.producer.RabbitmqProducerDelayExtendsConfig;

import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;

@Component
@RabbitListener(queues = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_FINAL_QUEUE)//监听的队列名称 
@Slf4j
public class RabbitmqDelayComsumerExtendsFinalComsumer {

	//@RabbitHandler
    public void process(JSONObject result) {
		log.info("消费者收到"+RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_FINAL_QUEUE+"消息  : " + result.toString());
    }
	
	@RabbitHandler
    public void receiveObjectDel(Channel channel, JSONObject json, Message message){

        log.info("接收到"+RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_FINAL_QUEUE+"的消息:"+json);

        //手动ACK
        //默认情况下如果一个消息被消费者所正确接收则会被从队列中移除
        //如果一个队列没被任何消费者订阅,那么这个队列中的消息会被 Cache(缓存),
        //当有消费者订阅时则会立即发送,当消息被消费者正确接收时,就会被从队列中移除
        try {
            //手动ack应答
        	//最后一个参数是:是否重回队列
            //channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
            //拒绝消息
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            //消息被丢失
            //channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            //消息被重新发送
            //channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            //多条消息被重新发送
            //channel.basicNack(message.getMessageProperties().getDeliveryTag(), true, true);
            //log.info("消息消费成功:id:{}",message.getMessageProperties().getDeliveryTag());
        } catch (IOException e) {
            e.printStackTrace();
            log.info("消息消费失败:id:{}",message.getMessageProperties().getDeliveryTag());
        }
    }
}

这个类也是一个消费者类,作为最后延时完成的消息接收及业务处理,这里设置的拒收消息channel.basicReject进行测试,要注意下

---------------------------------------------------------------------------------------------------------------------------------

二、应答失败自动延时再通知

在固定队列配置类RabbitmqProducerDelayExtendsConfig中的最终消费队列那里声明了死信交换机DELAY_EXTENDS_XF_FINAL_ACKFAIL_EXCHANGE,当应答失败时会自动将消息转发到最终消费应答失败转储队列。

而不是让消息重试,可以让后续正常消息正常处理,解决消息堵塞。转储队列设置的5分钟将消息再转发最终消息队列列尾,即是5分钟后回到正常消费队列排队进行重试。这个时间可以根据具体情况设置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值