【消息队列】RabbitMQ四种工作模式

258 阅读13分钟

【消息队列】RabbitMQ四种工作模式

学一门技术,先了解背景,为什么会出现它,它解决了什么问题,任何一项技术都不是凭空出生,肯定是为了解决某一个问题才出现的。

上一篇我们刚刚写的《【消息队列】RabbitMP入门实战》文章就是简单模式,只是简化到了最简单的情况:生产者只有一个发送一个消息。

消费者也只有一个,消息也只能被这个消费者消费,所以HelloWorld也称为简单模式。

如果是:生产者发送多个消息,由多个消费者来竞争,谁抢到算谁的?

其实

  • 多个消费者监听同一个队列,则各消费者之间对同一个消息是竞争的关系。
  • Work Queues工作模式适用于任务较重或任务较多的情况,多消费者分摊任务,可以提高消息处理的效率。

一、工作队列模式

工作队列模式概述:工作队列模式允许一个生产者将消息发送到队列,然后由多个消费者共享这个队列中的消息。消息一旦被消费者接收,就会从队列中删除。这种模式通常用于负载均衡和任务分发。

应用场景和示例

  • 订单处理:一个订单系统接收到新的订单后,将其发送到工作队列。多个订单处理服务实例从队列中取出订单并处理。
  • 图片处理:用户上传图片后,图片处理服务将图片处理任务发送到工作队列,多个图片处理工作进程从队列中取出任务并执行。

RabbitMQ核心功能和优势

  • 通过队列实现了消息的缓冲和存储。- 消费者之间可以并行处理消息,提高系统的吞吐量和响应速度。

下面运行看看demo效果:

注意:运行的时候先启动消费者1,2程序,然后再启动生产者端程序。 如果已经运行过生产者程序,则手动把work_queue队列删掉。

1. 生产者代码

封装工具类:ConnectionUtil.class

package com.nateshao.work_queue.util;
​
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class ConnectionUtil {
    public static final String HOST_ADDRESS = "localhost";
    public static Connection getConnection() throws Exception {
        // 定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 设置服务地址
        factory.setHost(HOST_ADDRESS);
        // 端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("123456");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
​
    public static void main(String[] args) throws Exception {
        Connection con = ConnectionUtil.getConnection();
        // amqp://guest@localhost:5672/
        System.out.println(con);
        con.close();
    }
}

消费者:Producer.class

package com.nateshao.work_queue;
​
import com.nateshao.work_queue.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Producer {
    public static final String QUEUE_NAME = "work_queue";
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        for (int i = 1; i <= 10; i++) {
            String body = i + "hello rabbitmq~~~";
            channel.basicPublish("", QUEUE_NAME, null, body.getBytes());
        }
        channel.close();
        connection.close();
    }
}

1.1 发送消息效果

可以看到name显示work_queue,记录着10条消息

2. 消费者代码:Consumer.class

创建Consumer1和Consumer2。Consumer2只是类名和打印提示不同,代码完全一样。

Consumer1.class

package com.nateshao.work_queue;
​
import com.nateshao.work_queue.util.ConnectionUtil;
import com.rabbitmq.client.*;
​
import java.io.IOException;
/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Consumer1 {
    static final String QUEUE_NAME = "work_queue";
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Consumer1 body:" + new String(body));
            }
        };
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

Consumer2.class

package com.nateshao.work_queue;
​
import com.nateshao.work_queue.util.ConnectionUtil;
import com.rabbitmq.client.*;
​
import java.io.IOException;
/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Consumer2 {
    static final String QUEUE_NAME = "work_queue";
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Consumer1 body:" + new String(body));
            }
        };
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

2.1 运行效果

最终两个消费端程序竞争结果如下:

二、发布订阅模式

在进入发布订阅模式之前,可以先了解一下交换机

生产者不是把消息直接发送到队列,而是发送到交换机,交换机接收消息,而如何处理消息取决于交换机的类型

