使用RabbitMQ原生API实现消息的收发

452 阅读21分钟

一、使用RabbitMQ原生API

使用RabbitMQ提供的原生客户端API进行交互。先来了解下如何使用Classic和Quorum队列。至于Stream队列,目前他使用的是和这两个队列不同的客户端,所以会在后面单独讨论。

1-1、maven依赖

创建一个Maven项目,引入依赖

<dependency>  
    <groupId>com.rabbitmq</groupId>  
    <artifactId>amqp-client</artifactId>  
    <version>5.9.0</version>  
</dependency>

1-2、创建发送程序(生产者)

发送程序,首先需要有一个connection,创建完connection就需要创建一个channel,有了channel就需要创建一个队列,这样就可以想队列中发送消息了。

1-2-1、创建一个连接RabbitMQ的工具类

通过下面这个工具类,就可以实现对RabbitMQ的连接,里面声明了host及端口,同时也设置了用户名密码以及我们要使用的virtualHost

public class RabbitMQUtil {

   private static Connection connection;
   private static final String HOST_NAME="192.168.253.131";
   private static final int HOST_PORT=5672;
   
   private RabbitMQUtil() {}
   
   public static Connection getConnection() throws Exception {
      if(null == connection) {
         ConnectionFactory factory = new ConnectionFactory();
         factory.setHost(HOST_NAME);
         factory.setPort(HOST_PORT);
         factory.setUsername("admin");
         factory.setPassword("admin");
         factory.setVirtualHost("/mirror");
         connection = factory.newConnection();
      }
      return connection;
   }
}

1-2-2、编写发送方法

发送消息主要分为以下几步:

  • 1、首先通过上面工具类创建一个连接

  • 2、然后再创建一个channel

  • 3、接着就是声明队列(创建队列有相关参数,可看下代码中的注释),关键代码:

    3-1、 普通队列

channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments);

api说明:

Declare a queue
Parameters:
queue the name of the queue
durable true if we are declaring a durable queue (the queue will survive a server restart)
exclusive true if we are declaring an exclusive queue (restricted to this connection)
autoDelete true if we are declaring an autodelete queue (server will delete it when no longer in use)
arguments other properties (construction arguments) for the queue
Returns:
a declaration-confirm method to indicate the queue was successfully declared
Throws:
java.io.IOException - if an error is encountered
See Also:
com.rabbitmq.client.AMQP.Queue.Declare
com.rabbitmq.client.AMQP.Queue.DeclareOk

3-2、Quorum队列

如果要声明一个Quorum队列,则只需要在后面的arguments中传入一个参数,x-queue-type,参数值设定为quorum

Map<String,Object> paramsnew HashMap<>();  
params.put("x-queue-type","quorum");  
//声明Quorum队列的方式就是添加一个x-queue-type参数,指定为quorum。默认是classic  
channel.queueDeclare(QUEUE_NAME, truefalsefalse, params);

注意:1、对于Quorum类型,durable参数就必须是true了,设置成false的话,会报错。同样,exclusive参数必须设置为false

3-3、Quorum队列

如果要声明一个Stream队列,则 x-queue-type参数要设置为 stream .

Map<String,Object> paramsnew HashMap<>();  
        params.put("x-queue-type","stream");  
        params.put("x-max-length-bytes"20_000_000_000L); // maximum stream size: 20 GB  
        params.put("x-stream-max-segment-size-bytes"100_000_000); // size of segment files: 100 MB  
        channel.queueDeclare(QUEUE_NAME, truefalsefalse, params);

注意:1、同样,durable参数必须是true,exclusive必须是false。 --你应该会想到,对于这两种队列,这两个参数就是多余的了,未来可以直接删除。

2、x-max-length-bytes 表示日志文件的最大字节数。x-stream-max-segment-size-bytes 每一个日志文件的最大大小。这两个是可选参数,通常为了防止stream日志无限制累计,都会配合stream队列一起声明。

