梗概
RabbitMQ作为一款高性能的消息队列中间件,主要的作用有业务解耦,削峰,限流等作用。本文主要围绕SpringBoot整合RabbitMQ后的基础使用,介绍一下RabbitMQ中交换机,消息确认,持久化等机制
引入RabbitMQ
第一步,就是需要在pom.xml中引入相关的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
因为RabbitMQ是基于amqp协议实现的,amqp其实就是一种消息队列的协议,后续再写个关于amqp的博客来聊一下
在application.properties(或者是yml也可以)写入:
# 你的宿主host ip
spring.rabbitmq.host=192.168.199.136
# 端口号 默认是5672
spring.rabbitmq.port=5672
# 均为默认值
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
基础的配置就完成啦~
RabbitMQ总体架构
看到一个图说的特别好,于是偷过来了(逃)
- Producer和Consumer:这两个好理解,就是消息的发送方和接收方呗
- Queue:队列。用来存放生产者推送的消息,而消费者从队列中取出消息进行消费,一个队列会与一个或者若干个路由键绑定,然后注册到交换机上,生产者和消费者通过路由键来找到对应的队列
- Exchange:交换机。其实就是起到路由器的作用,一个交换机上可以绑定多个队列,按照不同的路由键来区分
特性
交换机
交换机起到一个路由的作用,一个交换机可以通过路由键注册多个消息队列,那在RabbitMQ中,交换机的种类的一共有5种,用以支持不同的消息发送方式
Direct
直连模式,这种模式很简单,就是把路由键给写死,一个队列与一个路由键绑定。Java代码如下:
配置:
@Configuration
public class DirectRabbitmqConfig {
@Bean
public Queue directQueue(){
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("Testdirect.Queue",true,true,false);
//一般设置一下队列的持久化就好,其余两个就是默认false
return new Queue("direct.Queue",true);
}
//直连交换机
@Bean
public DirectExchange testDirectExchange(){
return new DirectExchange("direct.Exchange",true,false);
}
@Bean
public Binding bindDirect(){
return BindingBuilder.bind(directQueue()).to(testDirectExchange()).with("direct.Rout");
}
}
消费者:
@RabbitListener(queues = "direct.Queue")
@Slf4j
@Component
public class DirectRabbitmqReceiver {
@RabbitHandler
public void onMessage(String str,Channel channel,Message message) throws IOException {
System.out.println(str);
}
}
Controller:
@GetMapping("/mq")
public String sendMsg(){
rabbitTemplate.convertAndSend("direct.Exchange","direct.Rout","Hello World!");
return "SUCCESS";
}
启动项目,访问接口,就会发现消息都被消费者一个个消费了。这就是直连
System
System Exchange。其实和Direct没有啥区别,只不过不需要注册路由键
Topic
Topic用的其实很多,它也需要绑定路由键,但最大的特点就是支持路由键种加入占位符:
- *表示一个占位
- #表示一个或多个占位
直接看代码也很好理解:
@Bean
public Queue directQueue(){
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("Testdirect.Queue",true,true,false);
//一般设置一下队列的持久化就好,其余两个就是默认false
return new Queue("direct.Queue",true);
}
@Bean
public Queue directQueue2(){
return new Queue("direct.Queue2",true);
}
@Bean
public TopicExchange testTopicExchange(){
return new TopicExchange("testTopicExchange",true,false);
}
@Bean
public Binding bindTopic(){
return BindingBuilder.bind(directQueue()).to(testTopicExchange()).with("direct.Queue");
}
@Bean
public Binding bindTopic2(){
return BindingBuilder.bind(directQueue2()).to(testTopicExchange()).with("direct.#");
}
消费者就只有两个,分别对应这两个队列就行了
Controller:
@GetMapping("/queue")
public String sendTopic(){
rabbitTemplate.convertAndSend("testTopicExchange","direct.Queue","Queue");
return "SUCCESS";
}
@GetMapping("/queue2")
public String sendTopic2(){
rabbitTemplate.convertAndSend("testTopicExchange","direct.Queue2","Queue2");
return "SUCCESS";
}
调用后发现,如果调用”/queue“的话,两个消费者都收到了消息;如果调用"/queue2"的话,只有一个消费者收到了消息。
这也很好理解,direct.Queue这个路由键,即匹配了direct.Queue,也匹配了direct.#。所以testTopicExchange这个交换机会将其往两个队列上推。而direct.Queue2只匹配direct.#
Fanout
Fanout是一个多播的交换机,这个交换机上的路由键将失效,只要绑定在该交换机上的队列,都会收到推到该交换机上的消息
定义一个Fanout交换机:
@Bean
public FanoutExchange testFanoutExchange(){
return new FanoutExchange("",true,false);
}
其他代码和上面的都差不多
Header:
Header交换机用得很少,它不走路由键,只按照Header的字段进行匹配,分为any和all两种,any就是只要有其中一个字段匹配即可,all就是必须全部匹配
持久化
消息队列在很多场景下都是需要有持久下机制了。否则如果队列中挤压了大量消息,此时mq服务器重启,那么这些消息就都消失了,因为这些消息都只存在于内存,而RabbitMQ为了解决这个问腿,也提供了持久化机制
持久化分为三种
- 交换机持久化
- 队列持久化
- 消息持久化
交换机持久化
交换机持久化描述的是当这个交换机上没有注册队列时,这个交换机是否删除。如果要打开持久化的话也很简单
@Bean
public DirectExchange testDirectExchange(){
//第二个参数就是是否持久化,第三个参数就是是否自动删除
return new DirectExchange("direct.Exchange",true,false);
}
消费者类上的注解:
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "direct.Queue",autoDelete = "true"),
exchange = @Exchange(value = "direct.Exchange", type = ExchangeTypes.DIRECT,durable = "true"),
key = "direct.Rout"
)
)
@Exchange注解的durable属性设置为true(默认也是true,不设置也可以)。这样,即使这个交换机没有队列,也不会被删除
队列持久化
队列持久化描述的是当这个队列没有消费者在监听时,是否进行删除。持久化做法:
@Bean
public Queue txQueue(){
//第二个参数就是durable,是否持久化
return new Queue("txQueue",true);
}
消费者类的注解:
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "direct.Queue",autoDelete = "false",durable = "true"),
exchange = @Exchange(value = "direct.Exchange", type = ExchangeTypes.DIRECT,durable = "true"),
key = "direct.Rout"
)
)
可以看到,@Queue注解上,加上了durable="true"的注解。这样队列在重启的时候就不会被删除了
消息持久化
消息持久化和前面两个稍微有点不同。消息持久化实际上就是基于确认机制去做的。默认情况下,只要消费者接收到这个消息,这个消息就从队列上被删除了。、
但考虑这样一种场景,接口层接受到一个请求,然后推送一个消息,异步地去更新数据库。此时对于消费者端来说,一拿到消息,消息就从队列上被删除,然后开始执行数据库更新,但此时数据库更新失败了,方法直接返回。但队列上已经没有这条消息了,这个更新操作不就没有完成了吗?这肯定是有问题的。
所以RabbitMQ就有了消费者确认机制,只有消费者手动确认,消息才会被删除,否则该消息将一直存在队列中,开启的方法很简单:
application.properties上加上:
spring.rabbitmq.listener.simple.acknowledge-mode=manual
意为改为手动确认。
对于消费者端:
@RabbitHandler
public void onMessage(String str,Channel channel,Message message) throws IOException {
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); //手动调用
System.out.println(str);
}
手动调用下确认即可,消息就会被删除。这一步可以放在业务逻辑的执行之后
生产者通知
对于生产者来说,这个消息是否推送成功完全不得知,所以生产者一方其实还有个消息回调的机制
在配置文件中:
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate=new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("ConfirmCallback: "+"相关数据:"+correlationData);
System.out.println("ConfirmCallback: "+"确认情况:"+ack);
System.out.println("ConfirmCallback: "+"原因:"+cause);
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("ReturnCallback: "+"消息:"+message);
System.out.println("ReturnCallback: "+"回应码:"+replyCode);
System.out.println("ReturnCallback: "+"回应信息:"+replyText);
System.out.println("ReturnCallback: "+"交换机:"+exchange);
System.out.println("ReturnCallback: "+"路由键:"+routingKey);
}
});
return rabbitTemplate;
}
主要是两个回调,一个是confirm回调,一个是return回调,这两个有什么不同呢?
经检验得知,如果推送去一个不存在交换机上,那么就会触发confirm回调;如果推送去一个存在的交换机,但对应的路由键(或者说队列)不存在,则会触发return回调。了解了这个之后,就可以在该方法上根据具体的业务场景进行不同的操作了~