交换机有如下3种常见类型

  • Fanout:广播,将消息发送给所有绑定到交换机的队列
  • Direct:定向,把消息交给符合指定routing key的队列
  • Topic:通配符,把消息交给符合routing pattern(路由模式)的队列

其实

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

Publish/Subscribe 模式说明

组件之间关系:

  • 生产者把消息发送到交换机
  • 队列直接和交换机绑定

工作机制:消息发送到交换机上,就会以广播的形式发送给所有已绑定队列

  • 理解概念:

    • Publish:发布,这里就是把消息发送到交换机上
    • Subscribe:订阅,这里只要把队列和交换机绑定,事实上就形成了一种订阅关系

应用场景和示例

  • 实时日志系统:多个应用实例将日志消息发送到交换机,交换机将消息广播到多个日志收集队列,每个队列对应一个日志收集服务实例。
  • 新闻推送:新闻发布系统发布新闻到交换机,交换机将新闻广播到多个用户订阅的队列,用户从各自的队列中接收新闻推送。

RabbitMQ核心功能和优势

  • 交换机和队列的解耦设计,使得消息可以被广播到多个队列。
  • 消费者可以按需订阅自己关心的消息,实现消息的灵活分发。

依赖导入:pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.nateshao</groupId>
    <artifactId>code3_publish_subscribe</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>code3_pubsub</name>
    <description>pubsub</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.20.0</version>
        </dependency>
    </dependencies>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
​
</project>
​

1.1 生产者代码

package com.nateshao.pubsub;
​
import com.nateshao.pubsub.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Producer {
    public static void main(String[] args) throws Exception {
        // 1、获取连接
        Connection connection = ConnectionUtil.getConnection();
        // 2、创建频道
        Channel channel = connection.createChannel();
        // 参数1. exchange:交换机名称
        // 参数2. type:交换机类型
        //     DIRECT("direct"):定向
        //     FANOUT("fanout"):扇形(广播),发送消息到每一个与之绑定队列。
        //     TOPIC("topic"):通配符的方式
        //     HEADERS("headers"):参数匹配
        // 参数3. durable:是否持久化
        // 参数4. autoDelete:自动删除
        // 参数5. internal:内部使用。一般false
        // 参数6. arguments:其它参数
        String exchangeName = "test_fanout";
​
        // 3、创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT,true,false,false,null);
​
        // 4、创建队列
        String queue1Name = "test_fanout_queue1";
        String queue2Name = "test_fanout_queue2";
​
        channel.queueDeclare(queue1Name,true,false,false,null);
        channel.queueDeclare(queue2Name,true,false,false,null);
​
        // 5、绑定队列和交换机
        // 参数1. queue:队列名称
        // 参数2. exchange:交换机名称
        // 参数3. routingKey:路由键,绑定规则
        //     如果交换机的类型为fanout,routingKey设置为""
        channel.queueBind(queue1Name,exchangeName,"");
        channel.queueBind(queue2Name,exchangeName,"");
        String body = "日志信息:张三调用了findAll方法...日志级别:info...";
        // 6、发送消息
        channel.basicPublish(exchangeName,"",null,body.getBytes());
        // 7、释放资源
        channel.close();
        connection.close();
    }
}

1.2 消费者代码

消费者1号:Consumer1.class

package com.nateshao.pubsub;
​
import com.nateshao.pubsub.util.ConnectionUtil;
import com.rabbitmq.client.*;
​
import java.io.IOException;
/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Consumer1 {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String queue1Name = "test_fanout_queue1";
        channel.queueDeclare(queue1Name, true, false, false, null);
        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));
                System.out.println("队列 1 消费者 1 将日志信息打印到控制台.....");
            }
        };
        channel.basicConsume(queue1Name, true, consumer);
    }
}

消费者2号:Consumer2.class

