博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

RabbitMq

Posted on 2020-07-13 19:48  shyHorse  阅读(168)  评论(0编辑  收藏  举报

一、连接和通道

@Configuration
public class RabbirnqConfig {
    private static final String RABBIT_HOST = "localhost";
    private static final String RABBIT_USERNAME = "root";
    private static final String RABBIT_PASSWORD = "root";
    private static final int RABBIT_PORT = 5672;

    @Bean
    public static  Connection getConnection(){
            Connection connection = null;
            ConnectionFactory connectionFactory = new ConnectionFactory();

            //连接rabbitMq,第一种方式,通过分配属性
            connectionFactory.setHost(RABBIT_HOST);//主机名
            connectionFactory.setUsername(RABBIT_USERNAME);//用户名
            connectionFactory.setPassword(RABBIT_PASSWORD);//密码
            connectionFactory.setPort(RABBIT_PORT);//端口

            try {
                //连接rabbitMq,第二种方式,使用uri,如下会有默认值
                //connectionFactory.setUri("amqp:// userName:password @ hostName:portNumber / virtualHost");

                connection = connectionFactory.newConnection();
            }catch (Exception e){
                e.printStackTrace();
            }
        return connection;
    }
}

二、通过交换机进行消息传递的几种模式

  如果消息发送到没有队列绑定的交换机时,消息将丢失,因为交换机没有存储消息的能力,消息只能存在在队列中
  如果消息不通过交换机的,一条消息可能会有n个队列抢

  <一>、扇形交换机模式

        解释几个频繁使用的方法: 

       获取连接:Connection connection = RabbirnqConfig.getConnection();
        创建通道:Channel channel = connection.createChannel();
        声明交换机:channel.exchangeDeclare(EXCHANGE_NAME_FANOUT,"fanout",true,true,null);、
          exchangeDeclare有很多的方法重载。以参数最多的为例。此处列出每个参数,方便具体使用哪个重载方法时,里面用到的参数都可以在这里找到代表的意思
          DeclareOk exchangeDeclare(String var1, String var2, boolean var3, boolean var4, boolean var5, Map<String, Object> var6) throws IOException;
          var1(exchange):交换器名称
          var2(BuiltinExchangeType type):交换器类型,可选值:DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers")
          var3(boolean durable):是否持久化,durable设置为true表示持久化,反之是非持久化,持久化的可以将交换器存盘,在服务器重启的时候不会丢失信息
         var4(boolean autoDelete):是否自动删除,设置为TRUE则表是自动删除,自删除的前提是至少有一个队列或者交换器与这交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑,一般都设置为fase
         var5(boolean internal):是否内置,如果设置 为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器的方式
         var6(Map<String, Object> arguments):其它一些结构化参数比如:alternate-exchange

        消息发布:void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body)

exchange:要将消息发送到的Exchange(交换器)
routingKey:路由Key
mandatory:交换器无法根据自动的类型和路由键找到一个符合条件的队列,那么RabbitMq会调用Basic.Ruturn命令将消息返回给生产都,为false时,出现上述情况消息被直接丢弃
immediate: 如果交换器在消息路由到队列时发现没有任何消费者,那么这个消息将不会存和队列,当与路由匹配的所有队列都没有消费者时,会Basic.Return返回给生产者
props:其它的一些属性,如:{@link MessageProperties.PERSISTENT_TEXT_PLAIN}
body:消息内容

    1)生产者

public void fanoutPpro() throws Exception {//生产消息
    Connection connection = RabbirnqConfig.getConnection();
    //创建通道
    Channel channel = connection.createChannel();

    channel.exchangeDeclare(EXCHANGE_NAME_FANOUT,"fanout",true,true,null);

    String message = "Hello World!";

    channel.basicPublish(EXCHANGE_NAME_FANOUT,"",null,message.getBytes());
    System.out.println("生产者 send :"+message);
    channel.close();
    connection.close();
}

  

    2)  消费者

      几个方法解释:

