什么是MQ?

MQ全称为Message Queue,即消息队列。“消息队列”是在消息的传输过程中保存消息的容器。也是在消息的传输过程中保存消息的容器。用于分布式系统之间进行通信。

典型的:生产者、消费者模型。

MQ本身就相当于一个中间件,用于生产方通过MQ与消费方之间的交互作用。

中间件

RabbitMQ应用场景

场景一 、解耦

上面我们提到MQ本身就相当于一个中间件,用于生产方通过MQ与消费方之间的交互作用。让我们看一下传统模式和这种中间件模式的对比:

传统模式

系统间耦合性太强,如图所示,系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!

耦合

中间件模式:

​ 将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统A不需要做任何修改。

中间件模式

场景二 、异步

传统模式:

​ 一些非必要的业务逻辑以同步的方式运行,太耗费时间

同步传统模式

中间件模式:

​ 将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度

中间件模式

场景三、削峰

传统模式:

​ 并发量大的时候,所有的请求直接怼到数据库,造成数据库连接异常

传统模式直连数据库

中间件模式:

​ 系统A慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息

中间件模式

RabbitMQ的使用

RabbitMQ是一个消息代理:它接受和转发消息。 你可以把它想象成一个邮政信箱

RabbitMQ与邮局的主要区别是它不处理纸张,而是接受,存储和转发数据消息的二进制数据块。

如何避免消息堆积?

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

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

如何避免消息丢失?

1) 消费者的ACK机制。可以防止消费者丢失消息。

但是,如果在消费者消费之前,MQ就宕机了,消息就没了?

2)可以将消息进行持久化。要将消息持久化,前提是:队列、Exchange都持久化

RabbitMQ提供了6种消息模型,但是第6种是RPC,并不是MQ,因此不学习。那么也就剩下5种,3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。

六种模型

1.Simple-简单模型

简单模型

P(producer/ publisher):生产者,一个发送消息的用户应用程序。

C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序

队列(红色区域):rabbitmq内部类似于邮箱的一个概念。虽然消息流经rabbitmq和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。

引入依赖

我们将用Java编写两个程序;发送单个消息的生产者,以及接收消息并将其打印出来的消费者。我们将详细介绍Java API中的一些细节,这是一个消息传递的“Hello World”。我们将调用我们的消息发布者(发送者)Send和我们的消息消费者(接收者)Recv。发布者将连接到RabbitMQ,发送一条消息,然后退出。

新建一个spring boot项目,使用spring 2和jdk 8,引入下面的依赖
<!--引入rabbitmq依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    <version>3.1.5</version>
</dependency>

新建一个连接工具类:

package com.test.demo.util;

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

/**
 * rabbitMQ连接类
 *
 * @author jg
 * @since 2023/11/7 15:27
 */
public class ConnectionUtil {

    /**
     * 建立与RabbitMQ的连接
     *
     * @return Connection
     * @throws Exception 异常
     */
    public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("你的IP地址");
        //端口,rabbitmq默认端口号为5672,阿里云等服务器记得开启防火墙和安全组
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setUsername("username");
        factory.setPassword("password");
        //虚拟主机路径,不同虚拟主机之间的数据不共享
        factory.setVirtualHost("/");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}
生产者

创建一个send类,充当简单模型生产者来发送消息

package com.test.demo.util.simple;

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

/**
 * 简单模式---生产者
 *
 * @author jg
 * @since 2023/11/7 15:33
 */
public class Send {

    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        // 轻量级的 Connection,这是完成大部分API的地方。
        Channel channel = connection.createChannel();

        // 声明(创建)队列,必须声明队列才能够发送消息,我们可以把消息发送到队列中。
        // 声明一个队列是幂等的 - 只有当它不存在时才会被创建
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 消息内容
        String message = "Hello World!";
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");

        //关闭通道和连接
        channel.close();
        connection.close();
    }

}

运行之后控制台会出现:

控制台

这时候我们打开rabbitMQ的web页面,登陆上去查看队列中的信息:

图形页面

查看消息详情

这就是我们刚才发的消息,在页面的控制台查看消息是不会进行消费的,所以消息还在

消费者

创建消费者来获取消息

package com.test.demo.util.simple;

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

import java.io.IOException;

/**
 * 简单模型---消费者
 * 自动ACK:消息一旦被接收,消费者自动发送ACK
 *
 * @author jg
 * @since 2023/11/7 15:56
 */