声明的队列,如果服务端没有,那么会自动创建。但是如果服务端有了这个队列,那么声明的队列属性必须和服务端的队列属性一致才行,否则就会抛异常

4、通过channel给队列发送消息,关键代码如下:

channel.basicPublish(String exchange, String routingKey, BasicProperties props,message.getBytes("UTF-8")) ;

api说明:
Publish a message. Publishing to a non-existent exchange will result in a channel-level protocol exception, which closes the channel. Invocations of Channel#basicPublish will eventually block if a resource-driven alarm is in effect.
Parameters:
exchange the exchange to publish the message to
routingKey the routing key
props other properties for the message - routing headers etc
body the message body

其中exchange是一个Producer与queue的中间交互机制。可以让Producer把消息按一定的规则发送到不同的queue,不需要的话就传空字符串

5、关闭channel及connection

注:QUEUE_NAME为自定的队列名称

public static void sendBasic() throws Exception {
   Connection connection = RabbitMQUtil.getConnection();
   Channel channel = connection.createChannel();
   //声明队列会在服务端自动创建。
   //参数分别为:队列名称、是否持久化、是否独占、是否用完之后自动删除、相关参数
   channel.queueDeclare(QUEUE_NAME, false, false, false, null);
   //设置消息内容
   String message = "Hello World!333";
   //发送消息
   channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
   //输出打印的消息
   System.out.println(" [x] Sent '" + message + "'");
   //关闭channel及connection
   channel.close();
   connection.close();
}

1-2-3、运行以上方法就可以想MQ发送消息了,如下:

如下:折线图中标记了队列中的消息 image.png

1-3、创建读取消息程序(消费者)

定义消费者,消费消息进行处理,并向RabbitMQ进行消息确认。确认了之后就表明这个消息已经消费完了,否则RabbitMQ还会继续让别的消费者实例来处理

消息消费分为两种,分别为主动去MQ中获取消息,还有一种是等待MQ推送消息,下面都来分别实现一下。

1-3-1、主动拉取消息

从MQ拉取消息时,需要注意声明队列的方式必须和发送消息时声明队列的方式一致(比如是否持久化、是否独享等)

核心通过channel.basicGet方法获取参数,第一个参数为队列名称,第二个参数为,消费完消息是否自动应答(删除)。

public static void pullMsgBasic() throws Exception {
   Connection connection = RabbitMQUtil.getConnection();
   Channel channel = connection.createChannel();

   //队列的声明方式必须保持一致
   channel.queueDeclare(QUEUE_NAME, false, false, false, null);

   //获取消息
   GetResponse response = channel.basicGet(QUEUE_NAME, false);
   if(null != response){
      System.out.println(new String(response.getBody(),"UTF-8"));
   }
   channel.close();
   connection.close();
}

如下可以看到在IDE控制台可以获得上面发送的消息

image.png

如下在MQ控制台可以看到拉取消息的记录

image.png

可以通过设置第二个参数为true就可以删除消息了(下面就不做演示了)

GetResponse response = channel.basicGet(QUEUE_NAME, true);

1-3-2、等待MQ 推送消息

通过使用Consumer的实现类DefaultConsumer,并重写handleDelivery方法就可以获取MQ的相关消息了。

public static void pushMsgBasic() throws Exception {
   Connection connection = RabbitMQUtil.getConnection();
   Channel channel = connection.createChannel();
   channel.queueDeclare(QUEUE_NAME, false, false, false, null);
   //Consumer接口实现DefaultConsumer,通过DefaultConsumer获取推送的消息
   Consumer myconsumer = new DefaultConsumer(channel) {
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope,
                           BasicProperties properties, byte[] body)
            throws IOException {
         System.out.println("========================");
         String routingKey = envelope.getRoutingKey();
         System.out.println("routingKey >"+routingKey);
         String contentType = properties.getContentType();
         System.out.println("contentType >"+contentType);
         long deliveryTag = envelope.getDeliveryTag();
         System.out.println("deliveryTag >"+deliveryTag);
         System.out.println("content:"+new String(body,"UTF-8"));
         System.out.println("messageId:"+properties.getMessageId());

         //消息处理完后,进行答复。答复过的消息,服务器就不会再次转发。
         //没有答复过的消息,服务器会一直不停转发。
         //一次性是否对多条消息应答,当为false,则没一条消息都应答一次,如果未true则会积累一批一次确认
         channel.basicAck(deliveryTag, false);
      }
   };

   channel.basicConsume(QUEUE_NAME, false, myconsumer);
}

