首先引入依赖,由于springboot starter指定了rabbitMQ的版本,所以无需在引入依赖的时候指定版本
org.springframework.boot
spring-boot-starter-amqp
配置文件
# 应用名称
spring:
application:
name: rabbitmq
rabbitmq:
host: 192.168.227.4
port: 5672
username: admin
password: admin
publisher-confirm-type: correlated #发布确认
publisher-returns: true #路由失败 消息回退
# 应用服务 WEB 访问端口
server:
port: 8080
通过设置队列的ttl过期时间,当消息在队列中超过规定的ttl时间时未被消费时,便会抛到绑定的死信队列中去消费
用于初始化交换机、队列和路由
package com.example.rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @program: rabbitmq
* @description: ttl延迟队列配置类 @Bean可以设置组件的名字 没有设置时以方法名为组件名字
* @author: LiuZhuzheng
* @create: 2021-12-03 16:07
**/
@Configuration
public class TtlQueueConfig {
/** 普通交换机*/
public static final String NORMAL_EXCHANGE = "normal_exchange";
/** 死信交换机*/
public static final String DEAD_EXCHANGE = "dead_exchange";
/** 普通队列a*/
public static final String NORMAL_QUEUE_A = "normal_queue_a";
/** 普通队列b*/
public static final String NORMAL_QUEUE_B = "normal_queue_b";
/** 普通队列c*/
public static final String NORMAL_QUEUE_C = "normal_queue_c";
/** 死信队列*/
public static final String DEAD_QUEUE = "dead_queue";
/** 普通队列a路由key*/
public static final String NORMAL_ROUTING_KEY_A = "normal_routing_key_a";
/** 普通队列b路由key*/
public static final String NORMAL_ROUTING_KEY_B = "normal_routing_key_b";
/** 普通队列c路由key*/
public static final String NORMAL_ROUTING_KEY_C = "normal_routing_key_c";
/** 死信队列路由key*/
public static final String DEAD_ROUTING_KEY = "dead_routing_key";
/**
* 声明普通交换机
* 使用直接交换机 发布订阅方式
* 默认持久化 消费者断开连接时不自动删除
* */
@Bean
public DirectExchange normalExchange(){
return new DirectExchange(NORMAL_EXCHANGE);
}
/**
* 声明死信交换机
* */
@Bean
public DirectExchange deadExchange(){
return new DirectExchange(DEAD_EXCHANGE);
}
/**
* 声明普通队列a
* 设置ttl时间为10秒
* */
@Bean
public Queue queueA(){
// Map params = new HashMap<>();
// //声明当前队列绑定的死信交换机
// params.put("x-dead-letter-exchange",DEAD_EXCHANGE);
// //声明当前队列的死信路由key
// params.put("x-dead-letter-routing-key",DEAD_ROUTING_KEY);
// //声明队列ttl
// params.put("x-message-ttl",10000);
// return QueueBuilder.durable(NORMAL_QUEUE_A).withArguments(params).build();
return QueueBuilder.durable(NORMAL_QUEUE_A)
.deadLetterExchange(DEAD_EXCHANGE)
.deadLetterRoutingKey(DEAD_ROUTING_KEY)
.ttl(10000)
.build();
}
/**
* 声明普通队列b
* 设置ttl时间为40秒
* */
@Bean
public Queue queueB(){
return QueueBuilder.durable(NORMAL_QUEUE_B)
.deadLetterExchange(DEAD_EXCHANGE)
.deadLetterRoutingKey(DEAD_ROUTING_KEY)
.ttl(40000)
.build();
}
/**
* 声明普通队列c
* */
@Bean
public Queue queueC(){
return QueueBuilder.durable(NORMAL_QUEUE_C)
.deadLetterExchange(DEAD_EXCHANGE)
.deadLetterRoutingKey(DEAD_ROUTING_KEY)
.build();
}
/**
* 声明死信队列
* */
@Bean
public Queue queueD(){
return QueueBuilder.durable(DEAD_QUEUE).build();
}
/**
* 声明普通队列a与普通交换机绑定 设置路由key
* 参数因为已经注册在容器中 会自动从容器中取 但是名字必须一样
* */
@Bean
public Binding queueBindingA(Queue queueA, DirectExchange normalExchange){
return BindingBuilder.bind(queueA).to(normalExchange).with(NORMAL_ROUTING_KEY_A);
}
/**
* 声明普通队列b与普通交换机绑定 设置路由key
* */
@Bean
public Binding queueBindingB(Queue queueB, DirectExchange normalExchange){
return BindingBuilder.bind(queueB).to(normalExchange).with(NORMAL_ROUTING_KEY_B);
}
/**
* 声明普通队列c与普通交换机绑定 设置路由key
* */
@Bean
public Binding queueBindingC(Queue queueC, DirectExchange normalExchange){
return BindingBuilder.bind(queueC).to(normalExchange).with(NORMAL_ROUTING_KEY_C);
}
/**
* 声明死信队列与死信交换机绑定
* */
@Bean
public Binding queueBindingD(Queue queueD, DirectExchange deadExchange){
return BindingBuilder.bind(queueD).to(deadExchange).with(DEAD_ROUTING_KEY);
}
}
接收页面请求发送消息
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 普通发送消息 基于死信队列
* */
@GetMapping("/sendMsg/{msg}")
public void sendMsg(@PathVariable("msg") String msg){
log.info("当前时间:{},发送一条消息给两个队列:{}",System.currentTimeMillis(),msg);
rabbitTemplate.convertAndSend("normal_exchange","normal_routing_key_a","延时10秒的队列a" + msg);
rabbitTemplate.convertAndSend("normal_exchange","normal_routing_key_b","延时40秒的队列a" + msg);
}
通过注解监听死信队列来消费消息
package com.example.rabbitmq.listener;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* @program: rabbitmq
* @description: 死信队列消费者 利用监听器
* @author: LiuZhuzheng
* @create: 2021-12-03 16:56
**/
@Component
@Slf4j
public class DeadLetterQueueConsumer {
/**
* 接收消息
* */
@RabbitListener(queues = "dead_queue")
public void receiveDeadMessage(Message message, Channel channel) throws Exception{
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列消息:{}",System.currentTimeMillis(),msg);
}
}
显而易见,该方法存在一个缺陷,就是一个ttl时间对应一个队列,每增加一个ttl时间的需求就需要增加一个队列
针对上述问题,创建一个新队列c(上述代码已有),在初始化队列时不设置ttl时间,而是设置消息的ttl过期时间,其控制器代码如下
/**
* 设置过期时间的发送消息 基于死信队列
* */
@GetMapping("/sendMsg/{msg}/{ttl}")
public void sendMsgWithTtl(@PathVariable("msg") String msg, @PathVariable("ttl")String ttl){
log.info("当前时间:{},{}毫秒后发送消息:{}", Instant.now(),ttl,msg);
rabbitTemplate.convertAndSend("normal_exchange", "normal_routing_key_b", msg, message -> {
message.getMessageProperties().setExpiration(ttl);
return message;
});
}
这样每条消息的过期时间都可以自己设定
但是当测试时,先发送一条延时20秒的消息,在发送一条延时2秒的消息,发现后者并不会优先得到执行,而是等待前者执行完毕后立即执行
访问官网:https://www.rabbitmq.com/community-plugins.html
往下翻找到rabbitmq_delayed_message_exchange并下载
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
声明延时队列、交换机和路由key
package com.example.rabbitmq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @program: rabbitmq
* @description: 利用插件实现延迟队列配置类
* @author: LiuZhuzheng
* @create: 2021-12-15 20:11
**/
@Configuration
public class DelayedQueueConfig {
public static final String DELAYED_QUEUE_NAME = "delayed_queue";
public static final String DELAYED_EXCHANGE_NAME = "delayed_exchange";
public static final String DELAYED_ROUTING_KEY = "delayed_key";
/**
* 声明自定义交换机
* */
@Bean
public CustomExchange delayedExchange(){
Map argument = new HashMap<>(4);
argument.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,argument);
}
/**
* 声明队列
* */
@Bean
public Queue delayedQueue(){
return new Queue(DELAYED_QUEUE_NAME);
}
/**
* 绑定交换机和队列
* */
@Bean
public Binding delayedQueueBinding(Queue delayedQueue, CustomExchange delayedExchange){
return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
/**
* 基于插件发送延迟消息
* */
@GetMapping("/sendDelayedMsg/{msg}/{delayed}")
public void sendDelayedMsg(@PathVariable("msg")String msg, @PathVariable("delayed")Integer delayed){
log.info("当前时间:{},{}毫秒后给延迟队列发送消息:{}", Instant.now(),delayed,msg);
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, msg, message -> {
message.getMessageProperties().setDelay(delayed);
return message;
});
}
和上述类似
package com.example.rabbitmq.listener;
import com.example.rabbitmq.config.DelayedQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Arrays;
/**
* @program: rabbitmq
* @description: 延迟队列消费者 基于插件
* @author: LiuZhuzheng
* @create: 2021-12-15 20:30
**/
@Component
@Slf4j
public class DelayQueueConsumer {
/**
* 接收消息
* */
@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
public void receiveDelayedMessage(Message message){
String s = new String(message.getBody());
log.info("当前时间:{},收到延迟队列消息:{}", Instant.now(),s);
}
}
这样就解决了延时时间短的消息不能优先消费的问题
在生产环境中,可能RabbitMQ会挂掉或者重启,就有可能导致生产者消息投递失败,从而导致消息丢失,需要手动处理和恢复。
在配置文件中需要添加publisher-confirm-type=correlated,如文章开头所示
声明一些交换机、队列并进行绑定
普通交换机通过alternate方法绑定了备份交换机,因此在普通交换机中的消息因为消息生产者设置的路由key错误等导致不能被消费时,不会发生4.4回调中消息退回给生产者,而是会通过备份交换机转发给备份队列和报警队列进行消费。
package com.example.rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @program: rabbitmq
* @description: 发布确认高级配置
* @author: LiuZhuzheng
* @create: 2021-12-18 09:27
**/
@Configuration
public class ConfirmQueueConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm_exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm_queue";
public static final String CONFIRM_ROUTING_KEY = "confirm_key";
/**
* 备份交换机 队列
* */
public static final String BACKUP_EXCHANGE_NAME = "backup_exchange";
public static final String BACKUP_QUEUE_NAME = "backup_queue";
/**
* 报警队列
* */
public static final String WARNING_QUEUE_NAME = "warning_queue";
/**
* 声明交换机
* */
@Bean
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).alternate(BACKUP_EXCHANGE_NAME).build();
}
/**
* 声明队列
* */
@Bean
public Queue confirmQueue(){
return new Queue(CONFIRM_QUEUE_NAME);
}
/**
* 交换机与队列绑定
* */
@Bean
public Binding confirmQueueBinding(DirectExchange confirmExchange, Queue confirmQueue){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
}
/**
* 备份交换机
* */
@Bean
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
/**
* 备份队列
* */
@Bean
public Queue backupQueue(){
return new Queue(BACKUP_QUEUE_NAME);
}
/**
* 报警队列
* */
@Bean
public Queue warningQueue(){
return new Queue(WARNING_QUEUE_NAME);
}
/**
* 备份绑定
* */
@Bean
public Binding backupBind(FanoutExchange backupExchange, Queue backupQueue){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
/**
* 报警绑定
* */
@Bean
public Binding warningBind(FanoutExchange backupExchange, Queue warningQueue){
return BindingBuilder.bind(warningQueue).to(backupExchange);
}
}
package com.example.rabbitmq.controller;
import com.example.rabbitmq.config.ConfirmQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
/**
* @program: rabbitmq
* @description: 发布确认高级控制器
* @author: LiuZhuzheng
* @create: 2021-12-18 09:39
**/
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ConfirmController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发消息 测试发布确认高级
* */
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable("message") String message){
// 设置回调对象 发布确认
if (StringUtils.isEmpty(message)){
log.info("消息为空");
return;
}
Message msg = new Message(message.getBytes(StandardCharsets.UTF_8),new MessageProperties());
CorrelationData correlationData = new CorrelationData("1");
correlationData.setReturnedMessage(msg);
rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE_NAME, ConfirmQueueConfig.CONFIRM_ROUTING_KEY + "1", message, correlationData);
log.info("发送消息为:{}",message);
}
}
当消息生产者发送消息到达交换机时,就会触发该回调类
package com.example.rabbitmq.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
/**
* @program: rabbitmq
* @description: 发布确认回调类 交换机
* @author: LiuZhuzheng
* @create: 2021-12-18 09:54
**/
@Slf4j
@Component
public class MyConfirmCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 将当前接口注入 Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)
* 1 将当前类实例化
* 2 将rabbitTemplate自动注入
* 3 执行下面方法
* */
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* correlationData保存回调消息的ID及相关信息
* 第二个参数收到消息为true 没收到为false
* 第三个参数收到消息为null 没收到为失败原因
* */
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData != null ? correlationData.getId() : "";
String message = correlationData != null ? new String(correlationData.getReturnedMessage().getBody()) : "";
if (b){
log.info("交换机收到了消息,id为:{},消息为:{}", id, message);
}else {
log.info("交换机未收到id为{}的消息:{},原因是:{}",id, message, s);
}
}
/**
* 消息传递过程中不可达目的地将消息返回给生产者
* */
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息:{}被服务器退回,退回代码:{}, 退回原因:{}, 交换机是:{}, 路由 key:{}",
new String(message.getBody()), replyCode, replyText, exchange, routingKey);
}
}
package com.example.rabbitmq.listener;
import com.example.rabbitmq.config.ConfirmQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @program: rabbitmq
* @description: 发布确认高级消费者
* @author: LiuZhuzheng
* @create: 2021-12-18 09:44
**/
@Slf4j
@Component
public class ConfirmQueueConsumer {
@RabbitListener(queues = ConfirmQueueConfig.CONFIRM_QUEUE_NAME)
public void receiveMessage(Message message){
String msg = new String(message.getBody());
log.info("接收到confirm队列消息:{}",msg);
}
}
package com.example.rabbitmq.listener;
import com.example.rabbitmq.config.ConfirmQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @program: rabbitmq
* @description: 报警消费者
* @author: LiuZhuzheng
* @create: 2021-12-18 11:14
**/
@Slf4j
@Component
public class WarningQueueConsumer {
@RabbitListener(queues = ConfirmQueueConfig.WARNING_QUEUE_NAME)
public void receiveMsg(Message message){
String msg = new String(message.getBody());
log.info("报警发现不可路由消息:{}",msg);
}
@RabbitListener(queues = ConfirmQueueConfig.BACKUP_QUEUE_NAME)
public void receiveMessage(Message message){
String msg = new String(message.getBody());
log.info("发现不可路由消息:{}",msg);
}
}