public class Recv {
    private final static String QUEUE_NAME = "simple_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);
        // 定义队列的消费者
        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);
                //添加异常,测试自动ACK
                //int i = 1 / 0;
                System.out.println(" [x] received : " + msg + "!");
            }
        };
        // 监听队列,第二个参数:是否自动进行消息确认。
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

运行之后控制台会出现:

消费者

这时,我们再点开rabbitmq的web页面就会发现刚才发的消息没有了

页面

因为我们并没有关闭消费者的连接,所以消费者是一直可以接收生产者发送的消息,每调用一次生产者Send发送消息,消费者Recv的控制台都会出现一条信息

消息确认机制(ACK)

通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。

那么问题来了:RabbitMQ怎么知道消息被接收了呢?

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

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

选择哪种ACK主要看消息的重要性:

  • 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
  • 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。
测试自动ACK

还是拿简单模式举例:

在简单模式的消费者Recv中添加代码,引发异常:

添加代码

注意最后一行代码,第二个参数为true时,就会自动进行ACK

channel.basicConsume(QUEUE_NAME, true, consumer);

修改完成之后我们重启消费者Recv,然后启动生产者发送一条信息

消费者

消费

可以看到控制台报错,但是rabbitMQ的队列中消息依旧被消费了

测试手动ACK

我们注释掉引发报错的代码,并且把basicConsume方法第二个参数改为false(手动ACK)

手动ACK

先重启Recv然后用生产者发送一条信息

手动ACK

可以看到这里有一条未进行ACK消息确认的信息.

然后我们把Recv消费者停掉,控制台就会出现以下变化

变化

这是因为我们没有对消息进行ACK,所以消息没有被消费掉,在消费者宕机之后,消息就会再次进入ready状态

现在我们修改代码,进行手动ACK

                /**
                 * 手动进行ACK
                 *  deliveryTag:该消息的index
                 *  multiple:是否批量处理.true:将一次性ack所有消息。
                 */
                channel.basicAck(envelope.getDeliveryTag(), false);

修改代码

改完代码重新启动消费者Recv,打开控制台就会发现消息已经没了:

消费

消息手动ACK成功!

2.Work-工作模型

工作队列或者竞争消费者模式

工作队列,又称任务队列。主要思想就是避免执行资源密集型任务时,必须等待它执行完成。相反我们稍后完成任务,我们将任务封装为消息并将其发送到队列。 在后台运行的工作进程将获取任务并最终执行作业。当你运行许多消费者时,任务将在他们之间共享,但是一个消息只能被一个消费者获取。

这个概念在Web应用程序中特别有用,因为在短的HTTP请求窗口中无法处理复杂的任务。

工作模型

假设现在有以下场景:

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

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

C2:消费者2:领取任务并完成任务,假设完成速度慢

生产者

跟简单模型基本一样,不过这里是循环发送50条消息。

package com.test.demo.util.work;

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

/**
 * work模型---生产者
 * 工作模型:一个生产者对应多个消费者,一个消息只能被一个消费者消费
 * @author jg
 * @since 2023/11/7 16:30
 */
public class Send {
    private final static String QUEUE_NAME = "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();
    }
}
消费者1
package com.test.demo.util.work;

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

import java.io.IOException;

/**
 * 工作模型消费者1
 * @author jg
 * @since 2023/11/7 16:32
 */
public class Recver1 {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        String QUEUE_NAME = "work_queue";
        channel.queueDeclare(QUEUE_NAME,false,false,false, null);
        //保证一次只分发一个
        channel.basicQos(1);
        //接收消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                try {
                    String msg = new String(body);
                    System.out.println("Recver1:" + msg);
                    channel.basicAck(envelope.getDeliveryTag(),false);

                    Thread.sleep(2000);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        };
        channel.basicConsume(QUEUE_NAME,false, consumer);
    }
}
消费者2
package com.test.demo.util.work;

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

import java.io.IOException;

/**
 * 工作模型消费者2
 *
 * @author jg
 * @since 2023/11/7 16:33
 */
public class Recver2 {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        String QUEUE_NAME = "work_queue";
        channel.queueDeclare(QUEUE_NAME,false,false,false, null);
        //保证一次只分发一个
        channel.basicQos(1);