channel.basicConsume(QUEUE_NAME, false, myconsumer); 第二个参数autoAck是个关键。autoAck为true则表示消息发送到该Consumer后就被Consumer消费掉了,不需要再往其他Consumer转发。为false则会继续往其他Consumer转发。
要注意如果每个Consumer一直为false,会导致消息不停的被转发,不停的吞噬系统资源,最终造成宕机。

channel.basicAck(deliveryTag, false);:一次性是否对多条消息应答,当为false,则没一条消息都应答一次,如果未true则会积累一批一次确认

通过以上可以初步的了解到RabbitMQ的使用场景,下面看一下官方都提供了哪些场景。

1-4、官网的消息场景

原生API重点就是学习并理解RabbitMQ的官方消息模型。具体参见 www.rabbitmq.com/getstarted.… 。其中可以看到,RabbitMQ官方提供了总共七种消息模型,这其中,6 RPC部分是使用RabbitMQ来实现RPC远程调用,这个场景通常不需要使用MQ来实现,所以也就不当作重点来了解。而7 Publisher Confirms是当前版本新引进来的一种消息模型,对保护消息可靠性有很重要的意义。

这些消息模型基本上涵盖了日常开发中的绝大部分场景,而对于他们的API使用,其实都是大同小异,非常容易上手,并且在实际开发中,一般也有其他更好的框架来整合RabbitMQ使用 。所以对这一部分,理解业务场景是最为重要的。

image.png

1-4-1、hello world体验

image.png

最直接的方式,P端发送一个消息到一个指定的queue,中间不需要任何exchange规则。C端按queue方式进行消费。
关键代码:(其实关键的区别也就是几个声明上的不同。)

producer:

channel.queueDeclare(QUEUE_NAME,false,false,false,null);  
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));

consumer:

channel.queueDeclare(QUEUE_NAME, falsefalsefalsenull);

1-4-2、Work queues 工作序列

这就是kafka同一groupId的消息分发模式 image.png 工作任务模式,领导部署一个任务,由下面的一个员工来处理。

producer:

channel.queueDeclare(TASK_QUEUE_NAME, truefalsefalsenull); //任务一般是不能因为消息中间件的服务而被耽误的,所以durable设置成了true,这样,即使rabbitMQ服务断了,这个消息也不会消失  
channel.basicPublish("", TASK_QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,  
        message.getBytes("UTF-8"));

Consumer:

channel.queueDeclare(TASK_QUEUE_NAME, truefalsefalsenull);  
channel.basicQos(1);  
channel.basicConsume(TASK_QUEUE_NAME, false, consumer);

