spring boot 自学笔记(八) Rabbitmq 延迟消息(插件)

在前面文章有通过Rabbit的死信方式来实现延迟队列机制, 但是这种方式有极大的弊端, 机试不考虑死信队列性能问题,另外发送的消息并不能保证时间延迟的可靠性,。 举例如下:

同时发送两条延迟消息,分别是间隔10S 和 30S,正常情况下,会在10S 之后和 30S 之后分别收到消息, 但实际情况可能是 ,假如先发送的是30S消息, 再发送的10S消息 , 那么收到消息的情况可能会是在30S 之后同时收到两条消息,具体原因是因为队列先进先出原则。

本篇主要记录使用插件形式实现延迟消息队列, 并且避免上述情况:

插件名称: rabbitmq_delayed_message_exchange

官网下载地址 ,下载对应版本安装即可, 操作教程线上教程很多,自行扫荡,简单的说下我自己安装的docker测试环境:

1. 下载插件, 并且解压 :rabbitmq_delayed_message_exchange-20171201-3.7.x.ez

2. 在目录下新建 Dockerfile:

FROM rabbitmq:3.7-management

COPY rabbitmq_delayed_message_exchange-20171201-3.7.x.ez /plugins

RUN rabbitmq-plugins enable --offline rabbitmq_mqtt rabbitmq_federation_management rabbitmq_stomp rabbitmq_delayed_message_exchange

3. build 镜像:

docker build -t rabbitmq:3.7

4. 启动容器: 

docker run -d --hostname dev01 --name rabbitmq --network host -e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=zhanglu rabbitmq:3.7

此时如果机器对外部开放了15672端口,就可以访问到管理界面

 

注意: 如果存在上篇文章中的队列 ,请在管理界面中删掉对应的exchange 和 queue, 因为接下来需要重新绑定交换机和队列关系。 

 

解决思路:

1. 定义默认交换机 (主要提供给普通及时消息使用)

2. 定义默认的延迟消息专属交换机 和 转发消息队列(给延迟机制专属, 独立分开,避免干扰影响)

3. 配置普通队列。

执行思路:

1. 发送普通消息------- 普通交换机----> 普通队列 ----> 程序消费

2. 发送延迟消息------封装处理---> 延迟专属交换机----> 转发队列------- 普通交换机----> 普通队列 ----> 程序消费

 

配置文件:

#rabbitmq
spring.rabbitmq.host=192.168.85.133
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=zhanglu

 

rabbitmq 链接配置:

package com.book.rabbitmq.config;

import org.apache.log4j.Logger;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

/**
 * rabbitmq 配置类
 * 
 * @author victor
 *
 */
@Configuration
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitMQConfiguration {

	private static Logger logger = Logger.getLogger(RabbitMQConfiguration.class);

	private String host;

	private int port;

	private String username;

	private String password;

	@Bean
	public ConnectionFactory connectionFactory() {
		CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
		connectionFactory.setUsername(username);
		connectionFactory.setPassword(password);
		connectionFactory.setVirtualHost("/");
		connectionFactory.setPublisherConfirms(true);
		logger.info("Create ConnectionFactory bean ..");
		return connectionFactory;
	}

	@Bean
	@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
	public RabbitTemplate rabbitTemplate() {
		RabbitTemplate template = new RabbitTemplate(connectionFactory());
		return template;
	}

	public static Logger getLogger() {
		return logger;
	}

	public static void setLogger(Logger logger) {
		RabbitMQConfiguration.logger = logger;
	}

    //getter setter ....

}

 

相关常量类: MessageQueueConstants.java

/**
 * rabbitmq 消息队列常量
 * @author victor
 *
 */
public class MessageQueueConstants {
	
	/**
	 * 默认即时消息交换机名称
	 */
	public static String DEFAULT_DIRECT_EXCHANGE_NAME = "default.direct.exchange";
	
	
	/**
	 * 默认延迟交换机
	 */
	public static final String DEFAULT_DELAYED_EXCHANGE = "default.delayed.exchange";
	
	/**
	 * 默认延迟消息类型
	 */
	public static final String DEFAULT_DELAYED_TYPE_NAME= "x-delayed-message";
	

	/**
	 * 默认作为延时消息转发的接收队列名称
	 */
	public static final String DEFAULT_REPEAT_TRADE_QUEUE_NAME = "default.repeat.trade.queue";
	
	
	/**
	 * hello消息队列名称
	 */
	public static final String QUEUE_HELLO_NAME = "app.queue.hello";


}

 

队列JavaBean配置:

/**
 * 系统队列配置
 * 主要定义默认交换机bean以及延迟消息相关队列
 * @author victor
 *
 */
@Configuration
public class SystemQueueConfiguration {

	/**
	 * 默认及时消息交换机
	 * @return
	 */
	@Bean("defaultDirectExchange")
	public DirectExchange defaultDirectExchange() {
		return new DirectExchange(MessageQueueConstants.DEFAULT_DIRECT_EXCHANGE_NAME, true, false);
	}
	
	  /**
     * 配置默认延迟的 交换机
     */
    @Bean("defaultDelayedExchange")
    public CustomExchange defaultDelayedExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        //第二个参数是固定的  x-delayed-message
        return new CustomExchange(MessageQueueConstants.DEFAULT_DELAYED_EXCHANGE, MessageQueueConstants.DEFAULT_DELAYED_TYPE_NAME, true, false, args);
    }

    /**
     * 默认延迟消息接受并转发消息队列
     * @return
     */
    @Bean
    public Queue defaultRepeatTradeQueue() {
        return new Queue(MessageQueueConstants.DEFAULT_REPEAT_TRADE_QUEUE_NAME,true,false,false);
    }

    /**
     * 建立延迟转发队列和交换机之间的关系
     * @return
     */
    @Bean
    public Binding defaultRepeatTradeBinding() {
        return BindingBuilder.bind(defaultRepeatTradeQueue()).to(defaultDelayedExchange()).with(MessageQueueConstants.DEFAULT_REPEAT_TRADE_QUEUE_NAME).noargs();
    }

	
}
/**
 * hello队列配置
 * @author victor
 *
 */
@Configuration
public class HelloQueueConfiguration {

	@Autowired
	@Qualifier("defaultDirectExchange")
	private DirectExchange exchange;
	
	
	@Bean
	public Queue helloQueue() {
		Queue queue = new Queue(MessageQueueConstants.QUEUE_HELLO_NAME,true,false,false);
		return queue; 
	}
	
	@Bean
	public Binding  helloBinding() {
		return BindingBuilder.bind(helloQueue()).to(exchange).with(MessageQueueConstants.QUEUE_HELLO_NAME);
	}
	
}

 

发送消息方法接口和实现:

/**
 * 
 * @author victor
 * @desc 消息队列接口
 */
public interface IMessageQueueService {

	/**
	 * 发送消息,返回是否发送成功
	 * @param message
	 * @return
	 */
	public void send(QueueMessage message);
	
}

实现:

package com.book.rabbitmq.service.impl;


import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.book.rabbitmq.constants.MessageQueueConstants;
import com.book.rabbitmq.enums.message.MessageTypeEnum;
import com.book.rabbitmq.exception.MessageException;
import com.book.rabbitmq.message.QueueMessage;
import com.book.rabbitmq.service.IMessageQueueService;
import com.book.rabbitmq.utils.JSONUtils;
import com.book.rabbitmq.utils.StringUtils;

/**
 * rabbit mq 消息队列实现
 * @author victor
 *
 */
@Service
public class MessageQueueServiceImpl implements IMessageQueueService{
	
	@Autowired
	private RabbitTemplate rabbitTemplate;
	
	@Override
	public void send(QueueMessage message) {
		this.checkMessage(message);
		if(message.getType() == MessageTypeEnum.DEFAULT.getIndex()){//即时消息
			this.sendMessage(message.getExchange(),message.getQueueName(),message.getMessage());
		}
		if(message.getType() == MessageTypeEnum.DELAYED.getIndex()){//延时消息
			sendTimeMessage(message);
		}
	}
	
	private void sendMessage(String exchange,String queueName,String msg){
		rabbitTemplate.convertAndSend(exchange,queueName, msg);
	}
	
	public void sendTimeMessage(QueueMessage message) {
		int seconds = message.getSeconds();
		if(seconds <= 0){// 直接发送,无需进入死信队列
			sendMessage(message.getExchange(),message.getQueueName(), message.getMessage());
		}else{
			long times = seconds * 1000;//rabbit默认为毫秒级
			MessagePostProcessor processor = s -> {
				s.getMessageProperties().setHeader("x-delay", times);
                return s;
            };	
            //设置 x-delay 之后  将消息投递到专属交换机 , 转发队列
			rabbitTemplate.convertAndSend(MessageQueueConstants.DEFAULT_DELAYED_EXCHANGE,MessageQueueConstants.DEFAULT_REPEAT_TRADE_QUEUE_NAME, JSONUtils.toJson(message), processor);
		}
	}
	
	
	private void checkMessage(QueueMessage message){
		if (StringUtils.isNullOrEmpty(message.getExchange())) {
			throw new MessageException(10, "发送消息格式错误: 消息交换机(exchange)不能为空!");
		}
		if(message.getGroup() == null){
			throw new MessageException(10, "发送消息格式错误: 消息组(group)不能为空!");
		}
		if(message.getType() == null){
			throw new MessageException(10, "发送消息格式错误: 消息类型(type)不能为空!");
		}
		if(message.getStatus() == null){
			throw new MessageException(10, "发送消息格式错误: 消息状态(status)不能为空!");
		}
		if(StringUtils.isNullOrEmpty(message.getQueueName())){
			throw new MessageException(10, "发送消息格式错误: 消息目标名称(queueName)不能为空!");
		}
		if (StringUtils.isNullOrEmpty(message.getMessage())) {
			throw new MessageException(10, "发送消息格式错误: 消息内容(message)不能为空!");
		}
	}

}

 