        //接收消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                try {
                    String msg = new String(body);
                    System.out.println("Recver2:" + msg);
                    channel.basicAck(envelope.getDeliveryTag(),false);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        };
        channel.basicConsume(QUEUE_NAME,false, consumer);
    }
}

消费者2与消费者1基本类似,就是没有设置消费耗时时间。

这里是模拟有些消费者快,有些比较慢。

接下来,两个消费者一同启动,然后发送50条消息:

消费者1

消费者2

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

刚才的实现有问题吗?

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

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

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

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

怎么实现呢?

    我们可以使用basicQos方法和prefetchCount = 1设置。 这告诉RabbitMQ一次不要向工作人员发送多于一条消息。 或者换句话说,不要向工作人员发送新消息,直到它处理并确认了前一个消息。 相反,它会将其分派给不是仍然忙碌的下一个工作人员。

两个消费者对象中都需添加:

//设置每个消费者同时只能处理一条信息
channel.basicQos(1);

添加代码

改完之后再次测试就会发现消费者2处理了47条信息,消费者1实现了3条信息:

再次测试

这就实现能者多劳了.

3.Fanout-广播模型

导图

在广播模式下,消息发送流程是这样的:

1) 可以有多个消费者

2) 每个消费者有自己的queue(队列)

3) 每个队列都要绑定到Exchange(交换机)

4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。

5) 交换机把消息发送给绑定过的所有队列

6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费

生产者

跟简单模型和工作模型相比有两个变化:

  • 1) 声明Exchange,不再声明Queue
  • 2) 发送消息到Exchange,不再发送到Queue
package com.test.demo.util.fanout;

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

/**
 * 广播模式--生产者
 * 1) 声明Exchange,不再声明Queue
 * 2) 发送消息到Exchange,不再发送到Queue
 *
 * @author jg
 * @since 2023/11/8 10:35
 */
public class Send {

    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    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 = "Hello everyone";
        // 发布消息到Exchange
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [生产者] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}
消费者1
package com.test.demo.util.fanout;

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

import java.io.IOException;

/**
 * 广播模式--消费者1
 * 代码中:队列需要和交换机绑定
 * @author jg
 * @since 2023/11/8 10:36
 */
public class Recv {
    private final static String QUEUE_NAME = "fanout_exchange_queue_1";

    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    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(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
消费者2
package com.test.demo.util.fanout;

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

import java.io.IOException;

/**
 * 广播模式--消费者2
 * 代码中:队列需要和交换机绑定
 * @author jg
 * @since 2023/11/8 10:36
 */
public class Recv2 {
    private final static String QUEUE_NAME = "fanout_exchange_queue_2";

    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    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(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
因为我们这里是先声明交换机,然后再声明队列,所以我们要先启动生产者Send,然后再启动两个消费者,否则会报错
测试结果

启动完成后我们重新运行生产者Send发送一条信息,可以看到两个消费者的控制台出现:

消费者1

消费者2

这就说明广播模式创建成功了

广播模式是将消息发送给交换机下所有的队列,不用选择,所以速度也是最快的

4.Direct-定向模型

  • Direct-定向模型
  • 有选择性的接收消息
  • 在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。
  • 在路由模式中,我们将添加一个功能 - 我们将只能订阅一部分消息。 例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
  • 但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
  • 在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
  • 消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。导图

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

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

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

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

生产者
package com.test.demo.util.direct;

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

/**
 * 路由(定向)模式--生产者
 *
 * @author jg
 * @since 2023/11/8 13:51
 */
public class Send {
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为direct
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        // 消息内容
        String message = "商品新增了, id = 1001";
        String message2 = "商品修改了, id = 1002";
        String message3 = "商品删除了, id = 1003";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
        channel.basicPublish(EXCHANGE_NAME, "update", null, message2.getBytes());
        channel.basicPublish(EXCHANGE_NAME, "delete", null, message3.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}
消费者1
package com.test.demo.util.direct;

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

import java.io.IOException;
/**
 * 定向模式--消费者1
 *
 * @author jg
 * @since 2023/11/8 13:52
 */
public class Recv {
    private final static String QUEUE_NAME = "direct_exchange_queue_1";
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    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。假设此处需要update和delete消息
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

        // 定义队列的消费者
        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);
    }
}
消费者2
package com.test.demo.util.direct;

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

import java.io.IOException;
/**
 * 定向模式--消费者2
 *
 * @author jg
 * @since 2023/11/8 13:52
 */
public class Recv2 {
    private final static String QUEUE_NAME = "direct_exchange_queue_2";
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    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。订阅 insert、update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

        // 定义队列的消费者
        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);
    }
}

还是先启动Send来创建交换机Exchange,然后再启动两个消费者

测试结果

我们分别发送增、删、改的RoutingKey,发现结果:

消费者1

消费者2

定向模型的路由是全匹配的,不能带.或者#等符号,否则会获取不到消息

5.Topic-主题模型

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

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

通配符规则:

#:匹配一个或多个词

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

举例:

audit.#:能够匹配:audit.irs.corporate` 或者 `audit.irs
audit.*:只能匹配:audit.irs
*.audit.*:只能匹配:test.audit.irs

topic主题模型

我们创建了两个绑定:Q1绑定了绑定键“*.orange.*”,Q2绑定了“*.*.rabbit”和“lazy.#”。
Q1匹配所有的橙色动物。
Q2匹配关于兔子以及懒惰动物的消息。

生产者发送如下消息,会进入哪个队列?

