二、RabbitMQ 的常用工作模式

224 阅读4分钟

  RabbitMQ 中常用的工作模式有简单模式工作队列模式发布订阅模式路由模式通配符模式,我们将使用 RabbitMQ 的 Java API 来实现这些工作模式。本文关于 RabbtiMQ 的 API 都依赖于 amqp-client

  <!--rabbitmq java客户端-->
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.12.0</version>
    </dependency>
</dependencies>

一、简单模式

一个生产者、一个消费者,不需要设置交换机(使用默认的交换机) image.png

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

1. 生产者

public class HelloProducer {
    private static final String QUEUE_NAME = "hello-world";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        /**
         * 参数说明
         * queue: 队列名称
         * durable: 是否持久化消息内容(即消息是否落盘)
         * exclusive: 是否独占,只有一个消费者监听这个队列
         * autoDelete: 是否自动删除,当没有消费者存在时,自动删除队列
         * arguments: 其他配置参数
         */
        //该方法功能:如果没有名称为 queue 的队列,则会创建一个,存在 queue 时则不创建
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        /**
         * 参数说明
         * exchange: 交换机名称,简单模式下使用默认的
         * routingKey: 路由名称,简单模式下就是队列名称
         */
        String body = "hello rabbitmq ...";
        channel.basicPublish("", QUEUE_NAME, null, body.getBytes(StandardCharsets.UTF_8));

        // 2.释放资源
        channel.close();
        connection.close();
        System.out.println("producer done");
    }
}

2. 消费者

public class HelloConsumer {
    private static final String QUEUE_NAME = "hello-world";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        //该方法功能:如果没有名称为 queue 的队列,则会创建一个,存在 queue 时则不创建
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        System.out.println("消费端等待接收消息......");
        Consumer consumer = new DefaultConsumer(channel) {
            /**
             * 回调方法,当接收到消息后会自动执行该方法
             * @param consumerTag  消费者标识
             * @param envelope     消息的包装数据,比如交换机、路由key等
             * @param properties   配置信息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag: " + consumerTag + " Exchange: " + envelope.getExchange() + " RoutingKey: " + envelope.getRoutingKey());
                System.out.println("消费端接收到消息,body: " + new String(body));
            }
        };
        // autoAck: true 表示自动应答;false 表示手动应答
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

先运行消费者,在运行生产者,执行结果如下:

image.png

二、工作队列(Work queues)模式

  与简单模式相比,工作队列模式有一个或多个消费端,并且多个消费者共同消费同一个队列中的消息,这些消费者是竞争关系,即同一时刻只有一个消费者能去队列中拉取消息,该种模式也使用默认的交换机。

image.png

1. 生产者

public class WorkQueueProducer {
    private static final String QUEUE_NAME = "work-queue";

    public static void main(String[] args) throws Exception {
   
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("113.133.166.59");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
    
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        for (int i = 1; i < 11; i++) {
            String body = "第 " + i + " 次, hello rabbitmq ...";
            channel.basicPublish("", QUEUE_NAME, null, body.getBytes(StandardCharsets.UTF_8));
        }

        // 2.释放资源
        channel.close();
        connection.close();
        System.out.println("producer done");
    }

2. 消费者A

public class AWorkQueueConsumer {
    private static final String QUEUE_NAME = "work-queue";

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

        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("113.133.166.59");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        //该方法功能:如果没有名称为 queue 的队列,则会创建一个,存在 queue 时则不创建
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        System.out.println("消费端等待接收消息......");
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消费端接收到消息,body: " + new String(body));
            }
        };
        // autoAck: true 表示自动应答;false 表示手动应答
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }

3. 消费者B

public class BWorkQueueConsumer {
    private static final String QUEUE_NAME = "work-queue";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("113.133.166.59");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        //该方法功能:如果没有名称为 queue 的队列,则会创建一个,存在 queue 时则不创建
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        System.out.println("消费端等待接收消息......");
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消费端接收到消息,body: " + new String(body));
            }
        };
        // autoAck: true 表示自动应答;false 表示手动应答
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

三、发布订阅模式

订阅模式中,多了一个 Exchange 角色。Exchange 只负责消息转发,不具有消息存储能力,如果 Exchange 没有和任何消息队列绑定,那么消息就会丢失

  • 生产者发送消息,消息先发到交换机 Exchange 中,然后由交换机转发到绑定的消息队列中
  • 每个消息队列有自己对应的消费者,然后进行消息消费

image.png

  • 该种模式适用于生产者消息被多个客户端应用消费的场景

1. 生产者

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

        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("113.133.166.59");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        /**
         * 2.创建交换机,方法 exchangeDeclare,方法参数说明:
         * (1) exchange: 交换机机名称
         * (2) type: 交换机类型(参考枚举类BuiltinExchangeType,
         *                    direct:定向交换机,把消息转发指定的routing key绑定的queue中
         *                    fanout:广播交换机,把消息转发给所有绑定的queue
         *                    topic:通配符交换机,把消息转发给匹配到的routing key绑定的queue中)
         * (3) durable: 交换机是否持久化,会将 exchange 的信息保存到磁盘中,rabbitmq 重启时不会丢失
         * (4) autoDelete: 是否自动删除,true 表示当 exchange 不再被使用的时候会被自动删除
         * (5) internal: 是否内置,true 表示当前 exchange 是一个内置交换机,此时不能直接通过客户端程序向这个交换机中发送消息的
         * (6) arguments:设置交换机的其他参数
         */
        String exchangeName = "pub_sub_fanout";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, false, true, null);