创建队列:channel.queueDeclare(F_QUEUE_NAME,false,false,false,null); 此方法同一个队列名只能创建一次,如果再次接受消息是还是用该方法创建同一个队列名,会抛出异常(再次测试这个队列接受消息,把这个方法注释掉即可)
queueDeclare有多个重载方法,以参数最多的这个为例解释
com.rabbitmq.client.AMQP.Queue.DeclareOk queueDeclare(String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5) throws IOException;
  如果queueDeclare()不带参数,该方法默认创建一个由RabbitMq命名的名称,这种队列也称之为匿名队列,排他 的,自动删除的,非持久化的队列

  var1(queue):队列名称

  var2(durable):是否持久化, true ,表示持久化,会存盘,服务器重启仍然存在,false,非持久化

     var3(exclusive):exclusive : 是否排他的,true,排他。如果一个队列声明为排他队列,该队列会对首次声明它的连接可见,并在连接断开时自动删除;排他是基于连接的Connection可见的,同一个连接的不同信道是可以同时访问同一个连接创建的排他队列
               如果一个连接已经声明了一个排他队列,其它连接是不允许建立同名的排他队列,这个与普通队列不同,即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除

  var4(autoDelete);是否自动删除,true,自动删除,自动删除的前提:至少有一个消息者连接到这个队列,之后所有与这个队列连接的消息都断开时,才会自动删除;注意,生产者客户端创建这个队列,或者没有消息者客户端连接这个队列时,不会自动删除这个队列

  var5(arguments):其它一些参数。如:x-message-ttl,之类

绑定到交换机:channel.queueBind(F_QUEUE_NAME,EXCHANGE_NAME_FANOUT,"");

  public void fanoutCus1() throws Exception{
        Connection connection = RabbirnqConfig.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(F_QUEUE_NAME,false,false,false,null);
        channel.queueBind(F_QUEUE_NAME,EXCHANGE_NAME_FANOUT,"");

        channel.basicQos(5);//限制此通道预取数为5,当该队列没对接受到的消息进行回复时,不会再给其新的消息

        StringBuffer message = new StringBuffer();
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {

                super.handleDelivery(consumerTag, envelope, properties, body);
                String message = new String(body,"UTF-8");
                System.out.println("接到消息1:"+message);
          channel.basicAck(envelope.getDeliveryTag(),true);//手动回复确认
    } }; 
    channel.basicConsume(F_QUEUE_NAME,false,defaultConsumer);//对接收到的消息自动回复确认
}

    重点解释:

消费者消息确认方式分为:

  自动(交货无需确认,又称“开火即忘”)
  手动(送货需要客户确认)

  消息回复设置:channel.basicConsume(F_QUEUE_NAME,false,defaultConsumer);
    basicConsume(String var1, boolean var2, Consumer var3) throws IOException;
    当var2为true,表示自动回复。为false表示手动回复,此时需要与channel.basicQos()方法配合使用,限制消息交付数量,避免消费者超负荷

  channel.basicQos():限制此通道预取数,该值定义通道上允许的未确认交付的最大数量。通过使用basic.qos方法设置“预取计数”值。
            一旦数量达到配置的数量,RabbitMQ将停止在通道上传递更多消息,除非已确认至少一个未处理的消息。
  举例:现在通道里有1、2、3、4四个未确认交付的消息,队列channel里目前也有四个未确认的消息,且队列channel设置的预取值为2,
     现在chennel确认处理了其内部3个消息,那么就会再从通道取两个未确认的消息,因为其设置的预取值为2,即便channel里面还有一个空位也只会取2个消息

  手动回复确认:channel.basicAck();
  void basicAck(long var1, boolean var3) throws IOException;
    var1(deliveryTag):该消息的id,即RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID
    var2(multiple):是否批量.true:将一次性处理所有小于等于传入值的所有消息 

 

  <二>、路由,direct,由路由键决定发到对应的队列,有几个和路由规则匹配的,就发几份数据去往队列

处理无法路由的消息

如果发布的消息设置了“强制”标志,但无法路由,则代理会将其返回给发送客户端(通过AMQP.Basic.Return 命令)。