  • quick.orange.rabbit--Q1
  • lazy.orange.elephant--Q2
  • quick.orange.fox--Q1
  • lazy.pink.rabbit--Q2
  • quick.brown.fox--无
  • quick.orange.male.rabbit--无
  • orange--无

使用topic类型的Exchange,发送消息的routing key有3种: item.isnertitem.updateitem.delete

生产者
package com.test.demo.util.topic;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import com.test.demo.util.ConnectionUtil;
import org.springframework.amqp.rabbit.core.RabbitTemplate;

/**
 * Topic主题模型--生产者
 *
 * @author jg
 * @since 2023/11/8 14:03
 */
public class Send {
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为topic
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 消息内容
        String message = "新增商品 : id = 1001";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}
消费者1
package com.test.demo.util.topic;

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

import java.io.IOException;

/**
 * Topic主题模型--消费者
 *
 * @author 蒋果
 * @since 2023/11/8 14:04
 */
public class Recv {
    private final static String QUEUE_NAME = "topic_exchange_queue_1";
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    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。需要 update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete");

        // 定义队列的消费者
        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);
    }
}
消费者2
package com.test.demo.util.topic;

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * Topic主题模型--消费者2
 *
 * @author 蒋果
 * @since 2023/11/8 14:04
 */
/**
 * 消费者2
 */
public class Recv2 {
    private final static String QUEUE_NAME = "topic_exchange_queue_2";
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
        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 + "!");
                channel.basicAck(envelope.getDeliveryTag(), false);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

还是一样,先启动Send生产者创建交换机,然后再启动消费者,最后再重启生产者发送信息

测试结果

只有消费者2接收到消息,说明主题模式搭建成功!

消费者2

持久化

如何避免消息丢失?

1) 消费者的手动ACK机制。可以防止业务处理失败。

2) 但是,如果在消费者消费之前,MQ就宕机了,消息就没了。

是可以将消息进行持久化呢?

要将消息持久化,前提是:队列、Exchange都持久化

我们再前面的例子中创建了很多的交换机和队列:

交换机:

交换机

队列:

队列

现在我们重启下rabbitMQ服务:

重启rabbitmq服务

重启后,我们之前创建的交换机和队列都消失了

重启后的交换机

重启后的队列

正是因为这样,所以我们才需要持久化.

1.交换机持久化

交换机持久化

修改完成后启动,然后查看rabbitmq的交换机控制台

持久化交换机

出现这个标记,就表示这个交换机是持久化状态

亲测,重启后,这个交换机还在

2.队列持久化

队列持久化

运行后查看队列控制台:

队列持久化

队列持久化成功!

3.消息持久化

非持久化:
在做非持久化测试时,消费者不能设置自动ACK,要手动ACK,因为设置了线程睡眠rabbitmq的队列控制台不会显示消息数量.

生产者

package com.test.demo.util.topic;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import com.test.demo.util.ConnectionUtil;
import org.springframework.amqp.rabbit.core.RabbitTemplate;

/**
 * Topic主题模型--生产者
 *
 * @author jg
 * @since 2023/11/8 14:03
 */
