Rabbitmq延迟消息队列和定时执行任务等

本文介绍如何使用RabbitMQ实现消息的延迟消费,通过设置队列过期时间和死信队列,确保消息在特定时间后被消费,适用于如订单失效、定时通知等场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

   有时候我们希望需要隔多长时间执行一次,或者消费消息,并不是立刻通知他人,比如30分钟后订单失效,1小时后提醒通知等

这个时候就需要延迟的执行时间,Rabbitmq可以设置队列和消费的过期时间, 我们需要利用两个队列来转发实现这样的延迟消费,  queue1队列设置的过期时间,queue2死信队列从queue1队列过期后转发而来,然后消费队列。 

 话不多说,贴一些重要代码

1 核心 配置类

/**
 * 
 */
package SpringBootRabbitMQ.demo.mq;

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

import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * RabbitMQ 配置类 ,绑定队列,交换器,路由key
 * @author lmc
 * 2018年12月19日
 */
@Configuration
public class RabbitmqConfig {

//主函数启动加入依赖,不需要这样配置属性,  配置文件必须以 spring 开头才行
/*  @Value("${rabbitmq.host}")
    public String host;
   @Bean
    public CachingConnectionFactory connectionFactory() {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost(virtualHost);
        connectionFactory.setPublisherConfirms(publisherConfirms);// 手动应答模式
        return connectionFactory;
    }*/
    
	 /**
     * 方法rabbitAdmin的功能描述:动态声明queue、exchange、routing
     * @param connectionFactory
     * @return
     */
	@Bean
	public RabbitAdmin rabbitAdmin(ConnectionFactory  connectionFactory ){
		
		RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);   
		 //声明死信队列(Fanout类型的exchange)
		Queue deadQueue = new Queue(RabbitConstants.QUEUE_NAME_DEAD_QUEUE);
		 // 死信队列交换机,广播形式
        FanoutExchange deadExchange = new FanoutExchange(RabbitConstants.MQ_EXCHANGE_DEAD_QUEUE);
        rabbitAdmin.declareQueue(deadQueue);
        rabbitAdmin.declareExchange(deadExchange);
        rabbitAdmin.declareBinding(BindingBuilder.bind(deadQueue).to(deadExchange));//绑定队列 未指明路由KEY
        
        // Topic 形式 以主题方式发磅,接收者可以用表达比指定所关注的队列,消费消息
        
        // 发放奖励队列交换机  单一模式  只跟队列,路由键,交换器一致才发
        DirectExchange exchange = new DirectExchange(RabbitConstants.MQ_EXCHANGE_SEND_AWARD);
        //声明发送优惠券的消息队列(Direct类型的exchange) 设置延迟队列的关键
        Queue couponQueue = queue(RabbitConstants.QUEUE_NAME_SEND_COUPON);
        rabbitAdmin.declareQueue(couponQueue);
        rabbitAdmin.declareExchange(exchange);
        rabbitAdmin.declareBinding(BindingBuilder.bind(couponQueue).to(exchange).with(RabbitConstants.MQ_ROUTING_KEY_SEND_COUPON));
        
        // 测试不走死信队列或者延迟队列,直接发发送到队列
        Queue testQueue = new Queue(RabbitConstants.QUEUE_NAME_SEND_TEST);
        rabbitAdmin.declareQueue(testQueue);
        rabbitAdmin.declareExchange(exchange);
        rabbitAdmin.declareBinding(BindingBuilder.bind(testQueue).to(exchange).with(RabbitConstants.MQ_ROUTING_KEY_SEND_TEST));
        
