Rabbit MQ作为主流的消息中间件,今天来上手使用一下。
Rabbit MQ 常用模式 : 一对一直连 、一对多工作模式、发布订阅模式、路由模式、主题Topic模式
springBoot 集成rabbitMq
1、加入依赖
<!--rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、配置
spring:
rabbitmq:
host: 127..0.0.1
username: guest
password: guest
virtual-host: hello_ra
port: 5672
3、直连模式 直接跟队列打交道
@Configuration
public class HelloConfig {
@Bean
public Queue setQueue(){
return new Queue("helloQueue");
}
}
声明一个队列 ,使用RabbitTemplate发送消息到队列
/**
* 直连模式发送消息
* @param msg
* @return
*/
@RequestMapping(value = "/send")
public String send(String msg){
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
try {
rabbitTemplate.convertAndSend("helloQueue",new Message(msg.getBytes("UTF-8"),messageProperties));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "msg发送成功";
}
工作模式也类似直连模式 不同的是 一个队列会有多个消费者 消费会平均消费队列中的消息,举例说生产者发送到队列100条消息 两个消费者同时监听这个队列 每个消费50条。
发布订阅模式 引入交换机的概念 生产者将消息发送到交换机 ,交换机根据对应的绑定关系 将消息发送到对应队列,一个交换机对应多个队列
@Configuration
public class FanountConfig {
@Bean
public Queue f1Queue(){
return new Queue("f1Queue");
}
@Bean
public Queue f2Queue(){
return new Queue("f2Queue");
}
@Bean
public FanoutExchange setEx(){
return new FanoutExchange("FanoutExchange");
}
@Bean
public Binding bind1(){
return BindingBuilder.bind(f1Queue()).to(setEx());
}
@Bean
public Binding bind2(){
return BindingBuilder.bind(f2Queue()).to(setEx());
}
}
@RequestMapping(value = "/fanountSend")
public String fanountSend(String msg){
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
try {
rabbitTemplate.send("FanoutExchange","",new Message(msg.getBytes("UTF-8"),messageProperties));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "msg fanount 发送成功";
}
路由模式 发送消息 在发布订阅的基础上 增加了路由key的概念,发送消息时根据路由key 去路由到交换机所绑定的队列中。
@Configuration
public class DirectConfig {
@Bean
public Queue d1Queue(){
return new Queue("d1Queue");
}
@Bean
public Queue d2Queue(){
return new Queue("d2Queue");
}
@Bean
public DirectExchange setDirectExchange(){
return new DirectExchange("DirectExchange");
}
@Bean
public Binding directBind1(){
return BindingBuilder.bind(d1Queue()).to(setDirectExchange()).with("route.d1");
}
@Bean
public Binding directBind2(){
return BindingBuilder.bind(d2Queue()).to(setDirectExchange()).with("route.d2");
}
}
@RequestMapping(value = "/directSend")
public String directSend(String msg){
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
try {
rabbitTemplate.send("DirectExchange","route.d1",new Message(msg.getBytes("UTF-8"),messageProperties));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "msg direct 发送成功";
}
发送消息附带的key,消息会发送到d1Queue 队列中
会话模式 在路由模式的基础上增加了 模糊匹配的功能
@Configuration
public class TopicConfig {
@Bean
public Queue t1Queue(){
return new Queue("t1Queue");
}
@Bean
public Queue t2Queue(){
return new Queue("t2Queue");
}
@Bean
public TopicExchange setTopicExchange(){
return new TopicExchange("TopicExchange");
}
@Bean
public Binding topicBind1(){
return BindingBuilder.bind(t1Queue()).to(setTopicExchange()).with("route.*");
}
@Bean
public Binding topicBind2(){
return BindingBuilder.bind(t2Queue()).to(setTopicExchange()).with("#.d2");
}
}
@RequestMapping(value = "/topicSend")
public String topicSend(String msg){
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
try {
rabbitTemplate.send("TopicExchange","route.d3",new Message(msg.getBytes("UTF-8"),messageProperties));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "msg topic 发送成功";
}
路由KEY 符号“#”匹配路由键的一个或多个词,符号“*”匹配路由键的一个词。
以上为消息生产者发送消息到队列的过程。
消息消费,消费者 无需知道消息生产者,只需要针对相应的队列进行消息消费就可以。
@Component
public class MessageReceiver {
@RabbitListener(queues = "helloQueue")
public void helloRece(String message){
System.out.println("helloRece 接收到"+message);
}
以上为简单的实例使用。
消息队列常见的问题就是 消息丢失,rabbitMQ的特点是安全性比较好的,性能也比较高的 但吞吐量比着kafka相对较低,但也不能排除消息丢失的情况。
常见消息丢失场景 : 1、发送消息到交换机 消息丢失 2、交换机接收到消息,转发消息到队列时消息丢失 3、消费者接收到消息,执行业务逻辑异常,消费消息失败导致消息丢失。
spring:
rabbitmq:
host: 39.106.48.216
username: guest
password: guest
virtual-host: hello_ra
port: 5672
publisher-returns: true
publisher-confirm-type: correlated
增加 publisher-confirm-type: correlated 消息发送到交换机开启确认模式 publisher-returns: true 开启交换机到消息队列回调,如果发送到消息队列失败 则进入回调
配置 RabbitTemplate
package com.tf.conf;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
public class RabbitConfig {
@Autowired
private CachingConnectionFactory connectionFactory;
@Bean
public RabbitTemplate rabbitTemplate(){
//若使用confirm-callback ,必须要配置publisherConfirms 为true
connectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
//若使用return-callback,必须要配置publisherReturns为true
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//使用return-callback时必须设置mandatory为true,或者在配置中设置mandatory-expression的值为true
rabbitTemplate.setMandatory(true);
// 如果消息没有到exchange,则confirm回调,ack=false; 如果消息到达exchange,则confirm回调,ack=true
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause);
}else{
log.info("消息发送失败:correlationData({}),ack({}),cause({})",correlationData,ack,cause);
}
}
});
//如果exchange到queue成功,则不回调return;如果exchange到queue失败,则回调return(需设置mandatory=true,否则不回回调,消息就丢了)
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message);
}
});
return rabbitTemplate;
}
}
举例消息发送 1、发送消息 到一个不存在的交换机,RabbitTemplate.ConfirmCallback 中 ack 参数就为false 进入消息发送失败的逻辑处理
2、消息进入交换机,路由到一个并不存在的队列,意味着这个队列并不存在,new RabbitTemplate.ReturnCallback() 将进入到回调逻辑 输出失败信息,如果进入消息队列成功 将不进行回调
消费者 消费消息时 造成消息丢失,解决方法:开启消息签收模式,消费者接收到消息 执行业务逻辑成功后进行消息签收,队列会将签收后的消息删除,证明这条消息消费成功,RabbitMq默认是自动签收的。
1、开启配置
spring:
rabbitmq:
host: 39.106.48.216
username: guest
password: guest
virtual-host: hello_ra
port: 5672
publisher-returns: true
publisher-confirm-type: correlated
#开启消息监听手动签收
listener:
simple:
acknowledge-mode: manual
- AcknowledgeMode.NONE:自动确认
- AcknowledgeMode.AUTO:根据情况确认
- AcknowledgeMode.MANUAL:手动确认
消费者手动签收
@RabbitListener(queues = "t1Queue")
public void topicRece(String msg, Message message, Channel channel) throws IOException{
try {
System.out.println("topicRece 接收到"+msg);
System.out.println(1/0);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
} catch (Exception e) {
//multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
//requeue: true :重回队列,false :丢弃,我们在nack方法中必须设置 false,否则重发没有意义。
//channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
}
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true); 代表同意签收
上述代码如果发生异常情况将进入异常捕获 执行channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true); 最后一个参数为true 代表该消息要重新回到队列,等待再次消费,但有一种情况 如果是业务原因造成的 该消息会一直循环且消费签收不成功会造成死循环。可以对该消息进行记录,然后丢弃消息, 后期做业务补偿。
- basicAck 同意签收 支持批量,设置入参mutiple为true
- basicReject 拒绝签收,不支持批量,支持是否重新入队,设置入参requeue为true
- basicNack 拒绝签收,支持批量,支持是否重新入队,设置入参requeue为true
需要注意的 basicAck 方法需要传递两个参数
- deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
- multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息