public class Send {
    private final static String EXCHANGE_NAME = "topic_exchange_test";

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

        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为topic
        channel.exchangeDeclare(EXCHANGE_NAME, "topic",true);
        // 消息内容
        String message = "消息 : 发送";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        for (int i = 0; i < 50; i++) {
            channel.basicPublish(EXCHANGE_NAME, "quick.orange.rabbit", null, ("quick.orange.rabbit"+i).getBytes());
        }
        System.out.println(" [商品服务:] Sent '" + message + "'");

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

消费者

package com.test.demo.util.topic;

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

import java.io.IOException;

/**
 * Topic主题模型--消费者
 *
 * @author jg
 * @since 2023/11/8 14:04
 */
public class Recv {
    private final static String QUEUE_NAME = "topic_exchange_queue_1";
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        // 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete
        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 {
                try {
                    // body 即消息体
                    String msg = new String(body);
                    System.out.println(" [消费者1] received : " + msg + "!");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

可以看到消费者设置了,每接收一条信息就暂停2秒钟

Thread.sleep(2000);

发送50条信息后,在消费者还没接受完的时候,我们重启rabbitmq服务器,看看消息还在不在

消费者接收

消费者接收

控制台:

控制条

重启服务器:

重启

重启服务器

可以看到,正在处理消息时,rabbitmq服务器重启,没有设置持久化的消息会全部丢失
持久化:

在发送消息时在basicPublish方法中配置这个参数:

channel.basicPublish(EXCHANGE_NAME, "quick.orange.rabbit", MessageProperties.PERSISTENT_TEXT_PLAIN, ("quick.orange.rabbit"+i).getBytes());

消息持久化

然后发送消息,在Recv消费者处理消息的时候,重启rabbitmq服务器

重启时:

重启

重启后:

重启后

可以看到消息恢复了,并且消费者还在继续处理消息

消息持久化成功!

使用RabbitTemplate

RabbitAdmin类

RabbitAdmin类是Spring AMQP提供的一个辅助类,用于简化与RabbitMQ交互的操作。它提供了一些便捷的方法,可以用于声明和管理队列、交换机和绑定等。

RabbitAdmin类的主要作用有以下几点:

  1. 声明和删除队列:RabbitAdmin类提供了declareQueue()deleteQueue()等方法,可以用于声明和删除队列。你可以通过这些方法来创建和销毁队列,设置队列的属性和参数。
  2. 声明和删除交换机:RabbitAdmin类提供了declareExchange()deleteExchange()等方法,用于声明和删除交换机。你可以通过这些方法来创建不同类型的交换机,设置交换机的属性和参数。
  3. 声明和绑定队列和交换机:RabbitAdmin类提供了declareBinding()removeBinding()等方法,可以用于声明和删除队列和交换机之间的绑定。你可以使用这些方法来将队列绑定到交换机上,设置绑定的选项。
  4. 管理RabbitMQ组件:RabbitAdmin类还提供了其他一些方法,如declareExchange()declareBinding()等,用于管理RabbitMQ的其他组件,如连接、通道、消费者等。

通过使用RabbitAdmin类,我们可以通过简单的方法调用来完成与RabbitMQ的操作,而不需要手动编写复杂的AMQP协议代码。它提供了一种方便和易于使用的方式来管理RabbitMQ的各种组件,简化了与RabbitMQ的交互过程。

RabbitTemplate类

RabbitTemplate类是Spring AMQP提供的一个核心类,用于在应用程序中与RabbitMQ进行交互。它封装了与RabbitMQ的连接、通道和基本操作,提供了一组方法来发送和接收消息,以及处理消息发送和接收的相关逻辑。

RabbitTemplate类的主要作用有以下几点:

  1. 消息发送:RabbitTemplate类提供了多个发送消息的方法,例如convertAndSend()send()等。你可以使用这些方法将消息发送到指定的交换机和队列,并指定路由键、消息体和其他参数。
  2. 同步和异步发送:RabbitTemplate类支持同步和异步的消息发送方式。你可以选择使用同步方式发送消息,等待发送结果返回;也可以选择使用异步方式发送消息,通过回调函数处理发送结果。
  3. 消息接收:RabbitTemplate类提供了多个接收消息的方法,例如receiveAndConvert()receive()等。你可以使用这些方法从指定的队列中接收消息,并指定超时时间、消息转换器和其他参数。
  4. 消息转换:RabbitTemplate类支持消息的自动转换,可以将Java对象自动序列化为消息体,并在接收时将消息体自动反序列化为Java对象。你可以通过设置消息转换器(MessageConverter)来定制消息的序列化和反序列化过程。
  5. 消息回复:RabbitTemplate类支持发送消息后等待并接收消息的模式,用于实现请求-响应模式。你可以在发送请求时指定回调队列,并在接收到响应消息时进行处理。

通过使用RabbitTemplate类,我们可以方便地在应用程序中与RabbitMQ进行消息的发送和接收,而不需要手动处理底层的连接、通道和协议细节。它提供了一种高层次的抽象,简化了与RabbitMQ的交互过程,并提供了一些便捷的方法和功能来处理消息发送和接收的相关逻辑。

使用示例

1.配置部分

yml配置文件

server:
  port: 8080
spring:
  application:
    name: rabbitMQ-Demo

#rabbitmq连接配置
  rabbitmq:
    host: 你的IP地址
    port: 5672
    username: admin
    password: 123456
    virtual-host: /
2.代码部分
package com.test.demo.util;

import cn.hutool.json.JSONObject;
import lombok.AllArgsConstructor;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * RabbitTemplate测试类
 *
 * @author 蒋果
 * @since 2023/11/8 15:38
 */
@AllArgsConstructor
@RestController
@RequestMapping("/demo")
public class rabbitMQController {

    private final RabbitTemplate rabbitTemplate;

    @GetMapping("/send")
    public String send(@RequestParam("message") String message) {
        //创建管理器
        RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate.getConnectionFactory());
        //声明交换机
        rabbitAdmin.declareExchange(new DirectExchange("TopicExchange", true, false));
        //声明队列
        rabbitAdmin.declareQueue(new Queue("demo.Topic", true));
        //声明绑定
        rabbitAdmin.declareBinding(
                BindingBuilder
                        .bind(QueueBuilder.durable("demo.Topic").build())
                        .to(new DirectExchange("TopicExchange", true, false))
                        .with("msg")

        );
        //获取队列信息,名称,消息数量,消费者数量
        rabbitAdmin.getQueueInfo("demo.Topic");
        JSONObject msg = new JSONObject();
        msg.put("msg", message);
        msg.put("msg2", "31212312312");
        String s = msg.toString();
        rabbitTemplate.convertAndSend("DirectExchange", "msg", s);
        return message;
    }

}

RabbitAdmin类主要用于创建交换机,队列还有绑定交换机

RabbitTemplate类主要用来发送和接收队列中的信息,可以把信息转换成各种对象

@RabbitListener注解

开启注解

使用这个注解需要在启动类加上@EnableRabbit注解:

//开启rabbit注解
@EnableRabbit

image-20231109172232933

代码示例:

代码示例

package com.test.demo.util;

import cn.hutool.json.JSONObject;
import lombok.AllArgsConstructor;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;

/**
 * rabbitmq监听器
 *
 * @author 蒋果
 * @since 2023/11/9 11:11
 */
@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "demoDirect", durable = "true"),
        exchange = @Exchange(value = "DirectExchange", type = ExchangeTypes.DIRECT,declare = "true"),
        key = "msg"
))
@AllArgsConstructor
public class MyMessageListener {

