SpringBoot整合RabbitMQ之交换机,确认与通知,持久化

梗概

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回调。了解了这个之后,就可以在该方法上根据具体的业务场景进行不同的操作了~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章