AMQP RabbitMQ 新手入门 三种交换机及Producer+Consumer的消息确认机制+事务
引言
之前写过RabbitMQ的消息打回机制和死信队列,这次准备详细讲一下RabbitMQ,主要是给新手写的入门用的。
之前的文章:
Spring 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
RabbitMQ没有Kafka的partition和group,虽然在之前使用Stream整合RabbitMQ的时候,yml里配置写的是group,但其实RabbitMQ中没有group和partition这种东西。RabbitMQ中只有
Exchange(交换机)Queue(队列)RoutingKey(路由键)
如果
多个消费者消费同一个队列就会出现轮询或公平分发,所以需要匿名队列,或队列名不同。
交换机、队列、路由键 简述
Exchange就和物理硬件交换机的工作概念相同,它就是为了路由,这里要将交换机和队列放在一起说,因为这俩一旦分开就完全没有任何用处。
Queue相当于电脑,交换机的作用就是为了和队列绑定。这样在路由的时候通过交换机才能达到与其绑定的队列。
RoutingKey这里可以理解为网卡地址,我们知道电脑上的网卡可以有很多个,RoutingKey同样可以有很多个。
队列和交换机还有路由键是多对多关系,但是不要怕,看完栗子你应该就懂了
直连交换机
直连交换机是其中最容易理解的交换机了,它绑定的路由键没有什么泛型解析或者正则之类的东西,就是很直接。
举个栗子:
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableRabbit
public class RabbitMQConfigure {
@Bean
DirectExchange directExchange(){
return ExchangeBuilder.directExchange("direct_exchange_demo").build();
}
@Bean
Queue myQueue1(){
return QueueBuilder.durable("queue_demo1").build();
}
@Bean
Queue myQueue2(){
return QueueBuilder.durable("queue_demo2").build();
}
@Bean
Binding binding1 (){
return BindingBuilder.bind(myQueue1()).to(directExchange()).with("我是007");
}
@Bean
Binding binding2 (){
return BindingBuilder.bind(myQueue2()).to(directExchange()).with("我是007");
}
@Bean
Binding binding3 (){
return BindingBuilder.bind(myQueue2()).to(directExchange()).with("我同样是008");
}
}
为了加深理解,我没有使用
@RabbitListener直接创建绑定,也没有使用AmqpAdmin,我写了6个bean创建了这些东西:
- 1个
直连交换机。- 2个
队列。- 3个
绑定,将队列和交换机绑定在一起,并且设置了路由键。其中
第2个和第3个绑定队列交换机都一样,只是路由键不同。
第2个和第1个队列不同,但交换机相同?
发送消息及监听
发送消息
我们只要写上交换机的名字和路由键,它就会帮我们把消息发送到队列中去了。
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
rabbitTemplate.send("direct_exchange_demo" ,"我是007", MessageBuilder.withBody("我是消息1".getBytes()).build());
rabbitTemplate.send("direct_exchange_demo" ,"我同样是008", MessageBuilder.withBody("我是消息2".getBytes()).build());
}
监听
@Component
public class ListenerTest {
@RabbitListener(queues = {"queue_demo1","queue_demo2"})
public void consume(Message msg, @Header(AmqpHeaders.CONSUMER_QUEUE) Queue queue) {
System.out.println(queue.getName() + ":" + new String(msg.getBody()));
}
}
监听到的内容
queue_demo1:我是消息1
queue_demo2:我是消息1
queue_demo2:我是消息2
交换机在绑定队列的时候
可以给一个队列设置多个key,就像上面所见到的,queue_demo2这个队列收到了2条消息。
注意:如果监听到的消息不全,可以在test的窗口看,因为spring test在运行时本身就会走一遍源码的东西,也许是test那边的listener监听到了。
扇形交换机
记得最开始我说的:
交换机的作用就是为了和队列绑定,为什么没有带上路由键呢?
因为扇形交换机就是一个特殊的存在,它根本不关心路由键,只要发送给扇形交换机,那么和这个交换机绑定的所有队列都会收到消息,根本不管路由键,它的作用就是无差别广播。
这次我们用@RabbitListener直接创建+绑定+监听。
@RabbitListener 创建+绑定+监听
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
@Component
public class ListenerTest {
@RabbitListener(bindings = {
//注意这里的Queue和Header中到Queue不同,这里的Queue是个Annotation
@QueueBinding(value = @org.springframework.amqp.rabbit.annotation.Queue("queue1"), exchange = @Exchange(name= "fanout_exchange", type = "fanout")),
@QueueBinding(value = @org.springframework.amqp.rabbit.annotation.Queue("queue2"), exchange = @Exchange(name= "fanout_exchange", type = "fanout"))
})
public void consume(Message msg, @Header(AmqpHeaders.CONSUMER_QUEUE) Queue queue) {
System.out.println(queue.getName() + ":" + new String(msg.getBody()));
}
}
发送消息
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
//路由键直接为null
rabbitTemplate.send("fanout_exchange" ,null, MessageBuilder.withBody("我是消息".getBytes()).build());
}
我们发送消息的时候根本不去管路由键,但是和这个交换机绑定的2个队列,都会收到消息:
queue2:我是消息
queue1:我是消息
主题交换机
这是交换机中最复杂的交换机,直连交换机是很直接的,路由键绑定的是什么,它就找什么,而
主题交换机是可以泛型匹配路由键。
它的作用也比较重要,比如:有2个路由键分别是log.purchase和log.order。
此时如果是直连交换机,就需要发送2遍消息,而主题交换机只需要发送一次,直接匹配log.*。
路由键绑定在
topic交换机中的作用`
#匹配一个或者多个关键字,*匹配一个关键字- 绑定键为
*的队列会取到 info pay 这种绑定键消息- 绑定键为
#.的队列只会取到info. pay. 这种.后面没有单词的绑定键消息- 绑定键为
#.*的队列会取到 pay pay. pay.info pay.info.log 任意绑定键消息
创建及绑定
之前讲了
bean方式创建和绑定,还有@RabbitListener直接创建+绑定+监听,这次我们用AmqpAdmin来创建和绑定。
此次建立自动删除(断开连接就删除队列)和非持久化的匿名队列。
nonDurable或durable如果不指定要创建的队列名,就会随机生成,这叫匿名队列。
创建
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableRabbit
public class RabbitMQConfigure {
//因为匿名队列的原因,队列名是随机的,所以创建成bean,后面用spel表达式获取队列名
/*
所谓匿名队列,队列的名字就类似这样:
spring.gen-z3_6hvnjTYGLFkVT89JVQw
spring.gen-7I3jCFlGQkGttSa8Fcatfg
*/
@Bean
Queue queue1(){
return QueueBuilder.nonDurable().autoDelete().build();
}
@Bean
Queue queue2(){
return QueueBuilder.nonDurable().autoDelete().build();
}
}
监听
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class ListenerTest {
@Autowired
private AmqpAdmin amqpAdmin;
@Autowired
private Queue queue1;
@Autowired
private Queue queue2;
@PostConstruct
void createAndBound() {
//创建交换机
Exchange topic_exchange = ExchangeBuilder.topicExchange("topic_exchange").build();
amqpAdmin.declareExchange(topic_exchange);
//绑定
Binding log_purchase = BindingBuilder.bind(queue1).to(topic_exchange).with("#.").noargs();
amqpAdmin.declareBinding(log_purchase);
//绑定
Binding log_order = BindingBuilder.bind(queue2).to(topic_exchange).with("#.*").noargs();
amqpAdmin.declareBinding(log_order);
}
@RabbitListener(queues = {"#{queue1.getName()}", "#{queue2.getName()}"})
public void consume(Message msg, @Header(AmqpHeaders.CONSUMER_QUEUE) Queue queue) {
System.out.println(queue.getName() + ":" + new String(msg.getBody()));
}
}
发送消息
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
rabbitTemplate.send("topic_exchange" ,"log", MessageBuilder.withBody("我是log消息".getBytes()).build());
rabbitTemplate.send("topic_exchange" ,"log.", MessageBuilder.withBody("我是log.消息".getBytes()).build());
rabbitTemplate.send("topic_exchange" ,"log.shop", MessageBuilder.withBody("我是log.shop消息".getBytes()).build());
rabbitTemplate.send("topic_exchange" ,"log.shop.error", MessageBuilder.withBody("我是log.shop.error消息".getBytes()).build());
}
spring.gen-SAXc7tcdT4ysVNRnEesdjQ:我是log.消息
spring.gen-_-IuuEPKT6uV2yyghTJ42g:我是log消息
spring.gen-_-IuuEPKT6uV2yyghTJ42g:我是log.消息
spring.gen-_-IuuEPKT6uV2yyghTJ42g:我是log.shop消息
spring.gen-_-IuuEPKT6uV2yyghTJ42g:我是log.shop.error消息
第一个队列只能收到
我是log.消息,而第二个队列可以收到所有消息。
Producer 尽量确保消息不丢失
默认情况下使用
rabbitTemplate.send或rabbitTemplate.convertAndSend,无法确认消息是否在发送的时候丢失,也许队列满了,也许网络原因没有真的发送到,所以我们需要确保消息不会在发送时丢失(注意:只能尽力保证,无法100%,没有任何一个存储数据库或中间件能100%保证,因为就算触发回调做了处理,也许回调时服务器挂了。)
我们需要实现
RabbitTemplate.ConfirmCallback处理消息没有到达交换机,数据丢失的情况RabbitTemplate.ReturnCallback消息到达了交换机,但是没有根据路由键找到队列,3种可能,①交换机没有绑定队列 ②交换机根据路由键没有匹配到队列 ③队列消息已满
rabbitTemplate.setConfirmCallback; 设置producer的确认回调rabbitTemplate.setReturnCallback; 设置找不到队列打回消息的回调
设置完后还需要改一下配置才能生效
spring.rabbitmq.template.mandatory=truespring.rabbitmq.publisher-confirm-type=correlatedspring.rabbitmq.publisher-returns=true
注意:ConfirmType默认为None,具体看
org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType
栗子
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
@Component
public class MyProducer implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
private final RabbitTemplate rabbitTemplate;
public MyProducer(RabbitTemplate rabbitTemplate){
/*设置回调*/
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
//写个send
public void send(String exchangeName,String routingKey, Message msg, String msgID){
//设置关联信息,关联信息可以是订单号,UUID等等,确保唯一,这样在confirm回调中可以重新根据ID重新发送或者自己弄个队列延迟处理
CorrelationData correlationData = new CorrelationData(msgID);
rabbitTemplate.send(exchangeName, routingKey, msg, correlationData);
}
//写个convertAndSend
public void convertAndSend(String exchangeName,String routingKey, Object msg, String msgID){
//设置关联信息,关联信息可以是订单号,UUID等等,确保唯一,这样在confirm回调中可以重新根据ID重新发送或者自己弄个队列延迟处理
CorrelationData correlationData = new CorrelationData(msgID);
//注意:消息的后置处理器(MessagePostProcessor) 可以要,也可以不要
rabbitTemplate.convertAndSend(exchangeName, routingKey, msg, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);//设置消息持久化
return message;
}
}, correlationData);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("发送成功");
}else{
//处理消息没有到达交换机,数据丢失的情况
String msgID = correlationData.getId();
System.out.println("处理消息没有到达交换机:" + cause + msgID);
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
//消息到达交换机,但是没有根据路由键找到队列
System.out.println("没有找到队列的消息:" + message + " replyCode " + replyCode + " replyText " + replyText + " exchange " + exchange + " routingKey " + routingKey);
}
}
模拟失败消息
@Autowired
private MyProducer myProducer;
@Test
void contextLoads() {
//2条模拟非正常的消息
myProducer.send("不存在的交换机","log", MessageBuilder.withBody("乱发消息".getBytes()).build(),"123");
//这里需要把监听器的#.*改成*,不然不会找不到路由键的,做测试时需要注意
myProducer.send("topic_exchange","a.a.a.a.a.不存在的路由键", MessageBuilder.withBody("乱发消息".getBytes()).build(),"321");
}
控制台信息
其中发送成功就是那个无效路由键的,因为确实到了交换机,只是没找到队列。
处理消息没有到达交换机:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange '不存在的交换机' in vhost '/', class-id=60, method-id=40)
没有找到队列的消息:(Body:'[B@160549d0(byte[9])' MessageProperties [headers={spring_returned_message_correlation=321}, contentType=application/octet-stream, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0]) replyCode 312 replyText NO_ROUTE exchange topic_exchange routingKey a.a.a.a.a.不存在的路由键
发送成功
事务方式
除了上面的异步确认方式,也可以手动使用事务来确认是否发送成功,但是注意,如果使用事务,全局设置中的
spring.rabbitmq.publisher-confirm-type=correlated,要删除,并且事务方式也可以手动同步确认,推荐上面的异步方式处理,事务这里我就一笔带过了。
栗子
@Autowired
RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
Channel channel = rabbitTemplate.getConnectionFactory().createConnection().createChannel(true); //开启事务通道
try {
channel.txSelect();
channel.basicPublish("不存在的交换机", "log", false, false, MessageProperties.PERSISTENT_BASIC,"乱发消息".getBytes());
channel.basicPublish("topic_exchange","log", false, false, MessageProperties.PERSISTENT_BASIC,"正常消息".getBytes());
channel.txCommit();
} catch (IOException e) {
try {
channel.txRollback();
} catch (IOException ex) {
System.out.println("txRollback error...");
}
System.out.println("txCommit error...");
e.printStackTrace();
}
}
此时2条消息都不会发送出去,因为触发了异常。
Spring中使用事务方式很简单
@Configuration
public class RabbitConfig {
//配置rabbitmq事务管理器
@Bean("rabbitTransactionManager")
public RabbitTransactionManager rabbitTransactionManager(CachingConnectionFactory connectionFactory) {
return new RabbitTransactionManager(connectionFactory);
}
}
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
@Component
public class MyProducer implements RabbitTemplate.ReturnCallback {
private final RabbitTemplate rabbitTemplate;
public MyProducer(RabbitTemplate rabbitTemplate){
/*设置回调*/
this.rabbitTemplate = rabbitTemplate;
//事务方式确认回调无法使用 之前说过
//rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
//写个send
@Transactional(rollbackFor = Exception.class,transactionManager = "rabbitTransactionManager")
public void send(String exchangeName,String routingKey, Message msg, String msgID){
//设置关联信息,关联信息可以是订单号,UUID等等,确保唯一,这样在confirm回调中可以重新根据ID重新发送或者自己弄个队列延迟处理
CorrelationData correlationData = new CorrelationData(msgID);
rabbitTemplate.send(exchangeName, routingKey, msg, correlationData);
}
//写个convertAndSend
@Transactional(rollbackFor = Exception.class,transactionManager = "rabbitTransactionManager")
public void convertAndSend(String exchangeName,String routingKey, Object msg, String msgID){
//设置关联信息,关联信息可以是订单号,UUID等等,确保唯一,这样在confirm回调中可以重新根据ID重新发送或者自己弄个队列延迟处理
CorrelationData correlationData = new CorrelationData(msgID);
//注意:消息的后置处理器(MessagePostProcessor) 可以要,也可以不要
rabbitTemplate.convertAndSend(exchangeName, routingKey, msg, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);//设置消息持久化
return message;
}
}, correlationData);
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
//消息到达交换机,但是没有根据路由键找到队列
System.out.println("没有找到队列的消息:" + message + " replyCode " + replyCode + " replyText " + replyText + " exchange " + exchange + " routingKey " + routingKey);
}
}
Consumer消息确认Confirm机制
在
Spring中RabbitMQ的consumer消息是自动确认的,如何进行手动确认呢?
因为我们不光要保证消息是否在传输过程中丢失,也同样要保证如果消费时发生异常是否也能让消息不丢失。
设置ackMode后通过deliveryTag和channel就可以手动确认消息
@RabbitListener(queues = {"queue_demo1","queue_demo2"},ackMode = "MANUAL")
public void consume(Message msg, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, @Header(AmqpHeaders.CHANNEL) Channel channel) {
try {
//拒绝消息
channel.basicNack(deliveryTag, false, false);
//确认消息
//channel.basicAck(deliveryTag,false);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(new String(msg.getBody()));
}
channel.basicNack(deliveryTag, false, false);
第二个参数是否批量拒绝,也就是说拒绝所有在deliveryTag之前的消息(包含本条消息)
第三个参数是否重回队列,false为就是要丢弃channel.basicReject(deliveryTag, false);
拒绝单条消息,第二个参数是否重回队列channel.basicAck(deliveryTag, false);
确认消息,第二个参数为是否批量确认所有在deliveryTag之前的消息(包含本条消息)

本文深入讲解RabbitMQ的三种交换机类型及其应用场景,包括直连、扇形和主题交换机。同时,详细介绍了Producer如何确保消息不丢失的策略,如Confirm机制和事务处理,以及Consumer的消息确认机制。
656

被折叠的 条评论
为什么被折叠?