    @RabbitHandler
    public void ttlListerer(String msg){
        JSONObject msg1  = msg == null ? null : new JSONObject(msg);
        if (msg1 != null) {
            System.out.println("收到消息:"+msg1.get("msg"));
        }
    }
}

一些报错的处理

1.接收消息时反序列化出错

Attempt to deserialize unauthorized class java.util.HashMap; add allowed class name patterns to the message converter or, if you trust the message orginiator, set environment variable 'SPRING_AMQP_DESERIALIZATION_TRUST_ALL' or system property 'spring.amqp.deserialization.trust.all' to true

这个错误提示的是Spring AMQP正在尝试反序列化HashMap类,但是由于安全原因,这个类并未被允许。在Spring Boot中,可以通过以下方式来配置,在启动类中添加jsonMessageConverter和classMapper方法,并注册为bean:

package com.test.demo;

import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.support.converter.DefaultClassMapper;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@EnableRabbit
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public MessageConverter jsonMessageConverter() {
        Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
        converter.setClassMapper(classMapper());
        return converter;
    }
    @Bean
    public DefaultClassMapper classMapper() {
        DefaultClassMapper classMapper = new DefaultClassMapper();
        // 添加允许的类名模式
        classMapper.setTrustedPackages("java.util.HashMap");
        return classMapper;
    }

}
最后修改:2024 年 06 月 05 日
去码头整点薯片?