        String queueA = "fanout_queue_a";
        String queueB = "fanout_queue_b";
        channel.queueDeclare(queueA, false, false, true, null);
        channel.queueDeclare(queueB, false, false, true, null);

        // 当交换机类型是 BuiltinExchangeType.FANOUT 时,routingKey 为空串,不能为 null
        channel.queueBind(queueA, exchangeName, "");
        channel.queueBind(queueB, exchangeName, "");

        String body = "pub/sub fanout 模式测试 ...";
        channel.basicPublish(exchangeName, "", null, body.getBytes(StandardCharsets.UTF_8));

        channel.close();
        connection.close();
        System.out.println("PubSubProducer done");
    }
}

2.客户端A消费者

public class PubSubConsumerA {
    public static String queueA = "fanout_queue_a";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("113.133.166.59");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        System.out.println("消费端等待接收消息......");
        DeliverCallback deliverCallback = new DeliverCallback() {
            @Override
            public void handle(String consumerTag, Delivery message) throws IOException {
                String msg = new String(message.getBody());
                System.out.println("队列A接受到消息: " + msg);
            }
        };

        CancelCallback cancelCallback = new CancelCallback() {
            @Override
            public void handle(String consumerTag) throws IOException {
                System.out.println("队列A消息中断: " + consumerTag);
            }
        };

        // autoAck: true 表示自动应答;false 表示手动应答
        channel.basicConsume(queueA, true, deliverCallback, cancelCallback);
    }
}

3.客户端B消费者

public class PubSubConsumerB {
    public static String queueA = "fanout_queue_b";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("113.133.166.59");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        System.out.println("消费端等待接收消息......");
        DeliverCallback deliverCallback = new DeliverCallback() {
            @Override
            public void handle(String consumerTag, Delivery message) throws IOException {
                String msg = new String(message.getBody());
                System.out.println("队列B接受到消息: " + msg);
            }
        };

        CancelCallback cancelCallback = new CancelCallback() {
            @Override
            public void handle(String consumerTag) throws IOException {
                System.out.println("队列B消息中断: " + consumerTag);
            }
        };

        channel.basicConsume(queueA, true, deliverCallback, cancelCallback);
    }
}

四、路由模式

该种模式就是在 Exchange 和 队列 绑定时需要指定一个 routingKey (路由key)。

  • 生产者向 Exchange 发送消息时必须指定消息的 routingKey
  • Exchange 不直接把消息交给绑定的队列,而是根据 routingKey 进行判断,只有队列中绑定的 routingKey 和 消息中的 routingKey 完全一直时,消息才会被转发到该队列中

image.png

1. 生产者

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

        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("113.133.166.59");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        String exchangeName = "exchange.direct.test";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, false, true, null);

        String queueError = "direct_error";
        String queueInfo = "direct_info";
        channel.queueDeclare(queueError, false, false, true, null);
        channel.queueDeclare(queueInfo, false, false, true, null);

        channel.queueBind(queueError, exchangeName, "error");
        // 队列B绑定routingKey : info  error   warning
        channel.queueBind(queueInfo, exchangeName, "info");
        channel.queueBind(queueInfo, exchangeName, "error");
        channel.queueBind(queueInfo, exchangeName, "warning");

        String body = "日志级别:info";
        channel.basicPublish(exchangeName, "info", null, body.getBytes(StandardCharsets.UTF_8));

        channel.close();
        connection.close();
        System.out.println("RouteProducer done");
    }
}

image.png 启动生产者以后,我们可以在管理员后台看到队列 direct_info 中有一条未消费的消息,它绑定在 exchange.direct.test 这个 Exchange 中,对应的 routingKey 有 error 、info 和 warning