		return rabbitAdmin;
	}
	
	//1 重写了queue方式 ,主要是为了设置死信队列关联,绑定和转发的目的
	//2 如果有队列延时不同,必须定制多个队列绑定,因为队列消费是从顶端开始丢弃到死信队列,如果前一条消息过期时间1个小时,第二条30分钟,这样也只会等待第一条1小时的队列丢弃后才丢弃第二条30分钟过期的队列
	//所以需要保证同一队列过期时间是一致 PS 可以通过设置消息的expiration字段或者x-message-ttl属性来设置过期时间,两者是一样的效果,都设置了过期时间取两者的最小值
	 public Queue queue(String name) {
	        Map<String, Object> args = new HashMap<>();
	        // 设置死信队列
	        args.put("x-dead-letter-exchange", RabbitConstants.MQ_EXCHANGE_DEAD_QUEUE);
	        args.put("x-dead-letter-routing-key", RabbitConstants.MQ_ROUTING_KEY_DEAD_QUEUE);
	        // 设置消息的过期时间, 单位是毫秒
	        args.put("x-message-ttl", 10000);
	        // 是否持久化
	        boolean durable = true;
	        // 仅创建者可以使用的私有队列,断开后自动删除
	        boolean exclusive = false;
	        // 当所有消费客户端连接断开后,是否自动删除队列
	        boolean autoDelete = false;
	        return new Queue(name, durable, exclusive, autoDelete, args);
	    }
	 
}

 

2 发送消息的类

 /**
 * 
 */
package SpringBootRabbitMQ.demo.mq;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import com.alibaba.fastjson.JSON;

import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;

/**
 * Rabbit 发送消息配置类
 * @author lmc
 * 2018年12月19日
 */
@Service
public class RabbitSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback, InitializingBean {

	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	
	/**
     * Rabbit MQ 客户端注入
     */
    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 系统配置 注入
     */
    @Autowired
    private SystemConfig systemConfig;
    
    /*
	 * * 发送MQ消息
     * @param exchangeName 交换机名称
     * @param routingKey   路由名称
     * @param message      发送消息体
	 */
    public void sendMessage(String exchangeName,String routingKey,Object message){
    	  Assert.notNull(message, "message 消息体不能为NULL");
          Assert.notNull(exchangeName, "exchangeName 不能为NULL");
          Assert.notNull(routingKey, "routingKey 不能为NULL");
          
         // 获取CorrelationData对象,这里重写成自己的数据再封装
          CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString(), message);
          correlationData.setExchange(exchangeName);
          correlationData.setRoutingKey(routingKey);
          correlationData.setMessage(message);
          logger.info("发送MQ消息,消息ID:{},消息体:{}, exchangeName:{}, routingKey:{}",
                  correlationData.getId(), JSON.toJSONString(message), exchangeName, routingKey);
          // 发送消息
          this.convertAndSend(exchangeName, routingKey, message, correlationData);
    }
    /**
     * 发送消息
     *
     * @param exchange        交换机名称
     * @param routingKey      路由key
     * @param message         消息内容
     * @param correlationData 消息相关数据(消息ID)
     * @throws AmqpException
     */
    private void convertAndSend(String exchange, String routingKey, final Object message, CorrelationData correlationData) throws AmqpException {
        try {
            rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);
        } catch (Exception e) {
            logger.error("MQ消息发送异常,消息ID:{},消息体:{}, exchangeName:{}, routingKey:{}",
                    correlationData.getId(), JSON.toJSONString(message), exchange, routingKey, e);

            // TODO 保存消息到数据库
        }
    }
    
	/*
	 * 调用成功之后  ,回调方法 ,设置可用参数值
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
	}

	/**
     * 用于实现消息发送到RabbitMQ交换器,但无相应队列与交换器绑定时的回调, 属于发送一个空队列 (错误的网络分区)脑裂
     * 在脑裂的情况下会出现这种情况> rabbitmq3.4.2和以后版本修复了此bug。
     */
	@Override
	public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
		  logger.error("MQ消息发送失败,replyCode:{}, replyText:{},exchange:{},routingKey:{},消息体:{}",
	                replyCode, replyText, exchange, routingKey, JSON.toJSONString(message.getBody()));
	        // TODO 保存消息到数据库		
	}

	/**
     * 用于实现消息发送到RabbitMQ交换器后接收ack回调。
     * 如果消息发送确认失败就进行重试。
     *
     * @param correlationData
     * @param ack
     * @param cause
     */
	@Override
	public void confirm(org.springframework.amqp.rabbit.support.CorrelationData correlationData, boolean ack, String cause) {
            if (!ack && correlationData instanceof CorrelationData ) {
                CorrelationData correlationDataExtends = (CorrelationData) correlationData;
                //消息发送失败,就进行重试,重试过后还不能成功就记录到数据库
                if (correlationDataExtends.getRetryCount() < systemConfig.getMqRetryCount()) {
                	  logger.info("MQ消息发送失败,消息重发,消息ID:{},重发次数:{},消息体:{}", correlationDataExtends.getId(),
                            correlationDataExtends.getRetryCount(), JSON.toJSONString(correlationDataExtends.getMessage()));
                	 // 将重试次数加一,重写CorrelationData是为了得到重试次数
                      correlationDataExtends.setRetryCount(correlationDataExtends.getRetryCount() + 1);
                      // 重发发消息
                      this.convertAndSend(correlationDataExtends.getExchange(), correlationDataExtends.getRoutingKey(),
                              correlationDataExtends.getMessage(), correlationDataExtends);
                }else{
                	  //消息重试发送失败,将消息放到数据库等待补发,   重试发送次数超过3次,直接不发了,保存到数据库或者redis里,后续再处理
                    logger.warn("MQ消息重发失败,消息入库,消息ID:{},消息体:{}", correlationData.getId(),
                            JSON.toJSONString(correlationDataExtends.getMessage()));

                    // TODO 保存消息到数据库
                }
			} else {
				   logger.info("消息发送成功并处理成功,消息ID:{}", correlationData.getId());
			}
		
	}


    
}