这个模式应该是最常用的模式,也是官网讨论比较详细的一种模式,所以官网上也对这种模式做了重点讲述。

  • 首先。Consumer端的autoAck字段设置的是false,这表示consumer在接收到消息后不会自动反馈服务器已消费了message,而要改在对message处理完成了之后,再调用channel.basicAck来通知服务器已经消费了该message.这样即使Consumer在执行message过程中出问题了,也不会造成message被忽略,因为没有ack的message会被服务器重新进行投递。
    但是,这其中也要注意一个很常见的BUG,就是如果所有的consumer都忘记调用basicAck()了,就会造成message被不停的分发,也就造成不断的消耗系统资源。这也就是 Poison Message(毒消息)
  • 其次,官方特意提到的message的持久性。关键的message不能因为服务出现问题而被忽略。还要注意,官方特意提到,所有的queue是不能被多次定义的。如果一个queue在开始时被声明为durable,那在后面再次声明这个queue时,即使声明为 not durable,那这个queue的结果也还是durable的。
  • 然后,是中间件最为关键的分发方式。这里,RabbitMQ默认是采用的fair dispatch,也叫round-robin模式,就是把消息轮询,在所有consumer中轮流发送。这种方式,没有考虑消息处理的复杂度以及consumer的处理能力。而他们改进后的方案,是consumer可以向服务器声明一个prefetchCount,我把他叫做预处理能力值。channel.basicQos(prefetchCount);表示当前这个consumer可以同时处理几个message。这样服务器在进行消息发送前,会检查这个consumer当前正在处理中的message(message已经发送,但是未收到consumer的basicAck)有几个,如果超过了这个consumer节点的能力值,就不再往这个consumer发布。
    这种模式,官方也指出还是有问题的,消息有可能全部阻塞,所有consumer节点都超过了能力值,那消息就阻塞在服务器上,这时需要自己及时发现这个问题,采取措施,比如增加consumer节点或者其他策略
    Note about queue size If all the workers are busy, your queue can fill up. You will want to keep an eye on that, and maybe add more workers, or have some other strategy.
  • 另外 官网上没有深入提到的,就是还是没有考虑到message处理的复杂程度。有的message处理可能很简单,有的可能很复杂,现在还是将所有message的处理程度当成一样的。还是有缺陷的,但是目前也只看到dubbo里对单个服务有权重值的概念,涉及到了这个问题。

1-4-3、Publish/Subscribe 订阅 发布 机制

上面的消息是通过队列进行发布消息,也就是其他consumer都需要到队列去消费消息,比如在业务场景中,购买商品时候,需要调用扣减库存,以及增加积分服务,如果使用单队列,库存和积分服务在消费消息之后都不知道是否将消息设置为已应答,这样就可以用下面这种场景了

type为fanout 的exchange:

image.png

这个机制是对上面的一种补充。也就是把producer与Consumer进行进一步的解耦。producer只负责发送消息,至于消息进入哪个queue,由exchange来分配。如上图,就是把producer发送的消息,交由exchange同时发送到两个queue里,然后由不同的Consumer去进行消费。
关键代码 ===》

producer: //只负责往exchange里发消息,后面的事情不管。

channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));

receiver: //将消费的目标队列绑定到exchange上。

channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");

关键处就是type为”fanout” 的exchange,这种类型的exchange只负责往所有已绑定的队列上发送消息。

1-4-3-1、Publish端代码实现

如代码中,这次需要声明exchange,exchange有四种类型,分别为:fanouttopicheadersdirect,这次使用fanout类型,其作用为:fanout类型的exchange会往其上绑定的所有queue转发消息。

public class EmitLogFanout {

   private static final String EXCHANGE_NAME = "fanoutExchange";
   /**
    * exchange有四种类型, fanout topic headers direct
    * fanout类型的exchange会往其上绑定的所有queue转发消息。
    * @param args
    * @throws Exception
    */
   public static void main(String[] args) throws Exception{
      Connection connection = RabbitMQUtil.getConnection();
      Channel channel = connection.createChannel();
      //发送者只管往exchange里发消息,而不用关心具体发到哪些queue里。
      channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
      String message = "LOG INFO 222";
      channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
      
      channel.close();
      connection.close();
   }
}

1-4-3-2、Receive端接收消息

下面代码即为接收消息的方法,上面的方法只发送到了exchange,而exchange要通过queue发送消息,因为目前没有指定exchange都绑定了哪些队列,因此RabbitMQ会默认创建一个队列绑定到exchange上面。

public class ReceiveLogsFanout {

   private static final String EXCHANGE_NAME = "fanoutExchange";