通知这样的回报,客户可以实现ReturnListener 接口并调用Channel.addReturnListener。如果客户端尚未为特定通道配置返回侦听器,则关联的返回消息将被静默删除。

channel.addReturnListener(new ReturnListener(){
  public void handleReturn (int replyCode,
              字符串replyText,
              字符串交换,
              字符串routingKey,
              AMQP.BasicProperties属性,
              字节 []正文)
  抛出 IOException{
    ...
  }
});
例如,如果客户端发布的消息中将“强制”标志设置为不绑定到队列的“直接”类型的交换,则将调用返回侦听器。

 

备用交换机

备用交换器,从本质上说也就是一个普通交换器。当消息发送到普通交换器而没有队列绑定路由键的时候。该消息就会发送到备用交换器上面。如果备用交换器上面也没得队列绑定,那么这条消息也将被丢弃(如果配置了失败通知,那么将回调失败通知方法channel.addReturnListener())

         Map<String,Object> argsMap = new HashMap<String,Object>();
         argsMap.put("alternate-exchange",BACK_EXCHANGE_NAME);//定义主交换器的备用交换器名称
         channel.exchangeDeclare(EXCHANGE_NAME_DIRECT,"direct",false,false,argsMap);//创建主交换器
         channel.exchangeDeclare(BACK_EXCHANGE_NAME,BuiltinExchangeType.DIRECT);//创建备用交换器

      1、生产者

  public void directPpro() throws Exception {
        Connection connection = RabbirnqConfig.getConnection();
        //创建通道
        Channel channel = connection.createChannel();

        //备用交换器,从本质上说也就是一个普通交换器。当消息发送到普通交换器而没有队列绑定路由键的时候。该消息就会发送到备用交换器上面。如果备用交换器上面也没得队列绑定,那么这条消息也将被丢弃(如果配置了失败通知,那么将回调失败通知方法channel.addReturnListener())
        Map<String,Object> argsMap = new HashMap<String,Object>();
        argsMap.put("alternate-exchange",BACK_EXCHANGE_NAME);//定义主交换器的备用交换器名称

        channel.exchangeDeclare(EXCHANGE_NAME_DIRECT,"direct",false,false,argsMap);//创建主交换器

        channel.exchangeDeclare(BACK_EXCHANGE_NAME,BuiltinExchangeType.DIRECT);//创建备用交换器

        //创建队列
        //channel.queueDeclare(QUEUE_NAME,false,false,false,null);

//        for (int i = 0; i < 20; i++) {
//            message = message + i;
//            channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
//            Thread.sleep(1000);
//        }
        String message = "这是消息B";
        channel.basicPublish(EXCHANGE_NAME_DIRECT, "B", true,false,null, message.getBytes());
        String messageA = "这是消息A";
        channel.basicPublish(EXCHANGE_NAME_DIRECT, "A", true,false,null, messageA.getBytes());
        String messageC = "这是消息C";
        channel.basicPublish(EXCHANGE_NAME_DIRECT, "C", true,false,null, messageC.getBytes());
        System.out.println("生产者 send :"+message);
        System.out.println("生产者 send :"+messageA);
        System.out.println("生产者 send :"+messageC);


        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int i, String s, String s1, String s2, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
                System.out.println("消息没法路由:"+i+"-"+s+"-"+s1+"-"+s2);
            }
        });

        channel.close();
        connection.close();
    }

  

  

      2、消费者

    public void directCus1() throws Exception{
        Connection connection = RabbirnqConfig.getConnection();
        Channel channel = connection.createChannel();
        //channel.queueDeclare(QUEUE_NAME1,false,false,false,null);
        channel.exchangeDeclare(EXCHANGE_NAME_DIRECT,"direct");
        channel.queueBind(QUEUE_NAME1,EXCHANGE_NAME_DIRECT,"A");

        channel.basicQos(1);//当该队列没对接受到的消息进行回复时,不会再给其新的消息

        StringBuffer message = new StringBuffer();
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {

                super.handleDelivery(consumerTag, envelope, properties, body);
                String message = new String(body,"UTF-8");
                System.out.println("接到消息A:"+message);
            }
        };
        channel.basicConsume(QUEUE_NAME1,true,defaultConsumer);//对接收到的消息自动回复确认
    }

  

  public void directCus2() throws Exception{
        Connection connection = RabbirnqConfig.getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare("test_queueBack",true,false,false,null);
        //channel.exchangeDeclare(BACK_EXCHANGE_NAME,"direct");
        channel.queueBind("test_queueBack",BACK_EXCHANGE_NAME,"C");

        channel.basicQos(1);//限制此通道预取数为1,当该队列没对接受到的消息进行回复时,不会再给其新的消息

        StringBuffer message = new StringBuffer();
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {

                super.handleDelivery(consumerTag, envelope, properties, body);
                String message = new String(body,"UTF-8");
                System.out.println("接到消息C:"+message);
                channel.basicAck(envelope.getDeliveryTag(),false);//手动回复确认
                //deliveryTag:该消息的index    multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
                channel.basicReject(envelope.getDeliveryTag(),true);
            }
        };
        channel.basicConsume("test_queueBack",false,defaultConsumer);//对接收到的消息不自动回复确认,需手动回复
    }

  

  

  <三>、订阅,会把消息发到各订阅该主题的队列中,该队列连接后即可取到

        