QueueMessage 是自己定义的一个实体类, 主要是方便加参数, 其中的属性分组和重试,此处并没有实现,不要想多了,

	private String exchange;//交换机
	
	private String queueName;//队列
	
	private Integer type;//类型 , 主要区分普通消息和延迟消息 , 
	
	private Integer group;//分组
	
	private Date timestamp;//时间戳
	
	private String message;//消息内容
	
	private Integer status;//状态
	
	private int retry = 0; //重试次数
	
	private int maxRetry = 10;//最大次是
	
	private int seconds = 1;//延迟的秒数

 

最后一个步骤, 收到延迟队列的消费者,将消息重新投递个普通队列:

/**
 * 
 * @author victor
 * @desc 延迟消息接收处理消费者
 */
@Component
@RabbitListener(queues = MessageQueueConstants.DEFAULT_REPEAT_TRADE_QUEUE_NAME)
public class TradeProcessor {
	
	@Autowired
	private IMessageQueueService messageQueueService;

	@RabbitHandler
    public void process(String content) {
		QueueMessage message = JSONUtils.toBean(content, QueueMessage.class);
		message.setType(MessageTypeEnum.DEFAULT.getIndex());
		messageQueueService.send(message);
    }
}

 

测试部分, 给HELLO队列加一个消费监听, 并且测试发送几条消息查看结果:

package com.book.rabbitmq.processor.hello;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import com.book.rabbitmq.constants.MessageQueueConstants;

/**
 * hello 队列消费者
 * @author victor
 *
 */
@Component
@RabbitListener(queues = MessageQueueConstants.QUEUE_HELLO_NAME)
public class HelloProcessor {

	@RabbitHandler
    public void process(String content) {
		System.out.println("Hello 接受消息:" + content );
		System.out.println("发送延迟消息:" + (System.currentTimeMillis() / 1000) );
		System.out.println("---------");
    }
}

 

发送消息代码:

package com.book.rabbitmq.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.book.rabbitmq.constants.MessageQueueConstants;
import com.book.rabbitmq.enums.message.MessageTypeEnum;
import com.book.rabbitmq.message.QueueMessage;
import com.book.rabbitmq.service.IMessageQueueService;

@RestController
@RequestMapping("/example/*")
public class ExampleController {
	
	@Autowired
	private IMessageQueueService messageQueueService;

	@RequestMapping("/send")
	public String send(){
		QueueMessage message = new QueueMessage(MessageQueueConstants.QUEUE_HELLO_NAME, "测试及时消息...");
		messageQueueService.send(message);
		return "ok";
	}
	
	@RequestMapping("/send/delayed")
	public String sendDelayed(){
		System.out.println("发送延迟消息:" + (System.currentTimeMillis() / 1000));
		{
			//消息1    10秒延迟
			QueueMessage message = new QueueMessage(MessageQueueConstants.QUEUE_HELLO_NAME, "测试延时消息 001 --> 10s");
			message.setType(MessageTypeEnum.DELAYED.getIndex());
			message.setSeconds(10);
			messageQueueService.send(message);
		}
		{
			//消息2  30 秒延迟
			QueueMessage message = new QueueMessage(MessageQueueConstants.QUEUE_HELLO_NAME, "测试延时消息 002 --> 30s");
			message.setType(MessageTypeEnum.DELAYED.getIndex());
			message.setSeconds(30);
			messageQueueService.send(message);
		}
		{
			//消息3  5秒延迟
			QueueMessage message = new QueueMessage(MessageQueueConstants.QUEUE_HELLO_NAME, "测试延时消息 003 --> 5s");
			message.setType(MessageTypeEnum.DELAYED.getIndex());
			message.setSeconds(5);
			messageQueueService.send(message);
		}
		
		{
			//消息4  没延迟
			QueueMessage message = new QueueMessage(MessageQueueConstants.QUEUE_HELLO_NAME, "测试普通消息 004");
			message.setType(MessageTypeEnum.DEFAULT.getIndex());
			messageQueueService.send(message);
		}
		
		return "ok";
	}
}

 

访问测试地址: http://127.0.0.1:10086/book/rabbitmq/example/send/delayed

 

控制台结果截图:

 

示例下载  提示:

如果在rabbitmq环境下运行过我之前文章例子,请把原先例子创建的队列,交换机都在管理界面删除, 再重新运行本文例子。

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值