前言

有很多场景需要延迟一段时间完成,例如订单超过一定时间未支付等。可以用定时任务实现,也可以用消息队列MQ来实现。近些时日,学了一些,记录一下。不再赘述rabbitMQ以及插件的安装, 3.5.7版本以后支持延迟插件。

一、准备

springboot结合rabbitMQ只需要添加jar包,配置一下就可以了。

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

application.properties配置文件里配置一下服务地址。

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
spring.rabbitmq.virtual-host=/
spring.rabbitmq.connection-timeout=15000
#采用消息确认模式,消息发出去后,异步等待响应
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true

#消费端配置
#消费者监听并发数
spring.rabbitmq.listener.simple.concurrency=10
#最大并发数
spring.rabbitmq.listener.simple.max-concurrency=20
#签收模式  推荐使用manual手工签收
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#限流
spring.rabbitmq.listener.simple.prefetch=5

二、声明队列和交换机

声明处理消息的队列,通过routingkey绑定交换机,注意交换机为CustomExchange。

@Configuration
public class DelayMessageConfig {

	public static final String DELAY_EXCHANGE_NAME = "lwq_delay_exchange";
	
	public static final String DELAY_QUEUE_NAME = "lwq_delay_queue";
	
	public static final String ROUTING_KRY = "lwq_delay_queue";
	
	/**
	 * 声明一个延迟队列
	 * @return
	 */
	@Bean
	Queue delayQueue(){
		return QueueBuilder.durable(DELAY_QUEUE_NAME).build();
	}
	/**
	 * 声明一个交换机
	 * @return
	 */
	@Bean
	CustomExchange delayExchange(){
		
		Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
		return new CustomExchange(DELAY_EXCHANGE_NAME, "x-delayed-message", true,false, args);
		
	}
	/**
	 * 绑定
	 * @param delayQueue
	 * @param delayExchange
	 * @return
	 */
	@Bean
	Binding queueBinding(Queue delayQueue, CustomExchange delayExchange){
		
	    return BindingBuilder.bind(delayQueue).to(delayExchange).with(ROUTING_KRY).noargs();
		
	}
}

三、生产者

 生产者中直接注入RabbitTemplate就可以使用了,调用convertAndSend发送消息。为了保证消息投递的可靠性,可以为RabbitTemplate设置监听函数,然后将消息发送状态写入数据库等。

rabbitTemplate.setConfirmCallback(confirmCallback);

监听函数要实现RabbitTemplate下的ConfirmCallBack接口,重写confirm方法,根据ack判断是否投递成功,投递成功则修改消息的状态为成功。另外可以设置一个定时任务,定时从数据库拿中间状态(不知道失败还是成功)的消息重新发送,尝试三次后投递还是失败可以设置消息状态为失败,再进行人工处理。这里只是代码示例如何设置监听函数,消息的可靠投递详细可参考RabbitMQ消息中间件极速入门与实战

@Component
public class DelayMessageSender {

	private static final Logger log = LoggerFactory.getLogger(DelayMessageSender.class);

	@Autowired
	private RabbitTemplate rabbitTemplate;
	
	
	/**
	 * 生产者回调函数:confirm确认消息投递成功
	 */
	final ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
		
		@Override
		public void confirm(CorrelationData correlationData, boolean ack, String cause) {
			
			String messageId = correlationData.getId();
			if (ack) {
				log.info("消息投递成功,{}",messageId);
				//进行消息记录的数据库更新
				
			}else{
				log.info("消息投递失败");
			}
			
		}
	};
	
	/**
	 * 通过延迟消息插件发动延迟消息
	 * @param msg
	 * @param expiration
	 */
	public void sendDelayMessageByPlugins(Object msg,Long expiration){
		
		//绑定异步监听回调函数
		rabbitTemplate.setConfirmCallback(confirmCallback);
		
		rabbitTemplate.convertAndSend(DelayMessageConfig.DELAY_EXCHANGE_NAME,DelayMessageConfig.ROUTING_KRY, msg,(message)->{
			 message.getMessageProperties().setHeader("x-delay", expiration);//设置延迟时间
	         return message;
		},new CorrelationData("123"));
		
	}
	
	
}

四、消费者

消费者端使用@RabbitListener注解监听第二步声明的队列即可,消费者建议使用手工签收模式

@Component
public class DelayMessageReveiver {

	@RabbitListener(queues = "lwq_delay_queue")
	@RabbitHandler
	public void receive(@Payload String msg,@Headers Map<String, Object> headers,Channel channel) throws Exception {
		
		System.out.println("接收到的消息:"+msg +"||"+LocalDateTime.now());
	    
		//业务逻辑
	    //ACK 手工签收,通知rabbitMQ,消费端消费成功
	    Long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
	    channel.basicAck(deliveryTag,false);
	}
}

结语

不用延迟插件实现延迟消息还是挺麻烦的,有了插件,简直是程序员的一大福利啊。

源码参考我的码云:https://gitee.com/waynelee/rabbitMQ