   public static void main(String[] args) throws Exception {
      Connection connection = RabbitMQUtil.getConnection();
      Channel channel = connection.createChannel();
      
       channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
       String queueName = channel.queueDeclare().getQueue();
      System.out.println(queueName);
       channel.queueBind(queueName, EXCHANGE_NAME, "");

      Consumer myconsumer = new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag, Envelope envelope,
               BasicProperties properties, byte[] body)
               throws IOException {
             System.out.println("========================");
             String routingKey = envelope.getRoutingKey();
             System.out.println("routingKey >"+routingKey);
             String contentType = properties.getContentType();
             System.out.println("contentType >"+contentType);
             long deliveryTag = envelope.getDeliveryTag();
             System.out.println("deliveryTag >"+deliveryTag);
             System.out.println("content:"+new String(body,"UTF-8"));
             // (process the message components here ...)
             //消息处理完后,进行答复。答复过的消息,服务器就不会再次转发。
             //没有答复过的消息,服务器会一直不停转发。
//           channel.basicAck(deliveryTag, false);
         }
      };
      
      channel.basicConsume(queueName,true, myconsumer);
      
//    channel.close();
//    connection.close();
   }
}

启动如下代码,可以在RabbitMQ控制台看到已经默认创建了一个队列

image.png

在Publish端发送消息之后,就可以接收到消息了

image.png

当Receive端服务停了之后,刚刚自动创建的队列就会自动删除。因此在实际业务中,需要先将exchange和queue进行绑定,在消费端直接设置好队列名称即可,比如有库存队列和积分队列。

1-4-3-3、exchange和queue绑定

1-4-3-3-1、在控制台进行绑定
  • 1、打开Queues页面,创建stock队列 Virtual host选择之前创建的mirror,填写并选择相关配置信息,输入queue名称,然后点击创建

image.png

创建完成之后队列中就多了stock

image.png

  • 2、打开exchanges页面,点击要要做的exchange名称

image.png

  • 3、输入要绑定的队列名称(刚刚创建的stock),然后点击bind 需要注意的是要选择To queue

image.png 通过以上步骤就完成了exchanges和queue的绑定,这样在接受消息的时候就可以指定队列名称为stock

如下,消费的时候将队列stock传入即可

channel.basicConsume("stock",true, myconsumer);

给exchange绑定两个队列,如下:

image.png

消费端使用stock队列消费,而integral队列未被绑定消费,这样就可以解决下单分别通知库存和积分服务进行各自消费,并且互不影响

image.png

1-4-3-3-2、在代码中绑定

首先声明exchange,然后再声明stock队列,最后将stock队列和exchange进行绑定即可

channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare("stock",true,false,false,null).getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");

1-4-4、Routing基于内容的路由

type为”direct” 的exchange,direct类型的exchange会根据routingkey,将消息转发到该exchange上绑定了该routingkey的所有queue

image.png 这种模式一看图就清晰了。 在上一章 exchange 往所有队列发送消息的基础上,增加一个路由配置,指定exchange如何将不同类别的消息分发到不同的queue上。

关键代码===> Producer:

channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));

Receiver:

channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueBind(queueName, EXCHANGE_NAME, routingKey1);
channel.queueBind(queueName, EXCHANGE_NAME, routingKey2);
channel.basicConsume(queueName, true, consumer);

1-4-4-1、Publish端代码实现

首先设置了exchange为direct,然后发送了四种不同routingkey的消息

public class EmitLogDirect {