2. 错误信息消费者

public class RouteErrorConsumer {
    public static String queueA = "direct_error";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("113.133.166.59");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        System.out.println("RouteError等待接收消息......");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("RouteError 接受到消息 body : " + new String(body));
            }
        };
        channel.basicConsume(queueA, true, consumer);
    }
}

3. 正常信息消费者

public class RouteInfoConsumer {
    private static String queueInfo = "direct_info";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("113.133.166.59");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        System.out.println("RouteInfo等待接收消息......");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("RouteInfo 接受到消息 body : " + new String(body));
            }
        };
        channel.basicConsume(queueInfo, true, consumer);
    }
}

五、Topic 主题模式

该中模式可以实现 Pub/Sub 发布与订阅模式 和 Routing路由模式的 功能,只是 Topic 模式在配置 routingKey 时可以使用通配符,使得配置方式更加灵活。

image.png

  • *(星号):可以代替一个单词
  • #(井号):可以代替零个或者多个单词
  • 当一个队列绑定的 routingKey 是 #(井号),那么这个队列将接收所有数据,类似 fanout 类型 Exchange
  • 当一个队列绑定的 routingKey 中没有 #(井号)和 *(星号),类似 direct 类型 Exchange

根据上图routingKey的绑定关系,请看如下例子:

  • 消息被 Q1 接受到
    quick.orange.dog

  • 消息被 Q2 接受到
    lazy.brown.dog
    lazy.pink.rabbit (虽然满足两个routingKey绑定,但只被队列 Q2 接收一次)

  • 消息被 Q1、Q2 接受到
    quick.orange.rabbit
    lazy.orange.test

  • 消息被丢弃
    quick.brown.dog (没有匹配上任何routingKey)

1. 生产者

public class TopicProducer {

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitChannelUtil.getChannel();

        String exchangeName = "exchange.topic.test";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, false, false, null);

        /**
         * Q1 :绑定routingKey 是 *.orange.*
         * Q2 :绑定routingKey 是 *.*.rabbit 和 lazy.#
         */
        Map<String, String> bindingKeyMap = new HashMap<>();
        bindingKeyMap.put("quick.orange.rabbit", "Q1Q2:quick.orange.rabbit");
        bindingKeyMap.put("lazy.orange.test", "Q1Q2:lazy.orange.test");
        bindingKeyMap.put("quick.orange.dog", "Q1:quick.orange.dog");
        bindingKeyMap.put("lazy.brown.dog", "Q2:lazy.brown.dog");

        for (Map.Entry<String, String> entry : bindingKeyMap.entrySet()) {
            String bindingKey = entry.getKey();
            String body = entry.getValue();
            // 发送消息
            channel.basicPublish(exchangeName, bindingKey, null, body.getBytes(StandardCharsets.UTF_8));
        }

        System.out.println("TopicProducer done");
    }
}

2. *.orange.* 的消费者

public class OrangeTopicConsumer {
    public static String queueName = "Q1";
    private static String exchangeName = "exchange.topic.test";

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

        Channel channel = RabbitChannelUtil.getChannel();
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);

        channel.queueDeclare(queueName, false, false, false, null);
        channel.queueBind(queueName, exchangeName, "*.orange.*");

        System.out.println("OrangeTopicConsumer等待接收消息......");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("OrangeTopicConsumer 接受到消息 body : " + new String(body));
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

image.png

3. *.*.rabbit lazy.# 的消费者

public class LazyTopicConsumer {
    public static String queueName = "Q2";
    private static String exchangeName = "exchange.topic.test";

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

        Channel channel = RabbitChannelUtil.getChannel();
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);

        channel.queueDeclare(queueName, false, false, false, null);
        channel.queueBind(queueName, exchangeName, "*.*.rabbit");
        channel.queueBind(queueName, exchangeName, "lazy.#");

        System.out.println("LazyTopicConsumer等待接收消息......");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("LazyTopicConsumer 接受到消息 body : " + new String(body));
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

image.png

六、工作模式总结

  1. 简单模式

  一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)。

  1. 工作队列模式(Work Queue)

  一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)。

  1. 发布订阅模式(Publish / Subscribe)

  需要设置类型为 fanout 的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列。

  1. 路由模式(Routing)

  需要设置类型为 direct 的交换机,交换机和队列进行绑定,并且指定 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。

  1. 通配符模式(Topic)

  需要设置类型为 topic 的交换机,交换机和队列进行绑定,并且指定通配符方式的 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。

七、写在最后