RabbitMQ消息模型专题

RabbitMQ消息模型专题

本文大纲:
一.基本消息模型
二.work消息模型
三.发布/订阅模型(交换机类型:Fanout,也称广播)
四.Routing路由模型(交换机类型:direct)
五.Topics通配符模式(交换机类型:topics)
六.RPC模型

RabbitMQ的六种消息模型

一.基本消息模型

在这里插入图片描述

基本概念:
P:生产者,也就是要发送消息的程序
C:消费者:消息的接受者,会一直等待消息到来。
queue:消息队列,图中红色部分。可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。

1.1生产者示例

(1)新建一个maven工程,添加amqp-client依赖

 <!--amqp-client依赖-->
  <dependency>
      <groupId>com.rabbitmq</groupId>
      <artifactId>amqp-client</artifactId>
      <version>5.7.3</version>
  </dependency>
  <!--slf4j依赖-->
  <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-nop</artifactId>
      <version>1.7.2</version>
  </dependency>

(2)新建连接工具类:建立与RabbitMQ的连接

package demo.util;

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
 * 连接工具类
 */
public class ConnectionUtil {

    /**
     * 建立与RabbitMQ的连接
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception{
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("localhost");
        //端口
        factory.setPort(5672);
        //设置账号信息  vhost(虚拟机)、 用户名、密码
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("guest");
        //通过工厂获取连接
        Connection conn = factory.newConnection();
        return conn;
    }
}

(3)新建一个类:生产者发送消息

package demo;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import demo.util.ConnectionUtil;

/**
 * 生产者发送消息
 * @author qzz
 */
public class Send {

    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] args) throws Exception {

        // 1.获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 2.从连接中创建通道,使用通道才能完成与消息相关的操作
        Channel channel = connection.createChannel();
        // 3.声明(创建)队列
        // 参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        /**
         * 参数说明
         *   1.queue 队列名称
         *   2.durable 是否持久化;如果持久化,mq重启后队列还在
         *   3.exclusive 表示该消息队列是否只在当前connection生效(
         *   如果connection 连接关闭,则列队自动删除,如果将此参数设置为true可以用于临时队列的创建)
         *   4.autoDelete 自动删除,队列不再使用时是否自动删除此队列;
         *   如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
         *   5.arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        //4.消息内容
        String message = "Hello World!";
        //5.向指定的队列中发送消息
        //参数:String exchange,String routingKey,BasicProperties props,bytes[]body
        /**
         * 参数说明:
         *   1.exchange 交换机,如果不指定将使用mq的默认交换机(设置为“”)
         *   2.routingKey 路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名字
         *   3.props 消息的属性
         *   4.body 消息内容
         */
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
        System.out.println("[X] send '" + message + "'");
        //6.关闭通道和连接 (资源关闭最好用try-catch-finally 语句处理)
        channel.close();
        connection.close();

    }
}

运行此类,控制台信息可以看到,生产者发送信息成功!

web管理页面:服务器地址/端口号
本地: http://localhost:15672
默认用户及密码:guest guest
查看web界面,看队列的消息,有消息未被消费。

(4)新建一个类:消费者接受消息

package demo;

import com.rabbitmq.client.*;
import demo.util.ConnectionUtil;

import java.io.IOException;

/**
 * 消费者接受消息
 * @author qzz
 */
public class Receive {

    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] args) throws Exception{
        //1.获取到连接
        Connection connection = ConnectionUtil.getConnection();
        //2.从连接中创建会话通道,生成者金额mq服务所有通信都在channel通道中完成
        Channel channel = connection.createChannel();
        //3.声明(创建)队列
        //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        /**
         * 参数说明
         *   1.queue 队列名称
         *   2.durable 是否持久化;如果持久化,mq重启后队列还在
         *   3.exclusive 表示该消息队列是否只在当前connection生效
         *   (如果connection 连接关闭,则列队自动删除,如果将此参数设置为true可以用于临时队列的创建)
         *   4.autoDelete 自动删除,队列不再使用时是否自动删除此队列;
         *   如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
         *   5.arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        //4.实现消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel){
            //获取消息,并且处理;这个方法类似事件监听;如果有消息的,会被自动调用
            /**
             * 当接受到消息后,此方法将被调用
             * @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
             * @param envelope 信封,通过envelope
             * @param properties 消息属性
             * @param body  消息内容
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, 
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                //获取交换机
                String exchange = envelope.getExchange();
                //消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
                long deliveryTag = envelope.getDeliveryTag();
                //body 即消息体
                String msg = new String(body,"utf-8");
                System.out.println("[X] received: " + msg);
            }
        };
        //5.监听队列,第二个参数:是否自动进行消息确认
        //参数:String queue ,boolean autoAck,Consumer callback
        /**
         * 参数说明:
         *   1.queue 队列名称
         *   2.autoAck 自动回复,当消费者接受到消息后要告诉mq消息已接收,
         *   如果将此参数设置为true表示会自动回复mq,如果设置为false要通过编程实现回复
         *   3.callback 消费方法,当消费者接收到消息时要执行的方法
         */
        channel.basicConsume(QUEUE_NAME,true,consumer);
    }
}

运行此类,控制台信息可以看到消费者发送信息成功!
再看看队列的消息,已经被消费了。

我们发现,消费者已经获取了消息,但是程序没有停止,一直在监听队列中是否有新的消息。一旦有新的消息进入队列,就会立即打印。
验证:再次执行,控制台发送一条消息!

1.2.消息确认机制(ACK)

消息一旦被消费者接收,队列中的消息就会被删除。

如果消费者领取信息后,还没执行操作就挂掉了呢?或者抛出异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了。
因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。

ACK有两种情况:
自动ACK:消息一旦被接收,消费者自动发送ACK
手动ACK:消息接收后,不会发送ACK,需要手动调用

ACK使用看消息的重要性:
如果消息不太重要,丢失也没影响,那么自动ACK会比较方便
如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机(可以理解成死机),那么消息就丢失了。

如果需要手动ACK,需要改动我们的代码:

package demo;

import com.rabbitmq.client.*;
import demo.util.ConnectionUtil;

import java.io.IOException;

/**
 * 消费者接受消息 ---- 手动ACK
 * @author qzz
 */
public class Receive2 {

    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] args) throws Exception{
        //1.获取到连接
        Connection connection = ConnectionUtil.getConnection();
        //2.从连接中创建会话通道,生成者和mq服务所有通信都在channel通道中完成
        Channel channel = connection.createChannel();
        //3.声明(创建)队列
        //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        /**
         * 参数说明
         *   1.queue 队列名称
         *   2.durable 是否持久化;如果持久化,mq重启后队列还在
         *   3.exclusive 表示该消息队列是否只在当前connection生效
         *   (如果connection 连接关闭,则列队自动删除,如果将此参数设置为true可以用于临时队列的创建)
         *   4.autoDelete 自动删除,队列不再使用时是否自动删除此队列;
         *   如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
         *   5.arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        //4.实现消费方法(定义队列的消费者)
        DefaultConsumer consumer = new DefaultConsumer(channel){
            //获取消息,并且处理;这个方法类似事件监听;如果有消息的,会被自动调用
            /**
             * 当接受到消息后,此方法将被调用
             * @param consumerTag 消费者标签,用来标识消费者的,2022/7/31 20:23:53 2022/7/31 20:23:54 2022/7/31 20:23:55 2022/7/31 20:23:56 在监听队列时设置channel.basicConsume
             * @param envelope 信封,通过envelope
             * @param properties 消息属性
             * @param body  消息内容
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                //获取交换机
                String exchange = envelope.getExchange();
                //消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
                long deliveryTag = envelope.getDeliveryTag();
                //body 即消息体
                String msg = new String(body,"utf-8");
                System.out.println("[X] received: " + msg);

                //手动进行ACK
                //参数 long deliveryTag, boolean multiple
                /**
                 * 参数说明
                 *    1.deliveryTag 用来标识消息的id
                 *    2.multiple 是否批量 true:将一次ack所有小于deliveryTag的消息
                 */
                channel.basicAck(deliveryTag,false);
            }
        };
        //5.监听队列,第二个参数:是否自动进行消息确认,false 代表手动进行ack
        //参数:String queue ,boolean autoAck,Consumer callback
        /**
         * 参数说明:
         *   1.queue 队列名称
         *   2.autoAck 自动回复,当消费者接受到消息后要告诉mq消息已接收,
         *   如果将此参数设置为true表示会自动回复mq,如果设置为false要通过编程实现回复
         *   3.callback 消费方法,当消费者接收到消息时要执行的方法
         */
        channel.basicConsume(QUEUE_NAME,false,consumer);
    }
}

最后一行代码设置第二个参数为false,代表手动进行ack。

自动ACK存在的问题:
修改消费者,添加异常,如下:

二.work消息模型

在这里插入图片描述

工作队列或竞争消费者模式,work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息,但是一个消息只能被一个消费者获取。

P:生产者:任务的发布者。

C1:消费者1:领取任务并且完成任务,假设完成速度较慢(模拟耗时)。

C2:消费者2:领取任务并且完成任务,假设完成速度较快。

2.1.生产者示例

public class Send {
    private final static String QUEUE_NAME = "test_work_queue";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 循环发布任务
        for (int i = 0; i < 50; i++) {
            // 消息内容
            String message = "task .. " + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
 
            Thread.sleep(i * 2);
        }
        // 关闭通道和连接
        channel.close();
        connection.close();
    }
}

2.2.消费者1

public class Recv {
    private final static String QUEUE_NAME = "test_work_queue";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        //创建会话通道,生产者和mq服务所有通信都在channel通道中完成
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //实现消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel){
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, 
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body,"utf-8");
                System.out.println(" [消费者1] received : " + msg + "!");
                //模拟任务耗时1s
                try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); }
            }
        };
        // 监听队列,第二个参数:是否自动进行消息确认。
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

2.2.消费者2

public class Recv {
    private final static String QUEUE_NAME = "test_work_queue";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        //创建会话通道,生产者和mq服务所有通信都在channel通道中完成
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //实现消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel){
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, 
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body,"utf-8");
                System.out.println(" [消费者1] received : " + msg + "!");
                //模拟任务耗时1s
                //  try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); }
            }
        };
        // 监听队列,第二个参数:是否自动进行消息确认。
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

可以发现,两个消费者各自消费了不同25条消息,这就实现了任务的分发。

消费者1比消费者2的效率要低,一次任务的耗时较长

然而两人最终消费的消息数量是一样的

消费者2大量时间处于空闲状态,消费者1一直忙碌

现在的状态属于是把任务平均分配,正确的做法应该是消费越快的人,消费的越多。

通过 BasicQos 方法设置prefetchCount = 1。这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理1个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。相反,它会将其分派给不是仍然忙碌的下一个Consumer。

值得注意的是:prefetchCount在手动ack的情况下才生效,自动ack不生效。

订阅模型分类

说明下:
1、一个生产者多个消费者
2、每个消费者都有一个自己的队列
3、生产者没有将消息直接发送给队列,而是发送给exchange(交换机、转发器)
4、每个队列都需要绑定到交换机上
5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者消费
例子:注册->发邮件、发短信

X(Exchanges):交换机一方面:接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。

Exchange类型有以下几种:

Fanout:广播,将消息交给所有绑定到交换机的队列

Direct:定向,把消息交给符合指定routing key 的队列

Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

Header:header模式与routing不同的地方在于,header模式取消routingkey,使用header中的 key/value(键值对)匹配队列。

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

三.发布/订阅模型(交换机类型:Fanout,也称广播)

Publish/subscribe模型示意图 :
在这里插入图片描述

3.1.生产者

和前面两种模式不同:

1) 声明Exchange,不再声明Queue

2) 发送消息到Exchange,不再发送到Queue

public class Send {
 
    private final static String EXCHANGE_NAME = "test_fanout_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为fanout
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        
        // 消息内容
        String message = "注册成功!!";
        // 发布消息到Exchange
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [生产者] Sent '" + message + "'");
 
        channel.close();
        connection.close();
    }
}

3.2. 消费者1

public class Recv {
    private final static String QUEUE_NAME = "fanout_exchange_queue_sms";//短信队列
 
    private final static String EXCHANGE_NAME = "test_fanout_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
 
        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
 
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [短信服务] received : " + msg + "!");
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

3.3. 消费者2(注册成功发给邮件服务)

public class Recv2 {
    private final static String QUEUE_NAME = "fanout_exchange_queue_email";//邮件队列
 
    private final static String EXCHANGE_NAME = "test_fanout_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
 
        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [邮件服务] received : " + msg + "!");
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

运行两个消费者,然后发送1条消息:

1、publish/subscribe与work queues有什么区别。

区别:

1)work queues不用定义交换机,而publish/subscribe需要定义交换机。

2)publish/subscribe的生产方是面向交换机发送消息,work queues的生产方是面向队列发送消息(底层使用默认交换机)。

3)publish/subscribe需要设置队列和交换机的绑定,work queues不需要设置,实际上work queues会将队列绑定到默认的交换机 。

相同点:

所以两者实现的发布/订阅的效果是一样的,多个消费端监听同一个队列不会重复消费消息。

2、实际工作用 publish/subscribe还是work queues。

建议使用 publish/subscribe,发布订阅模式比工作队列模式更强大(也可以做到同一队列竞争),并且发布订阅模式可以指定自己专用的交换机。

四.Routing路由模型(交换机类型:direct)

Routing模型示意图:
在这里插入图片描述

P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。

X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列

C1:消费者,其所在队列指定了需要routing key 为 error 的消息

C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息

4.1.生产者

public class Send {
    private final static String EXCHANGE_NAME = "test_direct_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为direct
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 消息内容,
        String message = "注册成功!请短信回复[T]退订";
        // 发送消息,并且指定routing key 为:sms,只有短信服务能接收到消息
        channel.basicPublish(EXCHANGE_NAME, "sms", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
 
        channel.close();
        connection.close();
    }
}

4.2.消费者1

public class Recv {
    private final static String QUEUE_NAME = "direct_exchange_queue_sms";//短信队列
    private final static String EXCHANGE_NAME = "test_direct_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。可以指定多个
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "sms");//指定接收发送方指定routing key为sms的消息
        //channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "email");
 
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [短信服务] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

4.3.消费者2

public class Recv2 {
    private final static String QUEUE_NAME = "direct_exchange_queue_email";//邮件队列
    private final static String EXCHANGE_NAME = "test_direct_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。可以指定多个
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "email");//指定接收发送方指定routing key为email的消息
 
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [邮件服务] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

发送sms的RoutingKey,发现结果:只有指定短信的消费者1收到消息了。

五.Topics通配符模式(交换机类型:topics)

Topics模型示意图:
在这里插入图片描述

每个消费者监听自己的队列,并且设置带统配符的routingkey,生产者将消息发给broker,由交换机根据routingkey来转发消息到指定的队列。

Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:inform.sms
通配符规则:

#:匹配一个或多个词

*:匹配不多不少恰好1个词

举例:
audit.#:能够匹配audit.irs.corporate 或者 audit.irs

audit.*:只能匹配audit.irs

从示意图可知,我们将发送所有描述动物的消息。消息将使用由三个字(两个点)组成的Routing key发送。路由关键字中的第一个单词将描述速度,第二个颜色和第三个种类:“..”。

我们创建了三个绑定:Q1绑定了“.orange.”,Q2绑定了“...rabbit”和“lazy.#”。

Q1匹配所有的橙色动物。

Q2匹配关于兔子以及懒惰动物的消息。

下面做个小练习,假如生产者发送如下消息,会进入哪个队列:

quick.orange.rabbit       Q1 Q2   routingKey="quick.orange.rabbit"的消息会同时路由到Q1与Q2

lazy.orange.elephant    Q1 Q2

quick.orange.fox           Q1

lazy.pink.rabbit              Q2  (值得注意的是,虽然这个routingKey与Q2的两个bindingKey都匹配,但是只会投递Q2一次)

quick.brown.fox            不匹配任意队列,被丢弃

quick.orange.male.rabbit   不匹配任意队列,被丢弃

orange         不匹配任意队列,被丢弃

5.1.生产者

public class Send {
    private final static String EXCHANGE_NAME = "test_topic_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为topic
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        // 消息内容
        String message = "这是一只行动迅速的橙色的兔子";
        // 发送消息,并且指定routing key为:quick.orange.rabbit
        channel.basicPublish(EXCHANGE_NAME, "quick.orange.rabbit", null, message.getBytes());
        System.out.println(" [动物描述:] Sent '" + message + "'");
 
        channel.close();
        connection.close();
    }
}

5.2.消费者1