package com.nateshao.pubsub;
​
import com.nateshao.pubsub.util.ConnectionUtil;
import com.rabbitmq.client.*;
​
import java.io.IOException;
/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Consumer2 {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String queue1Name = "test_fanout_queue2";
        channel.queueDeclare(queue1Name, true, false, false, null);
        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));
                System.out.println("队列 2 消费者 2 将日志信息打印到控制台.....");
            }
        };
        channel.basicConsume(queue1Name, true, consumer);
    }
}

1.3 运行效果

先启动消费者,然后再运行生产者程序发送消息:

1.4 小结

交换机和队列的绑定关系如下图所示:

交换机需要与队列进行绑定,绑定之后,一个消息可以被多个消费者都收到。

发布订阅模式与工作队列模式的区别:

  • 工作队列模式本质上是绑定默认交换机
  • 发布订阅模式绑定指定交换机
  • 监听同一个队列的消费端程序彼此之间是竞争关系
  • 绑定同一个交换机的多个队列在发布订阅模式下,消息是广播的,每个队列都能接收到消息

三、路由模式

通过『路由绑定』的方式,把交换机和队列关联起来

  • 交换机和队列通过路由键进行绑定
  • 生产者发送消息时不仅要指定交换机,还要指定路由键
  • 交换机接收到消息会发送到路由键绑定的队列

在编码上与 Publish/Subscribe发布与订阅模式的区别:

  1. 交换机的类型为:Direct
  2. 队列绑定交换机的时候需要指定routing key。

路由模式概述:路由模式在发布/订阅模式的基础上增加了路由键(Routing Key)的概念。生产者发送消息时指定路由键,交换机根据路由键将消息发送到匹配的队列。

应用场景和示例

  • 邮件系统:邮件系统根据邮件类型(如工作邮件、个人邮件等)设置不同的路由键,交换机根据路由键将邮件发送到不同的处理队列。
  • 视频监控系统:摄像头产生的视频流根据监控区域设置不同的路由键,交换机根据路由键将视频流发送到对应的处理队列。

RabbitMQ核心功能和优势

  • 通过路由键实现了消息的过滤和分发,使得消息能够精确地发送到目标队列。
  • 提高了系统的灵活性和可扩展性。

1. 生产者代码

Producer.class

package com.nateshao.routing;

import com.nateshao.routing.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Producer {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String exchangeName = "test_direct";
        // 创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true, false, false, null);
        // 创建队列
        String queue1Name = "test_direct_queue1";
        String queue2Name = "test_direct_queue2";
        // 声明(创建)队列
        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);
        // 队列绑定交换机
        // 队列1绑定error
        channel.queueBind(queue1Name, exchangeName, "error");
        // 队列2绑定info error warning
        channel.queueBind(queue2Name, exchangeName, "info");
        channel.queueBind(queue2Name, exchangeName, "error");
        channel.queueBind(queue2Name, exchangeName, "warning");
        String message = "日志信息:张三调用了delete方法.错误了,日志级别error";
        // 发送消息  
        channel.basicPublish(exchangeName, "error", null, message.getBytes());
        System.out.println(message);
        // 释放资源
        channel.close();
        connection.close();
    }
}

2. 消费者代码

1、消费者1号:Consumer1.class

package com.nateshao.routing;

import com.nateshao.routing.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;

/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Consumer1 {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String queue1Name = "test_direct_queue1";
        channel.queueDeclare(queue1Name, true, false, false, null);
        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));
                System.out.println("Consumer1 将日志信息打印到控制台.....");
            }
        };
        channel.basicConsume(queue1Name, true, consumer);
    }
}

消费者2号:Consumer2.class

package com.nateshao.routing;

import com.nateshao.routing.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;

/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Consumer2 {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String queue2Name = "test_direct_queue2";
        channel.queueDeclare(queue2Name, true, false, false, null);
        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));
                System.out.println("Consumer2 将日志信息存储到数据库.....");
            }
        };
        channel.basicConsume(queue2Name, true, consumer);
    }
}