3  消费队列

 

/**
 * 
 */
package SpringBootRabbitMQ.demo.consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import com.alibaba.fastjson.JSON;
import com.rabbitmq.client.Channel;

import SpringBootRabbitMQ.demo.mq.RabbitConstants;
import SpringBootRabbitMQ.demo.mq.SendMessage;

/**
 * 1 延迟队列消费    这里是消费消息的类  customer 消费方  每条队列都可能会有死信队列,  实质上是从正常队列过期没消费就转到这里来消费,达到延迟执行,或者定时执行
 * 通过 两个队列结合来达到延迟消费
 * 2 首先需要创建2个队列。Queue1和Queue2。Queue1是一个消息缓冲队列,在这个队列里面实现消息的过期转发
 * 3 如果遇到不同的任务类型需要不同的延时的话,需要为每一种不同延迟时间的消息建立单独的消息队列
 * @author lmc
 * 2018年12月19日
 */
@Service
public class DeadMessageListener {
	  private final Logger logger = LoggerFactory.getLogger(DeadMessageListener.class);
	  
	  @RabbitListener(queues = RabbitConstants.QUEUE_NAME_DEAD_QUEUE)
	  public void process(SendMessage sendMessage, Channel channel, Message message) throws Exception {
		  logger.info("[{}]处理延迟队列消息队列接收数据,消息体:{}", RabbitConstants.QUEUE_NAME_SEND_COUPON, JSON.toJSONString(sendMessage));
		  System.out.println(message.getMessageProperties().getDeliveryTag());
		  
		  try {
	            // 参数校验
	            Assert.notNull(sendMessage, "sendMessage 消息体不能为NULL");

	            // TODO 处理消息

	            // 确认消息已经消费成功
	            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
	        } catch (Exception e) {
	            logger.error("MQ消息处理异常,消息体:{}", message.getMessageProperties().getCorrelationIdString(), JSON.toJSONString(sendMessage), e);

	            try {
	                // TODO 保存消息到数据库

	                // 确认消息已经消费成功
	                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
	            } catch (Exception dbe) {
	                logger.error("保存异常MQ消息到数据库异常,放到死性队列,消息体:{}", JSON.toJSONString(sendMessage), dbe);
	                // 确认消息将消息放到死信队列
	                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
	            }
	        }
	  }
}

  主要的代码都在这里, 设置过期时间的队列要与死信队列绑定,才能实现转发, 这点最关键,其它都跟普通的队列没有什么两样, 主要代码都放到我的github 上了,有兴趣的可以去看看

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值