RabbitMq消息中间件实战使用记录

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 小于等于传入值的所有消息

猜你喜欢

转载自blog.csdn.net/u012565281/article/details/113047993