springboot+rabbitmq回顾
- 本文声明
- rabbitmq安装
- 小概念
- 消息传递路径
- 三处消息确认
- 两种消息过期
- 死信队列
- 实现延迟队列
- 结合springboot实践
一、本文声明
本文只是个人初学rabbitmq后的回顾,不包含所有知识点 , 并不属于教学, 均为个人理解 ,仅供参考 ,如有错误请大佬们指正,非常感谢。
自学教材:尚硅谷rabbitmq , 狂神rabbitmq 以及查询了众多文章
二、rabbitmq的安装
安装环境 : linux , centos7.9
需要先安装rabbitmq的编译语言erlang在安装rabbitmq
安装教程自搜 ,个人安装方式。
三、小概念
- 生产者 producer
发送消息、调用实际处理业务的一方。 - 消费者 consumer
实际处理业务的一方 - 队列 queue
存储消息的位置 , 结构上确实是个队列。如果修改了队列配置需要先删除以前的,生产环境则建议声明一个新的。 - 路由key route key
交换机会根据路由key将消息推送到她该区的队列,可以有可以没有 - 交换机 exchange
医院分诊台,病人(消息)为分诊台的护士(交换机)提供自己的症状(路由key)前往正确的诊室(队列)排队等待医生(消费者)诊治(消费) - 其他
死信队列和延迟队列需要结合之后的进入死信队列的条件和延迟队列的实现
四、消息传递路径
生产者(producer)-》rabbitmq服务(broker)-》交换机(exchange)-》队列(queue)-》消费者(consumer)
加入死信队列后 :
生产者(producer)-》rabbitmq服务(broker)-》交换机(exchange)-》队列(queue)
-》消费者(consumer)
或
-》死信交换机-》死信队列-》消费者
可以看出只要是进入队列都需要进入交换机
注意 : 交换机和队列之间有绑定关系,有路由key的交换机会根据路由key去找队列,没有就全发(类似广播);
一台交换机可以绑定多个队列,一个队列也可以有多个路由key
五、三处消息确认
消息确认 即 消息确认到达了某个位置 。共三个,一个是rabbitmq服务broker(看了不少,说法不同,有说是broker的有说是交换机exchange。我的理解是到达broker,broker使用交换机中途无消息传递二是相当有调用方法消息是参数) , 一个是队列queue,一个是消费者。
默认情况下这三个位置是不会主动通知生产者是否到达了某处,需要自己去开启。
六、两种消息过期
消息过期即给消息设置删除时间,到期自动删除,类似于redis的过期,但是如果有死信队列的话可以配置死信队列然后过期的消息加入到死信队列中,这也是消息死信队列的条件之一。
两种消息过期:
一种给队列设置消息过期时间。即发送到这个队列中的消息,如设置为5秒,消息存在于这个队列5秒后就是从这个队列中消失(删除或加入死信队列);
另一种是为单独一条一条消息设置过期时间,过期后同样结果。
七、进入死信队列的条件
- 消费者拒绝且配置了死信队列
- 超出队列可容纳长度(手动规定队列容量然后超出,内存磁盘盛不下超出)
- 消息过期且队列配置了死信队列
八、实现延迟队列
- 给队列设置过期时长
- 给消息设置过期时长
- 插件实现
九、springboot+rabbitmq代码实现
依赖引入
个人选择,没有理由
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.4.3</version>
</dependency>
配置文件
server:
port: 8082
spring:
rabbitmq:
port: 5672
username: admin
password: admin
virtual-host: /
host: 127.0.0.1
#不多做配置的话消费者是自动确认的,消息被消费后会在队列中直接消失,于是乎你可以在你的消费者方法中看到消息但是在rabbitmq的web端看不到
声明交换机
@Configuration
public class ConfirmConfig {
public final static String CONFIRM_EXCHANGE_NAME="confirm_exchange";
@Bean
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME,true,false);//交换机名,是否持久化,是否自动删除
}
}
声明队列
@Configuration
public class ConfirmConfig {
//队列名称
public final static String CONFIRM_QUEUE_NAME="confirm_queue";
//声明队列
@Bean
public Queue confirmQueue(){
//两种方式
Map<String,Object> args=new HashMap<>();
return new Queue(CONFIRM_QUEUE_NAME,true,false,false,args);
// return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
//参数解释
//队列名。。。。
//持久化 : 持久化可以让rabbitmq服务重启后仍保留之前的queue和
//独占队列 : 如果我们宣布独占性队列(该队列将仅由声明的连接使用)
//其他参数 :map《String,object》格式的map,key是固定的
}
}
声明交换机和队列的绑定关系
@Configuration
public class ConfirmConfig {
public final static String CONFIRM_ROUTE_KEY="confirm";
@Bean
public Binding queueBindingExchange(){
// 声明队列的返回对象 | 声明交换机的返回对象 | 交换机找到队列的路由key
return BindingBuilder.bind(confirmQueue()).to(confirmExchange()).with(CONFIRM_ROUTE_KEY);
}
}
以上流程卸载生产者和消费这种都是可以的
生产者
@RestController
@RequestMapping("/mq")
public class SendMsgController {
public final static String CONFIRM_QUEUE_NAME="confirm_queue";
public final static String CONFIRM_EXCHANGE_NAME="confirm_exchange";
public final static String CONFIRM_ROUTE_KEY="confirm";
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping("/confirm/{msg}")
public String confirmMsg(@PathVariable(value = "msg") String msg){
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,CONFIRM_ROUTE_KEY,msg);
Date date = new Date();
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.err.println(msg+" 的发送时间 :"+simpleDateFormat.format(date));
return "SUCCESS";
}
消费者
@Component
public class ConsumerService {
// @RabbitListener(queues ="confirm_queue")
public void getMessage(Message msg, Channel channel) throws IOException {
String s = new String(msg.getBody(), "UTF-8");
Date date = new Date();
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.err.println("接收消息 : "+s+"接收到消息的时间:"+simpleDateFormat.format(date));
}
}
消息确认
1 信息到达消费者确认
这个是没有回调的 , 这个消息是发给rabbitmq的生产者不能直接感知 , 之后的两个是可以直接感知
修改配置文件
server:
port: 8082
spring:
rabbitmq:
port: 5672
username: admin
password: admin
virtual-host: /
host: 127.0.0.1
listener:
simple:
acknowledge-mode: manual #代码中手动确认需要加上这一行
这样配置如果不修改消费者代码的话,rabbitmq的页面会出现unack , 此时需要手动的去ack
@Component
public class ConsumerService {
// @RabbitListener(queues ="confirm_queue")
public void getMessage(Message msg, Channel channel) throws IOException {
String s = new String(msg.getBody(), "UTF-8");
Date date = new Date();
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.err.println("接收消息 : "+s+"接收到消息的时间:"+simpleDateFormat.format(date));
channel.basicAck(msg.getMessageProperties().getDeliveryTag(),false);//还有其他的,如拒绝消息,其他请自查
}
}
2 信息到broker和队列的异步回调
仅记录异步回调
配置文件修改
server:
port: 8081
spring:
rabbitmq:
port: 5672
username: admin
password: admin
virtual-host: /
host: 127.0.0.1
publisher-confirm-type: correlated # 开启确认回调
publisher-returns: true # 开启退回回调
confirm是消息到broker的
return是到达队列
两种写法 :
其一:
@Slf4j
@Component
public class OtherCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 初始化方法
*/
@PostConstruct
public void initMethod() {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
Integer receivedDelay = null;
if (null != correlationData) {
correlationData.getReturnedMessage().getMessageProperties().getReceivedDelay();
}
if (receivedDelay != null && receivedDelay > 0) {
// 是一个延迟消息,忽略这个错误提示
return;
}
if (ack) {
System.out.println("消息已经送达Exchange,ack已发");
} else {
System.out.println("消息没有送达Exchange");
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error("消息未送达oqueue message:{},replyCode:{},replyText:{},exchange:{},routingKey:{}",
message.getBody(),
replyCode,
replyText,
exchange,
routingKey);
}
}
其二:
@Component
@Slf4j
public class MyCallback{
@Autowired
RabbitTemplate rabbitTemplate;
@PostConstruct
public void initRabbitTemplate() {
//设置一个确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* @param correlationData 当前消息唯一关联数据
* @param b 是否成功收到
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String cause) {
log.info("投递到broker correlationData:{},ack:{},cause:{}", correlationData, b, cause);
}
});
//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
//只要消息没有投递到指定队列,就回触发这个失败回调
/**
*
* @param message 该类有以下变量
* Message message; 投递失败的消息详细信息
* replyCode; 回复的状态吗
* replyText; 回复的文本内容
* exchange; 当时这个消息发给那个交换机
* routingKey; 当时这个消息用那个路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error("投递到队列失败message:{},replyCode:{},replyText:{},exchange:{},routingKey:{}",
message.getBody(),
replyCode,
replyText,
exchange,
routingKey);
}
});
}
}
在看其他教学视频或笔记可能会发现以上回调,人家的代码中可能是RabbitTemplate.ReturnsCallback ,并且returnedMessage也不一样 , 不必担心,从方法内容中可以看到,他们输出的那些东西都成了我的这个的参数可以直接获取 , 可能是版本不同吧
两种消息过期
注意 : 这里还没有涉及到死信队列,消息过期没配置死信队列的话直接删除消失,配置了死信队列消息过期后才会到死信队列 , 死信队列是设置给某个队列的。
1给队列设置过期
//声明队列
@Bean
public Queue confirmQueue(){
//两种方式
Map<String,Object> args=new HashMap<>();
args.put("x-message-ttl",5000); //队列消息过期时间为5秒
return new Queue(CONFIRM_QUEUE_NAME,true,false,false,args);
// return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
问题 : 每一种过期时间就要创建一个队列
2 给消息设置过期时间
我这里是自己做测试,将过期时间设置成了参数
@RequestMapping("/confirm/{msg}/{ttlTime}")
public String confirmMsg(@PathVariable(value = "msg") String msg,@PathVariable(value = "ttlTime")String ttlTime){
MessagePostProcessor messagePostProcessor=new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration(ttlTime); // 消息过期时长 , 毫秒 , 消息过期后直接被抛弃
message.getMessageProperties().setContentEncoding("UTF-8");
return message;
}
};
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,CONFIRM_ROUTE_KEY,msg,messagePostProcessor);
Date date = new Date();
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.err.println(msg+" 的发送时间 :"+simpleDateFormat.format(date));
return "SUCCESS";
}
问题:如果发送两条消息,第一条消息20秒过期,第二条2秒过期,正常情况下应该是2秒的先到,但是实际上是一起到并且是20秒后。
问题 : 如果同时设置会怎样 ?
答 : 按短的走
死信队列
其实就是一个普通的队列 , 也需要交换机才能将消息送到死信队列中 , 创建死信队列、私信交换机以及他们的绑定方式和普通队列无异,之所以成为死信队列应该是因为他的特殊用途。看一下进入死信队列的条件就知道为什么特殊了:
-
消息被拒绝
-
消息过期
-
队列已满进入不了队列的消息
可以看出都是些没有被消费者消费的消息,跟一般信息相比确实不一样。
为某个队列配置上死信队列后,这个队列中的消息只要满足以上三点中的一点就会被移入到死信队列中。 -
保持你配置文件中的配置,不要让消费者自动确认消息,保持手动确认,但是需要把其中确认消息的代码换成
// channel.basicAck(msg.getMessageProperties().getDeliveryTag(),false);
// 替换为
channel.basicReject(msg.getMessageProperties().getDeliveryTag(),false); //basicReject Reject 拒绝
- 消息过期的话,把消费者注释掉,过期是停留在队列中无人消费,所以不能有消费者接收,所以把 @RabbitListener(queues =“confirm_queue”)注释掉
然后在生产者的接口中加入消息过期的配置,或者在队列配置过期 , 代码上面有就不写了。 - 队列满队意识配置了队列的最大容量,而是内存磁盘满了,第一种好实现一点,只要测试的时候发送的消息超过配置的大小就行了,多的都在死信队列 , 也不能有消费者。
//声明队列
@Bean
public Queue confirmQueue(){
Map<String,Object> args=new HashMap<>();
//如果过期就放到死信队列中去
args.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
args.put("x-dead-letter-routing-key",DEAD_ROUTE_KEY);
args.put("x-max-length",5); //队列最大容量
return new Queue(CONFIRM_QUEUE_NAME,true,false,false,args);
}
死信队列和普通队列绑定后消息的流动链路:
生产者 -》交换机 -》队列 -(满足进入死信队列的条件)-》死信交换机 -》死信队列 - 》死信队列消费者 (是的可以消费死信队列中的消息,前面也说死信队列跟普通队列相同只是用途特殊一点)
延迟队列
意思就是消息会延迟一会在能被消费者消费。但是我们知道消费者开着只要消息进入队列就会被消费,但是之前有两个东西结合起来可以实现这个效果。
死信队列有三个进入条件 , 其中一条是消息过期 , 消息过期有两种方式都可以过了过期事件后才能进入死信队列 , 那么把死信队列当作需要消费的队列不久可以实现消息延迟到达消费者了吗。
eg:生产者 -》交换机 -》队列 -过期时间-》死信交换机 -》死信队列 -》死信队列消费者
但是由于两种过期方式的问题,使用插件的方式实现消息队列。
下载安装可搜索尚硅谷笔记,我这里只记录代码实现。
其实也只是个普通的队列,只需要在创建队列时增加一个额外参数即可指定该队列使用插件实现延迟队列,交换机和绑定关系与普通队列无异。
private static final String DELAYED_QUEUE_NAME = "delayed.queue";
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-delayed-type", "direct"); //key是固定的
/**
* 1.交换机的名称
* 2.交换机的类型
* 3.是否需要持久化
* 4.是否需要自动删除
* 5.其他的参数
*/
return new CustomExchange(DELAY_EXCHANGE_NAME, "x-delayed-message", true, false, arguments);
}
发送消息(注意,我把ttlTime的类型换了)
@RequestMapping("/confirm/{msg}/{ttlTime}")
public String confirmMsg(@PathVariable(value = "msg") String msg,@PathVariable(value = "ttlTime")Integer ttlTime){
MessagePostProcessor messagePostProcessor=new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setDelay(ttlTime); //延迟时间设置
return message;
}
};
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,CONFIRM_ROUTE_KEY,msg,messagePostProcessor);
Date date = new Date();
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.err.println(msg+" 的发送时间 :"+simpleDateFormat.format(date));
return "SUCCESS";
}