   private static final String EXCHANGE_NAME = "directExchange";
   /**
    * exchange有四种类型, fanout topic headers direct
    * direct类型的exchange会根据routingkey,将消息转发到该exchange上绑定了该routingkey的所有queue
    * @param args
    * @throws Exception
    */
   public static void main(String[] args) throws Exception{
      Connection connection = RabbitMQUtil.getConnection();
      Channel channel = connection.createChannel();
      //发送者只管往exchange里发消息,而不用关心具体发到哪些queue里。
      channel.exchangeDeclare(EXCHANGE_NAME, "direct");

      channel.basicPublish(EXCHANGE_NAME, "虚拟", null, "虚拟订单".getBytes());
      channel.basicPublish(EXCHANGE_NAME, "电子", null, "电子订单".getBytes());
      channel.basicPublish(EXCHANGE_NAME, "衣帽", null, "衣帽订单".getBytes());
      channel.basicPublish(EXCHANGE_NAME, "二手", null, "二手订单".getBytes());

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

1-4-4-2、Receive端接收消息

消息接收端,只接收routingkey为电子和二手的,另外另个routingkey的消息就无法接收到了

public class ReceiveLogsDirect {

    private static final String EXCHANGE_NAME = "directExchange";

    public static void main(String[] args) throws Exception {
        Connection connection = RabbitMQUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//        String queueName = channel.queueDeclare().getQueue();
        String queueName="direct_queue";
        channel.queueDeclare(queueName,false,false,false,null);

        channel.queueBind(queueName, EXCHANGE_NAME, "二手");
        channel.queueBind(queueName, EXCHANGE_NAME, "电子");

        Consumer myconsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       BasicProperties properties, byte[] body)
                    throws IOException {
                System.out.println("========================");
                String routingKey = envelope.getRoutingKey();
                System.out.println("routingKey >" + routingKey);
                String contentType = properties.getContentType();
                System.out.println("contentType >" + contentType);
                long deliveryTag = envelope.getDeliveryTag();
                System.out.println("deliveryTag >" + deliveryTag);
                System.out.println("content:" + new String(body, "UTF-8"));
                // (process the message components here ...)
                //消息处理完后,进行答复。答复过的消息,服务器就不会再次转发。
                //没有答复过的消息,服务器会一直不停转发。
//           channel.basicAck(deliveryTag, false);
            }
        };
        channel.basicConsume(queueName, true, myconsumer);
    }
}

执行结果如下:

image.png

1-4-5、Topics话题

type为"topic" 的exchange,topic类型的exchange在根据routingkey转发消息时,可以对rouytingkey做一定的规则,比如anonymous.info可以被*.info匹配到。

image.png

这个模式也就在上一个模式的基础上,对routingKey进行了模糊匹配
单词之间用,隔开,* 代表一个具体的单词。# 代表0个或多个单词。

关键代码===> Producer:

channel.exchangeDeclare(EXCHANGE_NAME, "topic");
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));

Receiver:

channel.exchangeDeclare(EXCHANGE_NAME, "topic");
channel.queueBind(queueName, EXCHANGE_NAME, routingKey1);
channel.queueBind(queueName, EXCHANGE_NAME, routingKey2);
channel.basicConsume(queueName, true, consumer);

1-4-5-1、Publis端代码实现

首先设置了exchange为topic模式,然后发送了四个不同routingkey

public class EmitLogTopic {