3. 运行结果

4.绑定关系

四、主题模式

topic类型与Direct相比,都是可以根据RoutingKey把消息路由到不同的队 列。只不过Topic类型Exchange可以让队列在绑定Routing key的时候使用 通配符

  • Routingkey一般都是由一个或多个单词组成,多个单词之间以“.”分割, 例如:item.insert

通配符规则:

  • #:匹配零个或多个词 • *:匹配一个词

主题模式概述:主题模式在路由模式的基础上使用了更复杂的路由规则。路由键不再是一个简单的字符串,而是一个由点分隔的字符串。交换机根据路由键和队列绑定的模式进行匹配,将消息发送到匹配的队列。

应用场景和示例

  • 股票交易系统:股票交易系统根据股票的代码和类型设置路由键,如"US.STOCK.AAPL"表示美国股市的苹果公司股票。交换机根据路由键和队列绑定的模式(如"US.*"表示接收美国股市的所有股票信息)将消息发送到匹配的队列。

RabbitMQ核心功能和优势

  • 通过模式匹配实现了更复杂的消息过滤和分发。
  • 使得系统能够灵活地处理大量的消息和多样化的需求。

1.生产者代码

package com.nateshao.topics;

import com.nateshao.topics.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Producer {
    public static void main(String[] args) throws Exception {
        Connection connection   = ConnectionUtil.getConnection();
        Channel    channel      = connection.createChannel();
        String     exchangeName = "test_topic";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, true, false, false, null);
        String queue1Name = "test_topic_queue1";
        String queue2Name = "test_topic_queue2";
        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);
        // 绑定队列和交换机  
        // 参数1. queue:队列名称
        // 参数2. exchange:交换机名称
        // 参数3. routingKey:路由键,绑定规则
        //      如果交换机的类型为fanout ,routingKey设置为""
        // routing key 常用格式:系统的名称.日志的级别。  
        // 需求: 所有error级别的日志存入数据库,所有order系统的日志存入数据库  
        channel.queueBind(queue1Name, exchangeName, "#.error");
        channel.queueBind(queue1Name, exchangeName, "order.*");
        channel.queueBind(queue2Name, exchangeName, "*.*");

        // 分别发送消息到队列:order.info、goods.info、goods.error  
        String body = "[所在系统:order][日志级别:info][日志内容:订单生成,保存成功]";
//        channel.basicPublish(exchangeName,"order.info",null,body.getBytes());

//        body = "[所在系统:goods][日志级别:info][日志内容:商品发布成功]";
//        channel.basicPublish(exchangeName,"goods.info",null,body.getBytes());

        body = "[所在系统:goods][日志级别:error][日志内容:商品发布失败]";
        channel.basicPublish(exchangeName, "goods.error", null, body.getBytes());
        channel.close();
        connection.close();
    }
}

2.消费者代码

2.1 消费者1号

消费者1监听队列1:

package com.nateshao.topics;

import com.nateshao.topics.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
class Consumer1 {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel    channel    = connection.createChannel();
        String     QUEUE_NAME = "test_topic_queue1";
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        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));
            }
        };
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

2.2 消费者2号

消费者2监听队列2:

package com.nateshao.topics;

import com.nateshao.topics.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
public class Consumer2 {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String QUEUE_NAME = "test_topic_queue2";
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);
        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));
            }
        };
        channel.basicConsume(QUEUE_NAME,true,consumer);
    }
}

3.运行效果

关键代码如下

channel.queueBind(queue1Name, exchangeName, "#.error");
channel.queueBind(queue1Name, exchangeName, "order.*");
channel.queueBind(queue2Name, exchangeName, "*.*");

// 分别发送消息到队列:order.info、goods.info、goods.error  
String body = "[所在系统:order][日志级别:info][日志内容:订单生成,保存成功]";
channel.basicPublish(exchangeName,"order.info",null,body.getBytes());

路由匹配队列1,2

队列1:

队列2: