【RabbitMQ的那点事】Exchange类型(超详细)

Exchange在消息中间件中是个非常重要的角色——负责消息分发——而且是按一定规则去分发。

本文介绍Exchange的4种类型:

  • Direct exchange:(Empty string) and amq.direct。不指定exchange时,即empty string(Default exchange),也会使用该类型。
  • Fanout exchange:amq.fanout
  • Topic exchange:amq.topic
  • Headers exchange:amq.match (and amq.headers in RabbitMQ)

文章内容:(Exchange/routingKey/Queue名字可能和demo对不上)
Exchange Type.jpg

以下所有的示例都是用Java amqp-client.jar来实现的,官网上可查阅到API文档:https://www.rabbitmq.com/api-guide.html#connecting
注:实现情况下,大概率Java项目都是Spring Boot项目,可以用spring-boot-starter-amqp来实现。


1. Default Exchange

在介绍Direct Exchange之前,先介绍特殊的一种Direct exchange —— Default exchange(即没有指定type的exchange)。

Default exchange优点是简单,在这个模式下所有queue都默认binding 到该交换器上。所有binding到该交换器上的queue,routing-key都和queue的name一样。

官方上有个Hello world的例子用的就是该模式:https://www.rabbitmq.com/tutorials/tutorial-one-python.html

Default exchange

用Java代码来实现:

1.1. 首先创建Connection连接工具类,可以为整篇文章所用:

public class MQUtils {
    public static Connection getConnection() throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();//MQ采用工厂模式来完成连接的创建
        //2.在工厂对象中设置连接信息(ip,port,virtualhost,username,password)
        factory.setHost("localhost");//设置MQ安装的服务器ip地址
        factory.setPort(5672);//设置端口号
        factory.setVirtualHost("/");//设置虚拟主机名称
        //factory.setUsername("xxxxxx");//设置用户名称
        //factory.setPassword("123456");//设置用户密码
        //3.通过工厂对象获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

1.2 发送/接收数据前准备:创建queue
1.2.1 可通过Console UI创建:

通过Console创建queue

1.2.2 或者通过代码channel.queueDeclare创建(如果已经存在则不会再次创建):
关于方法queueDeclare有很多参数,请看:https://blog.csdn.net/lovekjl/article/details/108616353
可以把以下代码写入Producer类或者Consumer类中:
注:如果写在Consumer中的话,那么可以先启动Consumer,否则Producer先启动的话,这时候还没有绑定Queue,消息会丢失。

        // 声明queue,如果没有就先创建
        String queue = "hello";
        channel.queueDeclare(queue, true, false, false, null);

注:Default exchange模式下不需要主动创建exchange,因为空的exchange默认就在的。
Default exchange

1.3 Default Exchange的Producer类:

public class Producer {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection connection = MQUtils.getConnection();
        //创建Channel
        Channel channel = connection.createChannel();
        //basicPublish 将消息发送到指定的交换机
        String msg = "Hello World!";
        /**
         * exchange: empty string
         * routingKey: hello
         */
        channel.basicPublish("", "hello", null, msg.getBytes());
        //关闭连接
        channel.close();
        connection.close();
    }
}

1.4 定义Default Exchange的消费类:

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

        // 创建消费者,并接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            // 回调函数
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 接收到的消息是字节数组形式,需要转成字符串
                String msg = new String(body, "UTF-8");
                System.out.println("Received message : '" + msg + "'");
            }
        };

        // 开始获取消息,push模式
        String queue = "hello";
        boolean autoAct = true;
        channel.basicConsume(queue, autoAct, consumer);
    }
}
模拟过程:
  • 对于Producer,他只需要指定exchange和routing_key,那么exchange就会负责绑定到相应的queue上,Producer并不需要直接跟queue打交道。
  • 对于Consumer, auto_act=True表示在收到消息后自动向Broker确认消息。
    Default Exchange模拟过程

2. Direct Exchange

官网示例:https://www.rabbitmq.com/tutorials/tutorial-four-python.html

【示例1】

Direct exchange

用Java代码来实现:

2.1 发送/接收数据前准备:

  • 创建两个队列:Q1和Q2(同#1.2一样,可通过UI或是queueDeclare方法创建,不再赘述。
  • 创建direct exchange,名称为:X
    • 通过UI创建:
      创建Type=direct的exchange,名为X
    • 通过方法exchangeDeclare创建(代码写在Producer中):指定了exchangeName以及type=direct;

        String exchangeName = "X";
        String exchangeType = "direct";
        channel.exchangeDeclare(exchangeName, exchangeType);

2.2 Direct Exchange的Producer类:
queueBind(queueName, exchangeName, routingKey),可以放在UI上,也可放在Producer代码中,也可放在各自的Consumer代码中。

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

        String exchangeName = "X";

        /**
         * 绑定Exchange,按routingKey对Queue进行绑定:
         * 参数1为Queue name, 参数2是Exchange Name, 参数3是routing key
         * queueBind可以在UI上做,也可以放在各自的Consumer中进行绑定:
         */
        channel.queueBind("Q1", exchangeName, "orange");
        channel.queueBind("Q2", exchangeName, "black");
        channel.queueBind("Q2", exchangeName, "green");

        // basicPublish 将消息发送到指定的交换机
        channel.basicPublish(exchangeName, "orange", null, "Orange Msg!".getBytes());
        channel.basicPublish(exchangeName, "black", null, "Black Msg!".getBytes());
        channel.basicPublish(exchangeName, "green", null, "Green Msg!".getBytes());

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

2.3 定义Direct Exchange的消费类:
2.3.1 C1的实现类:消费消息绑定的是Q1:

public class MyConsumer1 {
    public static void main(String[] args) throws Exception {
        // 创建connection/channel,略,同上#1.4
        // 创建消费者,并接收消息,略,同上#1.4

        // 开始获取消息,push模式:queue = "Q1", autoAct = true
        channel.basicConsume("Q1", true, consumer);
    }
}

2.3.1 C2的实现类:消费消息绑定的是Q2:

public class MyConsumer2 {
    public static void main(String[] args) throws Exception{
        // 创建connection/channel,略,同上#1.4
        // 创建消费者,并接收消息,略,同上#1.4

        // 开始获取消息,push模式:queue = "Q2", autoAct = true
        channel.basicConsume("Q2", true, consumer);
    }
}
模拟过程:
Direct exchange模拟过程

【示例2(不在官网示例中)如果有两个Consumer同时订阅一个queue】
RabbitMQ的设定:当多个消费者同时监听一个队列时,他们并不能同时消费一条消息,而是随机消费消息,即一个队列中一条消息,只能被一个消费者消费。

Producer按照routing key=multi发消息给Direct exchange=X时,消息会轮询(round robin)的被Consumer3和Consumer4接收。

Direct exchange经常用来循环分发任务给多个工作者(workers)。当这样做的时候,我们需要明白一点,在AMQP 0-9-1中,消息的负载均衡是发生在消费者(consumer)之间的,而不是队列(queue)之间。
示例2

2.4 示例2-Producer

        String exchangeName = "X";
        channel.basicPublish(exchangeName, "multi", null, "Hello Direct!".getBytes());

2.5 示例2-Consumer
在测试的时候,可以多run一次main方法,这样子就等于有了两个Consumer实例:

        channel.basicConsume("Q3", true, consumer);

注:关于示例2(一个queue有多个消费者),官网也有示例:https://www.rabbitmq.com/tutorials/tutorial-two-python.html

Work Queues


3. Fanout Exchange

fan out的意思是成扇形展开,即官网中给中的publisher/subcribe例子,用来做广播最好:https://www.rabbitmq.com/tutorials/tutorial-three-python.html

因为扇型交换机投递消息的拷贝到所有绑定到它的队列,所以他的应用案例都极其相似:

  • 大规模多用户在线(MMO)游戏可以使用它来处理排行榜更新等全局事件
  • 体育新闻网站可以用它来近乎实时地将比分更新分发给移动客户端
  • 分发系统使用它来广播各种状态和配置更新

【示例】图中的红色队列名称是随机生成的,我们就暂且叫Q4,Q5好了:


image.png
Java代码实现:

这次exchangeName=F,以及queue=Q4, Q5都在代码中创建。
关于exchange通过routingKey与queue的绑定,也写在代码中。
在fanout的模式下,不需要指定routing key。

3.1 Producer代码

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

        String exchangeName = "F";
        channel.exchangeDeclare(exchangeName, "fanout");

        /**
         * 绑定Exchange,按routingKey对Queue进行绑定:Fanout模式下routingKey为空
         */
        channel.queueBind("Q4", exchangeName, "");
        channel.queueBind("Q5", exchangeName, "");

        // basicPublish 将消息发送到指定的交换机
        channel.basicPublish(exchangeName, "", null, "Score Updated!".getBytes());

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

3.2 Consumer1代码:
Consumer2的代码,把下面Q4换成Q5即可。

public class MyConsumer1 {
    public static void main(String[] args) throws Exception {
        // 创建connection/channel,略,同上#1.4
        // 创建消费者,并接收消息,略,同上#1.4

        // 声明queue
        String queue = "Q4";
        channel.queueDeclare(queue, true, false, false, null);

        // 开始获取消息,push模式:queue = "Q4", autoAct = true
        channel.basicConsume(queue, true, consumer);
    }
}

测试结果:
Consumer1和Consumer2都能收到消息:Received message : 'Score Updated!'

模拟过程:
Fanout exchange模拟过程

综上,Fanout exchange会忽略routing key。在Producer端,只需要关心将消息发送给哪个fanout exchange。在Consumer端,会负责将queue和上述的fanout exchange绑定起来,而且订阅该queue。


4. Topic Exchange

Topic exchange通过对消息的路由键和队列到交换机的绑定模式之间的匹配,将消息路由给一个或多个队列。

这里的routing key可以有通配符:
  • 星号(*):表示准确的匹配一个单词
  • 井号(#):则表示匹配0个或者多个单词。

官网同样给出了例子:https://www.rabbitmq.com/tutorials/tutorial-five-python.html

image.png

上述的图中,routing key是有通配符的。假设我们的场景是根据规则=<运动速度>.<颜色>.<物种>来绑定queue,那么:

  • Q1队列绑定的key=.orage.,表示Q1队列想要知道所有橙色的动物。
  • Q2队列绑定的key=..rabbit和lazy.#,表示Q2队列想要知道兔子或者所有运动速度不快的(懒懒的)动物。

展开来讲,

  • 如果一个message在发送的时候,routing key是quick.orange.rabbit,那么两个queue都能收到。
  • 如果routing key是lazy.orange.elephant,那么两个queue也都能收到。
  • 如果routing key是quick.orange.fox,那么只有Q1能收到。
  • 如果routing是lazy.brown.fox,那么只有Q2能收到。
  • 如果routing key是quick.brown.fox,那么没有queue会收到。
用Java实现:

Producer类:把前面的DEMO创建的Exchange和Queue先清空下,因为有重名:

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

        // 创建exchange:
        String exchangeName = "X";
        channel.exchangeDeclare(exchangeName, "topic");

        // basicPublish 将消息发送到指定的交换机
        channel.basicPublish(exchangeName, "quick.orange.rabbit", null, "quick.orange.rabbit!".getBytes()); // C1 C2都能收到
        channel.basicPublish(exchangeName, "lazy.orange.elephant", null, "lazy.orange.elephant!".getBytes()); // C1 C2都能收到
        channel.basicPublish(exchangeName, "quick.orange.fox", null, "quick.orange.fox!".getBytes()); // C1能收到
        channel.basicPublish(exchangeName, "lazy.brown.fox", null, "lazy.brown.fox!".getBytes());// C2能收到
        channel.basicPublish(exchangeName, "quick.brown.fox", null, "quick.brown.fox!".getBytes()); // C1 C2都不能收到

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

这次的binding放在Consumer代码中:
先是Consumer1:

public class MyConsumer1 {
    public static void main(String[] args) throws Exception {
        // 创建connection/channel,略,同上#1.4
        // 创建消费者,并接收消息,略,同上#1.4

        // 声明queue
        String queue = "Q1";
        channel.queueDeclare(queue, true, false, false, null);

        /**
         * 绑定Exchange,按routingKey对Queue进行绑定:
         * 参数1为Queue name, 参数2是Exchange Name, 参数3是routing key
         * queueBind可以在UI上做,也可以放在Producer中进行绑定:
         */
        channel.queueBind(queue, "X", "*.orange.*");

        // 开始获取消息,push模式:queue = "Q1", autoAct = true
        channel.basicConsume(queue, true, consumer);
    }
}

再是Consumer2:

public class MyConsumer2 {
    public static void main(String[] args) throws Exception{
        // 创建connection/channel,略,同上#1.4
        // 创建消费者,并接收消息,略,同上#1.4

        // 声明queue
        String queue = "Q2";
        channel.queueDeclare(queue, true, false, false, null);

        /**
         * 绑定Exchange,按routingKey对Queue进行绑定:
         * 参数1为Queue name, 参数2是Exchange Name, 参数3是routing key
         * queueBind可以在UI上做,也可以放在Producer中进行绑定:
         */
        channel.queueBind(queue, "X", "*.*.rabbit");
        channel.queueBind(queue, "X", "lazy.#");

        // 开始获取消息,push模式:queue = "Q2", autoAct = true
        channel.basicConsume(queue, true, consumer);
    }
}

Topic exchange的功能非常强大,也可以通过设置不同的routing key,达到别的exchange的用法:

  • 如果queue绑定在routing key = “#”时,那么它可以匹配所有的routing key,就像fanout exchange。
  • 如果queue绑定的时候不使用“*”或者“#”,那么此时的topic exchange就像是direct exchange,是完全匹配模式。

主题交换机拥有非常广泛的用户案例。无论何时,当一个问题涉及到那些想要有针对性的选择需要接收消息的多消费者/多应用(multiple consumers/applications) 的时候,主题交换机都可以被列入考虑范围。

使用案例:

  • 分发有关于特定地理位置的数据,例如销售点由多个工作者(workers)完成的
  • 后台任务,每个工作者负责处理某些特定的任务
  • 股票价格更新(以及其他类型的金融数据更新)
  • 涉及到分类或者标签的新闻更新(例如,针对特定的运动项目或者队伍)

5. Header Exchange

header exchange(自定义交换器),根据自定义的header attribute去匹配不同的queue。


参考:
RabbitMQ官网 - Queue
rabbitmq的exclusive 排他队列
RabbitMQ的六种模式总结

你可能感兴趣的:(【RabbitMQ的那点事】Exchange类型(超详细))