AMQP RabbitMQ 新手入门 三种交换机及Producer+Consumer的消息确认机制+事务

34 篇文章 0 订阅
8 篇文章 0 订阅
本文深入讲解RabbitMQ的三种交换机类型及其应用场景,包括直连、扇形和主题交换机。同时,详细介绍了Producer如何确保消息不丢失的策略,如Confirm机制和事务处理,以及Consumer的消息确认机制。
摘要由CSDN通过智能技术生成

引言

之前写过RabbitMQ的消息打回机制和死信队列,这次准备详细讲一下RabbitMQ,主要是给新手写的入门用的。
之前的文章:


Spring 依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • RabbitMQ没有Kafkapartitiongroup,虽然在之前使用Stream整合RabbitMQ的时候,yml里配置写的是group,但其实RabbitMQ中没有grouppartition这种东西。
  • RabbitMQ中只有
  1. Exchange(交换机)
  2. Queue(队列)
  3. 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.purchaselog.order
此时如果是直连交换机,就需要发送2遍消息,而主题交换机只需要发送一次,直接匹配log.*。

路由键绑定在topic交换机中的作用`

  • #匹配一个或者多个关键字,*匹配一个关键字
  • 绑定键为 * 的队列会取到 info pay 这种绑定键消息
  • 绑定键为 #. 的队列只会取到info. pay. 这种.后面没有单词的绑定键消息
  • 绑定键为 #.* 的队列会取到 pay pay. pay.info pay.info.log 任意绑定键消息

创建及绑定

之前讲了bean方式创建和绑定,还有@RabbitListener直接创建+绑定+监听,这次我们用AmqpAdmin来创建和绑定。
此次建立自动删除(断开连接就删除队列)非持久化匿名队列

  • nonDurabledurable如果不指定要创建的队列名,就会随机生成,这叫匿名队列

创建

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.sendrabbitTemplate.convertAndSend,无法确认消息是否在发送的时候丢失,也许队列满了,也许网络原因没有真的发送到,所以我们需要确保消息不会在发送时丢失(注意:只能尽力保证,无法100%,没有任何一个存储数据库或中间件能100%保证,因为就算触发回调做了处理,也许回调时服务器挂了。)
我们需要实现

  • RabbitTemplate.ConfirmCallback 处理消息没有到达交换机,数据丢失的情况
  • RabbitTemplate.ReturnCallback 消息到达了交换机,但是没有根据路由键找到队列,3种可能①交换机没有绑定队列 ②交换机根据路由键没有匹配到队列 ③队列消息已满

  • rabbitTemplate.setConfirmCallback; 设置producer的确认回调
  • rabbitTemplate.setReturnCallback; 设置找不到队列打回消息的回调

设置完后还需要改一下配置才能生效

  • spring.rabbitmq.template.mandatory=true
  • spring.rabbitmq.publisher-confirm-type=correlated
  • spring.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机制

SpringRabbitMQconsumer消息是自动确认的,如何进行手动确认呢?
因为我们不光要保证消息是否在传输过程中丢失,也同样要保证如果消费时发生异常是否也能让消息不丢失
设置ackMode后通过deliveryTagchannel就可以手动确认消息

	@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之前的消息(包含本条消息)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没事干写博客玩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值