public class Recv {
    private final static String QUEUE_NAME = "topic_exchange_queue_Q1";
    private final static String EXCHANGE_NAME = "test_topic_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅所有的橙色动物
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.orange.*");
 
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

5.3.消费者2

public class Recv2 {
    private final static String QUEUE_NAME = "topic_exchange_queue_Q2";
    private final static String EXCHANGE_NAME = "test_topic_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅关于兔子以及懒惰动物的消息
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.*.rabbit");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "lazy.#");
 
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

结果C1、C2是都接收到消息了:

六.RPC模型

RPC模型示意图:


基本概念:

Callback queue 回调队列,客户端向服务器发送请求,服务器端处理请求后,将其处理结果保存在一个存储体中。而客户端为了获得处理结果,那么客户在向服务器发送请求时,同时发送一个回调队列地址reply_to。

Correlation id 关联标识,客户端可能会发送多个请求给服务器,当服务器处理完后,客户端无法辨别在回调队列中的响应具体和那个请求时对应的。为了处理这种情况,客户端在发送每个请求时,同时会附带一个独有correlation_id属性,这样客户端在回调队列中根据correlation_id字段的值就可以分辨此响应属于哪个请求。

流程说明:

当客户端启动的时候,它创建一个匿名独享的回调队列。
在 RPC 请求中,客户端发送带有两个属性的消息:一个是设置回调队列的 reply_to 属性,另一个是设置唯一值的 correlation_id 属性。

将请求发送到一个 rpc_queue 队列中。
服务器等待请求发送到这个队列中来。当请求出现的时候,它执行他的工作并且将带有执行结果的消息发送给 reply_to 字段指定的队列。
客户端等待回调队列里的数据。当有消息出现的时候,它会检查 correlation_id 属性。如果此属性的值与请求匹配,将它返回给应用

六.Header交换机

Headers类型的exchange使用的比较少,它也是忽略routingKey的一种路由方式。是使用Headers来匹配的。
Headers是一个键值对,可以定义成Hashtable。
发送者在发送的时候定义一些键值对,接收者也可以再绑定时候传入一些键值对,两者匹配的话,则对应的队列就可以收到消息。

匹配有两种方式all和any。这两种方式是在接收端必须要用键值"x-mactch"来定义。
all代表定义的多个键值对都要满足,
any则代码只要满足一个就可以了。fanout,direct,topic exchange的routingKey都需要要字符串形式的,
而headers exchange则没有这个要求,因为键值对的值可以是任何类型。

6.1.生产者Producer

package cn.slimsmart.rabbitmq.demo.headers;
 
import java.util.Date;
import java.util.Hashtable;
import java.util.Map;
 
import org.springframework.amqp.core.ExchangeTypes;
 
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.AMQP.BasicProperties.Builder;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
 
public class Producer {
	private final static String EXCHANGE_NAME = "header-exchange";
	
	@SuppressWarnings("deprecation")
	public static void main(String[] args) throws Exception {
		// 创建连接和频道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("192.168.36.102");
		// 指定用户 密码
		factory.setUsername("admin");
		factory.setPassword("admin");
		// 指定端口
		factory.setPort(AMQP.PROTOCOL.PORT);
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		
		//声明转发器和类型headers
		channel.exchangeDeclare(EXCHANGE_NAME, ExchangeTypes.HEADERS,false,true,null);
		String message = new Date().toLocaleString() + " : log something";
		
		Map<String,Object> headers =  new Hashtable<String, Object>();
		headers.put("aaa", "01234");
		Builder properties = new BasicProperties.Builder();
		properties.headers(headers);
		
		// 指定消息发送到的转发器,绑定键值对headers键值对
		channel.basicPublish(EXCHANGE_NAME, "",properties.build(),message.getBytes());
		
		System.out.println("Sent message :'" + message + "'");
		channel.close();
		connection.close();
	}
}

6.2.消费者Consumer

package cn.slimsmart.rabbitmq.demo.headers;
 
import java.util.Hashtable;
import java.util.Map;
 
import org.springframework.amqp.core.ExchangeTypes;
 
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
 
public class Consumer {
	private final static String EXCHANGE_NAME = "header-exchange";
	private final static String QUEUE_NAME = "header-queue";
	
	public static void main(String[] args) throws Exception {
		// 创建连接和频道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("192.168.36.102");
		// 指定用户 密码
		factory.setUsername("admin");
		factory.setPassword("admin");
		// 指定端口
		factory.setPort(AMQP.PROTOCOL.PORT);
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		
		//声明转发器和类型headers
		channel.exchangeDeclare(EXCHANGE_NAME, ExchangeTypes.HEADERS,false,true,null);
		channel.queueDeclare(QUEUE_NAME,false, false, true,null);
		
		Map<String, Object> headers = new Hashtable<String, Object>();
		headers.put("x-match", "any");//all any
		headers.put("aaa", "01234");
		headers.put("bbb", "56789");
		// 为转发器指定队列,设置binding 绑定header键值对
		channel.queueBind(QUEUE_NAME, EXCHANGE_NAME,"", headers);
		QueueingConsumer consumer = new QueueingConsumer(channel);
		// 指定接收者,第二个参数为自动应答,无需手动应答
		channel.basicConsume(QUEUE_NAME, true, consumer);
		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			System.out.println(message);
		} 
	}
}

面试题

避免消息堆积

1) 采用workqueue,多个消费者监听同一队列。

2)接收到消息以后,而是通过线程池,异步消费。

如何避免消息丢失

1) 消费者的ACK机制。可以防止消费者丢失消息。
但是,如果在消费者消费之前,MQ就宕机了,消息就没了?

2)可以将消息进行持久化。要将消息持久化,前提是:队列、Exchange都持久化
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值