   private static final String EXCHANGE_NAME = "topicExchange";
   /**
    * exchange有四种类型, fanout topic headers direct
    * topic类型的exchange在根据routingkey转发消息时,可以对rouytingkey做一定的规则,比如anonymous.info可以被*.info匹配到。
    * @param args
    * @throws Exception
    */
   public static void main(String[] args) throws Exception{
      Connection connection = RabbitMQUtil.getConnection();
      Channel channel = connection.createChannel();
      //发送者只管往exchange里发消息,而不用关心具体发到哪些queue里。
      channel.exchangeDeclare(EXCHANGE_NAME, "topic");
      channel.basicPublish(EXCHANGE_NAME, "org.jony", null, "org.jony消息".getBytes());
      channel.basicPublish(EXCHANGE_NAME, "org.abc.jony", null, "org.jony.controller消息".getBytes());
      channel.basicPublish(EXCHANGE_NAME, "org.jony.controller", null, "org.jony.controller消息".getBytes());
      channel.basicPublish(EXCHANGE_NAME, "org.abc.jony.controller", null, "org.abc.jony.controller消息".getBytes());

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

1-4-5-2、Receive端实现

消息接收端的routingkey设置了两种分别为*.jony#.jony这样就意味着以jony结尾的所有routingkey都可以匹配到。非jony结尾的就无法匹配到

public class ReceiveLogsTopic {

   //一个exchange只能是一个类型
   private static final String EXCHANGE_NAME = "topicExchange";
   
   public static void main(String[] args) throws Exception {
      Connection connection = RabbitMQUtil.getConnection();
      Channel channel = connection.createChannel();
      
       channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
       String queueName = channel.queueDeclare().getQueue();
       //topic的routingkey,*代表一个具体的单词,#代表0个或多个单词。
       channel.queueBind(queueName, EXCHANGE_NAME, "*.jony");
       channel.queueBind(queueName, EXCHANGE_NAME, "#.jony");
       
      Consumer myconsumer = new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag, Envelope envelope,
               BasicProperties properties, byte[] body)
               throws IOException {
             System.out.println("========================");
             String routingKey = envelope.getRoutingKey();
             System.out.println("routingKey >"+routingKey);
             String contentType = properties.getContentType();
             System.out.println("contentType >"+contentType);
             long deliveryTag = envelope.getDeliveryTag();
             System.out.println("deliveryTag >"+deliveryTag);
             System.out.println("content:"+new String(body,"UTF-8"));
             // (process the message components here ...)
             //消息处理完后,进行答复。答复过的消息,服务器就不会再次转发。
             //没有答复过的消息,服务器会一直不停转发。
//           channel.basicAck(deliveryTag, false);
         }
      };
      channel.basicConsume(queueName,true, myconsumer);
   }
}

执行结果(发送了4个routingkey消息,以jony结尾的routingkey消息全部可以接收到):

image.png

通过exchange的topic基本就可以满足大多数场景的使用了。

1-4-6、Header消息

Headers 类型的Exchanges是不处理路由键的,而是根据发送的消息内容中的headers属性进行匹配。在绑定Queue与Exchange时指定一组键值对;当消息发送到RabbitMQ时会取到该消息的headers与Exchange绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers属性是一个键值对,可以是Hashtable,键值对的值可以是任何类型

headers ExchangeDirect Exchange类似,性能方面比Direct Exchange差很多,所以在实际项目中用的很少

可以使用多个Header进行匹配,将队列绑定到标题交换。在这种情况下,代理需要从应用程序开发者那里获得另一条信息,即,它是否应考虑具有任何匹配的标头的消息,或全部匹配的标头?这就是“ x-match”绑定参数的作用。当“ x-match”参数设置为“ any”时,仅一个匹配的标头值就足够了。或者,将“ x-match”设置为“ all”要求所有值必须匹配

1-4-7、Publisher Confirms 发送者消息确认

RabbitMQ的消息可靠性是非常高的,但是他以往的机制都是保证消息发送到了MQ之后,可以推送到消费者消费,不会丢失消息。但是发送者发送消息是否成功是没有保证的。我们可以回顾下,发送者发送消息的基础API:Producer.basicPublish方法是没有返回值的,也就是说,一次发送消息是否成功,应用是不知道的,这在业务上就容易造成消息丢失。而这个模块就是通过给发送者提供一些确认机制,来保证这个消息发送的过程是成功的。

如果了解了这个机制就会发现,这个消息确认机制就是跟RocketMQ的事务消息机制差不多的。而对于这个机制,RocketMQ的支持明显更优雅。

​ 发送者确认模式默认是不开启的,所以如果需要开启发送者确认模式,需要手动在channel中进行声明。

channel.confirmSelect();

​ 在官网的示例中,重点解释了三种策略:

​ 1、发布单条消息

​ 即发布一条消息就确认一条消息。核心代码:

for (int i = 0; i < MESSAGE_COUNT; i++) {
    String body = String.valueOf(i);
    channel.basicPublish("", queue, null, body.getBytes());
    channel.waitForConfirmsOrDie(5_000);
}

