在前面文章有通过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环境下运行过我之前文章例子,请把原先例子创建的队列,交换机都在管理界面删除, 再重新运行本文例子。