检索单个消息

可以按需检索单个消息(“拉API”又称轮询)。这种消耗方式效率极低,因为它有效地轮询并且即使绝大多数请求都没有结果,应用程序也必须反复询问结果。因此,强烈建议不要使用此方法。

要“拉”消息,请使用Channel.basicGet方法。返回的值是GetResponse的实例,可以从中提取标头信息(属性)和消息正文:

boolean autoAck = false ;
GetResponse响应= channel.basicGet(queueName,autoAck);
if(response == null){
  //未检索到消息。
} 其他 {
  AMQP.BasicProperties props = response.getProps();
  字节 []正文= response.getBody();
  长 deliveryTag = response.getEnvelope()。getDeliveryTag();
  // ...
  并且由于此示例使用了手动确认(上面的autoAck = false),因此您还必须调用Channel.basicAck以确认您已成功接收到消息:

  // ...
  channel.basicAck(method.deliveryTag,false); //确认收到消息
}

     1、生产者

public void topicPro() throws Exception{
        Connection connection = RabbirnqConfig.getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME_TOPIC,"topic");
        String message = "主题订阅";
        channel.basicPublish(EXCHANGE_NAME_TOPIC,"a.b",false,false,null,message.getBytes());
        System.out.println("已发送:"+message);
        channel.close();
        connection.close();
    }

      2、消费者

public void topicCus1() throws Exception{
        Connection connection = RabbirnqConfig.getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare("topic_cus1",false,false,false,null);
        channel.exchangeDeclare(EXCHANGE_NAME_TOPIC,"topic");
        channel.queueBind("topic_cus1",EXCHANGE_NAME_TOPIC,"a.*");
        channel.basicQos(1);

        Consumer consumer = new DefaultConsumer(channel) {
             @Override
             public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                 throws IOException {
                     super.handleDelivery(consumerTag, envelope, properties, body);
                     System.out.println("t1: "+new String(body,"UTF-8"));
                 }
         };

        channel.basicConsume("topic_cus1",true,consumer);
    }

 

public void topicCus2() throws Exception{
        Connection connection = RabbirnqConfig.getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare("topic_cus2",false,false,false,null);
        channel.exchangeDeclare(EXCHANGE_NAME_TOPIC,"topic");
        channel.queueBind("topic_cus2",EXCHANGE_NAME_TOPIC,"*.b");
        channel.basicQos(1);
        GetResponse response = channel.basicGet("topic_cus2", false);
        if(response == null){
            System.out.println("---------------没消息进来");
        }
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("T2:"+new String(body,"UTF-8"));
                channel.basicAck(envelope.getDeliveryTag(),false);

            }
        };

        channel.basicConsume("topic_cus2",false,consumer);
    }

  

三、生产中经常考虑的问题

关于消息堆积问题解决方案:

1、消息堆积可能是生产者发送消息频率高于消费者处理消息,可以考虑优化消费者代码,或者降低生产者消息发布频率

2、队列长度限制,队列的最大长度可以限制为一定数量的消息,当设置了最大队列长度,RabbitMQ的默认行为是从队列的最前面丢弃消息或已死消息

    可以通过为x-max-length队列声明参数提供非负整数值来设置最大消息数。
    也可以通过为x-max-length-bytes队列声明参数提供非负整数值来设置最大长度(以字节 为单位)
    如下示例:该队列的最大长度为10条消息
    Map <String,Object> args = new HashMap <String,Object>();
    args.put(“ x-max-length”,10);
    channel.queueDeclare(“ myqueue”,false,false,false,args);
    此处注意:如果生产者启用了消息确认,即channel.confirmSelect(),则消费者需要用basicNack()方法通知发布者拒绝,该消息仍将发布到所有其他可以排队的队列

3、给消息设置有效时间,超时就丢弃

    方法一:
    通过在 queue.declare 中设置 x-message-ttl 参数,可以控制被 publish 到 queue 中的 message 被丢弃前能够存活的时间
    如下示例:message 在该 queue 的存活时间最大为 60 秒
    Map<String, Object> args = new HashMap<String, Object>();
    args.put("x-message-ttl", 60000);
    channel.queueDeclare("myqueue", false, false, false, args);

    方法二:
    通过 basic.publish 命令发送 message 时设置 expiration 字段,以微秒为单位表示 TTL 值,与 x-message-ttl 具有相同的约束条件
    如下示例:
    byte[] messageBodyBytes = "Hello, world!".getBytes();
    AMQP.BasicProperties properties = new AMQP.BasicProperties();
    properties.setExpiration("60000");
    channel.basicPublish("myexchange", "routingkey", properties, messageBodyBytes);

    注意:当同时指定了 queue 和 message 的 TTL 值,则两者中较小的那个才会起作用

    方法三:
    queue.declare命令中的x-expires参数控制queue被自动删除前可以处于未使用状态的时间。未使用的意思是queue上没有任何消费者,队列没有被重新声明,并且在过期时间段内未调用过basic.get命令。该方式可用于,例如,RPC-style的回复队列,其中许多队列会被创建出来,但是却从未被使用。 
    服务器会确保在过期时间到达后排队被删除,但是不保证删除的动作有多么的及时。在服务器重启后,持久化的队列的超时时间将重新计算。
    用于表示超期时间的x-expires参数值以微秒为单位,并且服从和x-message-ttl一样的约束条件,且不能设置为0.所以,如果该参数设置为4000,则表示该队列如果在4秒钟之内未被使用则会被删除
    如下示例:
    Map<String, Object> args = new HashMap<String, Object>();
    args.put("x-expires", 4000);
    channel.queueDeclare(QUEUE_NAME, false, false, false, args);

4、设置并发消费两个关键属性concurrentConsumers和prefetchCount

     concurrentConsumers设置的是对每个listener在初始化的时候设置的并发消费者的个数
    prefetchCount是每次一次性从broker里面取的待消费的消息的个数,有的版本里也叫batchSize
    配置方法:修改application.properties:
    spring.rabbitmq.listener.concurrency=m
    spring.rabbitmq.listener.prefetch=n

5、使用缓存:

     生产者端缓存数据,在mq被消费完后再发送到mq,打破发送循环条件。设置合适的qos值(channel.BasicQos()方法:每次从队列拉取的消息数量),当qos值被用光,而新的ack没有被mq接收时,就可以跳出发送循环,去接收新的消息。
    消费者主动block接收进程,消费者感受到接收消息过快时主动block,利用block和unblock方法调节接收速率,当接收线程被block时,跳出发送循环。

RabbitMQ的消息丢失解决方案

    1、消息持久化:Exchange 设置持久化:durable:true;Queue 设置持久化;Message持久化发送。
    2、ACK确认机制:消息发送确认;消息接收手动确认ACK。



面试题:转载 https://blog.csdn.net/jerryDzan/article/details/89183625