​ channel.waitForConfirmsOrDie(5_000);这个方法就会在channel端等待RabbitMQ给出一个响应,用来表明这个消息已经正确发送到了RabbitMQ服务端。但是要注意,这个方法会同步阻塞channel,在等待确认期间,channel将不能再继续发送消息,也就是说会明显降低集群的发送速度即吞吐量。

官方说明了,其实channel底层是异步工作的,会将channel阻塞住,然后异步等待服务端发送一个确认消息,才解除阻塞。但是我们在使用时,可以把他当作一个同步工具来看待。

然后如果到了超时时间,还没有收到服务端的确认机制,那就会抛出异常。然后通常处理这个异常的方式是记录错误日志或者尝试重发消息,但是尝试重发时一定要注意不要使程序陷入死循环。

​ 2、发送批量消息

​ 之前单条确认的机制会对系统的吞吐量造成很大的影响,所以稍微中和一点的方式就是发送一批消息后,再一起确认。

​ 核心代码:

   int batchSize = 100;
            int outstandingMessageCount = 0;

            long start = System.nanoTime();
            for (int i = 0; i < MESSAGE_COUNT; i++) {
                String body = String.valueOf(i);
                ch.basicPublish("", queue, null, body.getBytes());
                outstandingMessageCount++;

                if (outstandingMessageCount == batchSize) {
                    ch.waitForConfirmsOrDie(5_000);
                    outstandingMessageCount0;
                }
            }

            if (outstandingMessageCount > 0) {
                ch.waitForConfirmsOrDie(5_000);
            }

​ 这种方式可以稍微缓解下发送者确认模式对吞吐量的影响。但是也有个固有的问题就是,当确认出现异常时,发送者只能知道是这一批消息出问题了, 而无法确认具体是哪一条消息出了问题。所以接下来就需要增加一个机制能够具体对每一条发送出错的消息进行处理。

​ 3、异步确认消息

​ 实现的方式也比较简单,Producer在channel中注册监听器来对消息进行确认。核心代码就是一个:

channel.addConfirmListener(ConfirmCallback var1, ConfirmCallback var2);

​ 按说监听只要注册一个就可以了,那为什么这里要注册两个呢?如果对照下RocketMQ的事务消息机制,这就很容易理解了。发送者在发送完消息后,就会执行第一个监听器callback1,然后等服务端发过来的反馈后,再执行第二个监听器callback2。

​ 然后关于这个ConfirmCallback,这是个监听器接口,里面只有一个方法: void handle(long sequenceNumber, boolean multiple) throws IOException; 这方法中的两个参数,

  • sequenceNumer:这个是一个唯一的序列号,代表一个唯一的消息。在RabbitMQ中,他的消息体只是一个二进制数组,并不像RocketMQ一样有一个封装的对象,所以默认消息是没有序列号的。而RabbitMQ提供了一个方法int sequenceNumber = channel.getNextPublishSeqNo());来生成一个全局递增的序列号。然后应用程序需要自己来将这个序列号与消息对应起来。没错!是的!需要客户端自己去做对应!
  • multiple:这个是一个Boolean型的参数。如果是false,就表示这一次只确认了当前一条消息。如果是true,就表示RabbitMQ这一次确认了一批消息,在sequenceNumber之前的所有消息都已经确认完成了。

对比下RocketMQ的事务消息机制,有没有觉得很熟悉,但是又很别扭?当然,考虑到这个对于RabbitMQ来说还是个新鲜玩意,所以有理由相信这个机制在未来会越来越完善。

1-4-8、小结

RabbitMQ在原生API方面提供了7中方案,在实际项目过程中一般使用exchange的方式进行发送消息,exchange有4中模式分别为:

fanout:类型的exchange会往其上绑定的所有queue转发消息。

direct:类型的exchange会根据routingkey,将消息转发到该exchange上绑定了该routingkey的所有queue

topic:类型的exchange在根据routingkey转发消息时,可以对rouytingkey做一定的规则,比如anonymous.info可以被*.info匹配到。

headers用得比较少,他可以发送头部信息。头信息可以理解为一个Map