1. MQ介绍

1.1 MQ是什么

MQ(Message Queue),消息队列。是进程级别的 生产者 和 消费者 模型。

1. RabbitMQ基础_erlang

1.2 MQ的作用

异步处理(异步通信)

应用解耦(平台无关)

流量削峰(秒杀,主要作用)

日志处理

给客户端快速响应

1.3 MQ使用场景

在项目中,将一些无需即时返回且耗时的操作提取出来,进行了异步处理,而这种异步处理的方式大大的节省了服务器的响应时间,从而提高了系统的吞吐量。比如这样的场景:用户在商城下单成功之后,给用户发送短信或邮件以感谢用户的购买。

1.4 MQ产品

ActiveMQ

老牌MQ,支持JMS规范

Kafka

高吞吐量,不支持事务,消息可靠性低,用于大数据领域

RocketMQ

借鉴于Kafka,支持高吞吐量、以及具备高可用的特点。但是高级功能是要收费的

RabbitMQ

使用Erlang语言开发,基于AMQP协议实现。可靠性高,性能低于Kafka

2. 安装RabbitMQ

以下安装环境为CentOS 7

2.1 安装Erlang

因为rabbitmq是使用erlang语言开发的,所以我们应该先安装erlang。erlang下载地址: https://www.erlang.org/downloads

1. RabbitMQ基础_erlang_02

将otp_src_23.3.tar.gz上传至linux

rz -y

如果提示命令无法识别,则下载一个命令,然后再上传

yum -y install lrzsz

安装erlang的依赖库

yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel unixODBC-devel

依赖库安装期间如果出现以下错误

Failed connect to mirrors.cloud.aliyuncs.com:80; Connection refused

就按这个方法解决,首先显示当前网络连接

nmcli connection show

然后修改当前网络连接对应的DNS服务器,这里的网络连接可以用名称或者UUID来标识

nmcli con mod ens33 ipv4.dns "114.114.114.114 8.8.8.8"

解压

tar zxvf otp_src_23.3.tar.gz

进入otp_src_23.3文件夹

cd otp_src_23.3

运行otp_src_23.3目录下的configure脚本,以指定erlang的安装目录,这里我们指定安装目录为/opt/erlang

./configure --prefix=/opt/erlang

安装erlang(该步骤花费时间较多)

make

make install

修改/etc/profile配置文件,在该文件的最下方添加下面的环境变量配置即可

ERLANG_HOME=/opt/erlang

export PATH=$PATH:$ERLANG_HOME/bin

export ERLANG_HOME

重新加载/etc/profile文件

source /etc/profile

验证erlang是否安装成功

erl

键入erl后,能看到以下内容,就表示erl已经安装成功!

1. RabbitMQ基础_java_03

2.2 安装RabbitMQ

RabbitMQ下载地址: https://github.com/rabbitmq/rabbitmq-server/releases

1. RabbitMQ基础_erlang_04

将下载好的rabbitmq-server-generic-unix-3.8.18.tar.xz传给linux

解压rabbitmq-server-generic-unix-3.8.18.tar.xz到/opt目录下

tar vxf rabbitmq-server-generic-unix-3.8.18.tar.xz -C /opt

为了方便,进入/opt目录,并修改rabbitmq_server-3.6.12文件夹的名字为rabbit

cd /opt

mv rabbitmq_server-3.8.18/ rabbitmq

修改/etc/profile文件,在最后面加上以下环境变量

export PATH=$PATH:/opt/rabbitmq/sbin

export RABBITMQ_HOME=/opt/rabbitmq

执行以下命令,让新的配置生效

source /etc/profile

修改 /etc/hosts,在最后一行加上 127.0.0.1 对应自己的 linux 系统的主机名

1. RabbitMQ基础_java_05

启动RabbitMQ服务

rabbitmq-server -detached

以上的 -detached 参数的作用是让RabbitMQ服务以守护进程的方式在后台运行,这样就不会因为当前窗口的关闭而关闭了服务

执行以下命令查看RabbitMQ服务是否启动成功

rabbitmqctl status

能看到以下内容,就说明RabbitMQ服务启动成功了

1. RabbitMQ基础_erlang_06

3. RabbitMQ控制台

3.1 创建账户

RabbitMQ有一个默认账户:guest,该账户只能在当前RabbitMQ主机上才能登录,所以我们要为RabbitMQ创建一个新的账户

rabbitmqctl add_user root root123

为root用户设置权限,以下设置的权限是最高权限,可配置,可读,可写,

rabbitmqctl set_permissions -p / root ".*" ".*" ".*"

为root用户设置角色

rabbitmqctl set_user_tags root administrator

3.2 启动控制台

启动RabbitMQ管理控制台

rabbitmq-plugins enable rabbitmq_management

开放服务器的5672、15672端口

firewall-cmd --add-port=5672/tcp --permanent

firewall-cmd --add-port=15672/tcp --permanent

firewall-cmd --reload

Docker安装RabbitMQ

拉取RabbitMQ时,要指定RabbitMQ的版本为management,因为该版本会自带web客户端

docker pull rabbitmq:management

docker run -dit --name mymq -e RABBITMQ_DEFAULT_USER=root \

-e RABBITMQ_DEFAULT_PASS=root -p 15672:15672 -p 5672:5672 \

rabbitmq:management

访问 ip地址:15672,账号密码就是上面创建的:root / root123

1. RabbitMQ基础_虚拟主机_07

1. RabbitMQ基础_虚拟主机_08

4. Hello World

让我们以RabbitMQ官方提供的Hello World,来开始RabbitMQ的旅程吧

 https://www.rabbitmq.com/getstarted.html

1. RabbitMQ基础_虚拟主机_09

4.1 引入依赖

<dependency>

<groupId>com.rabbitmq</groupId>

<artifactId>amqp-client</artifactId>

<version>5.7.3</version>

</dependency>

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>slf4j-simple</artifactId>

<version>1.7.25</version>

</dependency>

4.2 虚拟主机

4.2.1 虚拟主机的作用

虚拟主机的作用等同于数据库管理系统中的数据库,每一个项目都需要一个独立的数据库,同理,每一个项目都需要一个独立的RabbitMQ虚拟主机

4.2.2 创建虚拟主机

在RabbitMQ的web管理控制台中,有一个名为“/”的虚拟主机,我们就直接使用该虚拟主机作为Hello World例子中使用的虚拟主机

1. RabbitMQ基础_java_10

4.2.3 虚拟主机与用户

我们可以为某个虚拟主机绑定用户,被绑定到某个虚拟主机的用户才能操作这个虚拟主机。Hello World例子中,我们使用上面创建的root用户,因为root用户的是可以直接操作这个默认为"/"的虚拟主机的

4.3 生产者

package com.gao.a_hello;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2020/08/09 19:39:45

*/

public class Producer {

public static void main(String[] args) throws IOException, TimeoutException {

ConnectionFactory factory = new ConnectionFactory();

factory.setHost("192.168.1.51");

factory.setPort(5672);

factory.setVirtualHost("/");

factory.setUsername("root");

factory.setPassword("root123");

获取连接

Connection connection = factory.newConnection();

获取通道

Channel channel = connection.createChannel();

声明队列

参数1:队列名称,如果队列不存在,则创建队列,并与当前通道绑定,如果队列已经存在,则直接与当前通道绑定

参数2:是否持久化

参数3:是否排他

参数4:是否自动删除队列

参数5:附加参数

channel.queueDeclare("hello", false, false, false, null);

投递消息

参数1:交换机名称,这里使用的是默认交换机,默认与所有队列绑定

参数2:队列名称

参数3:消息的附加设置

参数4:消息的具体内容

channel.basicPublish("","hello", null, "hello world".getBytes());

channel.close();

connection.close();

}

}

执行以上程序,可以在Web管理界面中看到hello队列。

4.4 消费者

package com.gao.a_hello;

import com.rabbitmq.client.*;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2020/08/09 20:17:27

*/

public class Consumer {

public static void main(String[] args) throws IOException, TimeoutException {

ConnectionFactory factory = new ConnectionFactory();

factory.setHost("192.168.1.51");

factory.setPort(5672);

factory.setVirtualHost("/");

factory.setUsername("root");

factory.setPassword("root123");

Connection connection = factory.newConnection();

Channel channel = connection.createChannel();

让channel绑定队列,如果不存在则创建,如果已经存在则直接绑定

channel.queueDeclare("hello", false, false, false, null);

消费消息,注意,这里会消费掉队列中的所有消息(除非指定qos)

channel.basicConsume("hello", new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope; envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

System.out.println("recv: " + new String(body));

确认消费到了消息

channel.basicAck(envelope.getDeliveryTag(), false);

}

});

因为消费消息是异步的,所以这里故意暂停,防止消息还没有被消费就关闭了连接

System.out.println("Enter...");

System.in.read();

channel.close();

connection.close();

}

}

4.5 消费者确认

4.5.1 确认单个消息

在RabbitMQ服务把消息传递给消费者时,并不会立即把消息从服务端删掉,而是等待消费者确认消费了某个消息后,RabbitMQ服务才会把这个被消费者确认的消息从服务端删除掉!

这是通过deliveryTag来实现的,如下:

1. RabbitMQ基础_erlang_11

也可以由消费者自动确认,只需要在basicConsume方法中,多加一个参数即可,如下:

channel.basicConsume("hello", true, new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

System.out.println("recv: " + new String(body));

}

});

4.5.2 批量确认消息

修改Producer投递消息的代码

for (int i = 1; i <= 10; i++) {

投递消息

参数1:交换机名称,这里使用的是默认交换机,默认与所有队列绑定

参数2:队列名称

参数3:消息的附加设置

参数4:消息的具体内容

channel.basicPublish("", "hello", null, String.valueOf(i).getBytes());

}

修改Consumer消费消息的代码

// 消费消息,注意,这里会消费掉队列中的所有消息(除非指定qos)

channel.basicConsume("hello", new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

String msg = new String(body);

System.out.println("recv: " + msg);

if (msg.equals("10")) {

只有消费到的消息是10的时候,才确认消费到消息

测试时可以将第二个参数设置为 true 或 false,再观察队列中还剩几个消息

channel.basicAck(envelope.getDeliveryTag(), true);

}

}

});

4.6 参数

以下讲解:声明队列时,所使用到的参数。注意,重复声明同名队列的时候,如果参数与之前声明的不一致,则会报错!

4.6.1 durable

durable如果为true,则队列在Rabbitmq服务重启后还在,但是消息都没有了,因为消息本身没有开启持久化功能,要想让消息本身也是持久化的,只需在投递消息的时候,加上一个附加参数即可,如下:

channel.basicPublish("","hello", MessageProperties.PERSISTENT_TEXT_PLAIN,

"hello world".getBytes());

4.6.2 exclusive

exclusive如果为true,则表示当前队列只能绑定到同一个连接上(Connection),也就是说,只能绑定到同一个连接的多个通道上(Channel),但不能绑定到不同连接的通道上。且连接关闭后,队列也就被删除了。

4.6.3 autoDelete

exclusive = true

autoDelete = true

生产者 close

自动删除队列

不会自动删除队列

消费者 close

自动删除队列

自动删除队列

autoDelete设置为true的队列,只有所有与该队列连接的消费者都与此队列断开,才会删除该队列,无论该队列中有没有消息。

对比:exclusive设置为true的队列,只要生产者或消费者与队列断开,都会删除队列。

5. work queues

work queues,也被称为(Task queues),任务模型,当消息处理比较耗时的时候,生产消息的速度会远远大于消息的消费速度。这样会造成消息堆积越来越多的情况。此时就可以使用work queues模型:让多个消费者消费同一个队列。

1. RabbitMQ基础_虚拟主机_12

5.1 生产者

package com.gao.b_work_queues;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/05/05 17:57:04

*/

public class Producer {

public static void main(String[] args) throws IOException, TimeoutException {

ConnectionFactory factory = new ConnectionFactory();

factory.setHost("192.168.163.133");

factory.setPort(5672);

factory.setVirtualHost("/");

factory.setUsername("root");

factory.setPassword("root123");

Connection connection = factory.newConnection();

Channel channel = connection.createChannel();

channel.queueDeclare("queue", false, false, false, null);

for (int i = 1; i <= 20; i++) {

channel.basicPublish("", "queue", null, ("G.E.M" + i).getBytes());

}

channel.close();

connection.close();

}

}

5.2 消费者

消费者1

package com.gao.b_work_queues;

import com.rabbitmq.client.*;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/05/05 18:06:15

*/

public class Consumer {

public static void main(String[] args) throws IOException, TimeoutException {

ConnectionFactory factory = new ConnectionFactory();

factory.setHost("192.168.163.133");

factory.setPort(5672);

factory.setVirtualHost("/");

factory.setUsername("root");

factory.setPassword("root123");

Connection connection = factory.newConnection();

Channel channel = connection.createChannel();

channel.queueDeclare("queue", false, false, false, null);

channel.basicConsume("queue", new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

System.out.println("Consumer recv: " + new String(body));

channel.basicAck(envelope.getDeliveryTag(), false);

}

});

System.out.println("Enter...");

System.in.read();

channel.close();

connection.close();

}

}

消费者2

package com.gao.b_work_queues;

import com.rabbitmq.client.*;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/05/05 18:06:15

*/

public class Consumer2 {

public static void main(String[] args) throws IOException, TimeoutException {

ConnectionFactory factory = new ConnectionFactory();

factory.setHost("192.168.163.133");

factory.setPort(5672);

factory.setVirtualHost("/");

factory.setUsername("root");

factory.setPassword("root123");

Connection connection = factory.newConnection();

Channel channel = connection.createChannel();

channel.queueDeclare("queue", false, false, false, null);

channel.basicConsume("queue", new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

System.out.println("Consumer recv: " + new String(body));

channel.basicAck(envelope.getDeliveryTag(), false);

}

});

System.out.println("Enter...");

System.in.read();

channel.close();

connection.close();

}

}

测试时,应该先启动两个消费者,再启动生产者,会发现两个消费者消费到的消息数量是一样的。

1. RabbitMQ基础_erlang_13

1. RabbitMQ基础_虚拟主机_14

而如果我们故意将消费者2,消费消息的速度放慢,情况仍然是一样的,如下:

channel.basicConsume("queue", new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

System.out.println("Consumer recv: " + new String(body));

try {

Thread.sleep(2000);

} catch(Exception e) {

e.printStackTrace();

}

channel.basicAck(envelope.getDeliveryTag(), false);

}

});

这样的话,似乎有点不妥,不符合“能者多劳”的原则。为什么会这样呢,因为默认情况下,消费者能持有的“未被确认的消息”的个数是没有上限的,所以MQ服务可以平分消息给两个消费者。

为了做出“能者多劳”的效果,我们需要设置客户端能持有的“未被确认的消息”的个数!这样,当MQ服务把消息发送给一个消费者,而这个消费者确认消息的速度很慢的话,该消费者很快就会持有“未被确认的消息”的数量到最大限制的值,此时MQ服务就只能把消息发送给其他消费者了。

我们可以通过channel.basicQos()方法来设置消费者可以持有的“未被确认的消息”的最大值,修改两个消费者的代码如下:

消费者1

channel.basicQos(1);

channel.basicConsume("queue", new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

System.out.println("Consumer recv: " + new String(body));

channel.basicAck(envelope.getDeliveryTag(), false);

}

});

消费者2

channel.basicQos(1);

channel.basicConsume("queue", new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

System.out.println("Consumer recv: " + new String(body));

try {

Thread.sleep(2000);

} catch(Exception e) {

e.printStackTrace();

}

channel.basicAck(envelope.getDeliveryTag(), false);

}

});

生产者再次投递10个消息后,消费情况如下:

1. RabbitMQ基础_虚拟主机_15

1. RabbitMQ基础_erlang_16

6. 交换机

在hello_world和work_queues这两个例子中,我们绕过了一个在RabbitMQ中很重要的概念:交换机。现在我们将学习交换机在RabbitMQ中是如何使用的。

6.1 交换机

实际上,在RabbitMQ中,生产者是不会直接把消息发送给一个队列的。生产者会把一个消息发送给一个交换机。交换机的功能非常简单:1. 接收生产者的消息,2. 把消息真正地存入队列中。

下图是生产者投递消息的模型图。当生产者投递一个消息给MQ服务时,其实一共发了三部分数据:交换机名、路由键、消息体。交换机根据接收到的路由键,来决定把消息最终放入哪个队列中。

1. RabbitMQ基础_java_17

提取信息:

1. 生产者并不是把消息直接投递到队列中的,而是先投递给交换机

2. 一个交换机可以绑定多个队列(相对地,一个队列也可以绑定多个交换机)

3. 队列与交换机绑定,是需要一个“绑定键”的

4. 当一个消息被投递给交换机时,MQ会提取出消息中的路由键,与当前交换机的绑定键进行匹配,如果匹配成功,则消息进入对应的队列。匹配失败,消息就丢失。

5. 进入队列的只有消息体,交换机名 和 路由键 不会进入队列! 交换机 + 路由键 的价值就是找队列,一旦根据交换机和路由键找到队列了,那么交换机和路由键也就“尽忠”了。

6.2 默认交换机

在RabbitMQ中有一个默认交换机,这个默认交换机在安装好RabbitMQ服务之后就直接存在了。该默认交换机的名字为一个空的字符串:""。

每当我们在RabbitMQ服务中声明一个队列时,这个队列就会直接绑定到默认交换机上。绑定键就是队列名本身。比如,在hello_world例子中,我们声明了一个名字叫做“hello”的队列,这个队列就直接绑定到了默认交换机上。我们可以通过web控制台看到这一点。

首先点击hello队列,以查看hello队列的详细信息:

1. RabbitMQ基础_erlang_18

然后再查看队列的绑定信息:

1. RabbitMQ基础_java_19

可以发现,当前队列(hello队列)确实已经绑定到了默认交换机。

我们再从默认交换机的角度来看看这个绑定信息,先点击Exchanges,然后:

1. RabbitMQ基础_erlang_20

可以看到:

The default exchange is implicitly bound to every queue, with a routing key equal to the queue name. It is not possible to explicitly bind to, or unbind from the default exchange. It also cannot be deleted.

所以,默认交换机的模型看起来是这个样子:

1. RabbitMQ基础_erlang_21

再次查看hello_world例子中,生产者投递消息的代码:

channel.basicPublish("","hello", null, "hello world".getBytes());

其中,""就是默认交换机的名字,"hello"就是路由键,"hello world"就是消息体。

6.3 交换机的类型

交换机的类型有3种:

1. direct

2. topic

3. fanout

交换机默认是direct类型的。我们也可以在自定义交换机时,指定交换机的类型。

7. Publish/Subscribe

生产者发送的消息,只要发送给了fanout类型的交换机,该交换机就会把消息发送给每一个绑定到它的队列,从而实现广播的效果。

1. RabbitMQ基础_java_22

利用fanout类型的交换机,可以轻松实现以上模型

7.1 工具类

从这个例子开始,我们使用RabbitMQUtils来封装获取连接的逻辑。注意,这里将工具类放在了z包。

package com.gao.z_utils;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/06/30 16:31:51

*/

public class RabbitMQUtils {

private static final String HOST = "192.168.188.128";

private static final int PORT = 5672;

private static final String USERNAME = "root";

private static final String PASSWORD = "root123";

private static final String VIRTUAL_HOST = "/";

public static Connection newConnection() throws IOException, TimeoutException {

ConnectionFactory factory = new ConnectionFactory();

factory.setHost(HOST);

factory.setPort(PORT);

factory.setUsername(USERNAME);

factory.setPassword(PASSWORD);

factory.setVirtualHost(VIRTUAL_HOST);

return factory.newConnection();

}

public static void release(Channel channel, Connection connection) throws IOException, TimeoutException {

channel.close();

connection.close();

}

}

7.1 生产者

package com.gao.c_publish_subscribe;

import com.gao.z_utils.RabbitMQUtils;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/06/30 16:38:47

*/

public class Producer {

public static void main(String[] args) throws IOException, TimeoutException {

Connection connection = RabbitMQUtils.newConnection();

Channel channel = connection.createChannel();

参数1:交换机名称

参数2:交换机类型

channel.exchangeDeclare("logs", "fanout");

发送消息

for (int i = 0; i < 10; i++) {

channel.basicPublish("logs", "", null, ("Andy" + i).getBytes());

}

释放资源

RabbitMQUtils.release(channel, connection);

}

}

7.2 消费者

消费者1

package com.gao.c_publish_subscribe;

import com.gao.z_utils.RabbitMQUtils;

import com.rabbitmq.client.*;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/06/30 18:05:16

*/

public class Consumer {

public static void main(String[] args) throws IOException, TimeoutException {

Connection connection = RabbitMQUtils.newConnection();

Channel channel = connection.createChannel();

channel.exchangeDeclare("logs", "fanout");

声明一个临时队列,该临时队列的属性是:

非持久化

排他

自动删除

String queueName = channel.queueDeclare().getQueue();

channel.queueBind(queueName, "logs", "");

channel.basicConsume(queueName, true, new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

String msg = new String(body);

System.out.println("A revc: " + msg);

}

});

System.out.println("Enter...");

System.in.read();

RabbitMQUtils.release(channel, connection);

}

}

消费者2

package com.gao.c_publish_subscribe;

import com.gao.z_utils.RabbitMQUtils;

import com.rabbitmq.client.*;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/06/30 18:05:16

*/

public class Consumer2 {

public static void main(String[] args) throws IOException, TimeoutException {

Connection connection = RabbitMQUtils.newConnection();

Channel channel = connection.createChannel();

channel.exchangeDeclare("logs", "fanout");

声明一个临时队列,该临时队列的属性是:

非持久化

排他

自动删除

String queueName = channel.queueDeclare().getQueue();

channel.queueBind(queueName, "logs", "");

channel.basicConsume(queueName, true, new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

String msg = new String(body);

System.out.println("B revc: " + msg);

}

});

System.out.println("Enter...");

System.in.read();

RabbitMQUtils.release(channel, connection);

}

}

当生产者投递10个消息后,两个消费者都会接收到所有消息:

1. RabbitMQ基础_虚拟主机_23

1. RabbitMQ基础_java_24

7.3 临时队列

在hello_world和work queues例子中,我们为队列指定了名字。这是为了让多个消费者可以根据队列名从同一个任务队列中获取任务,从而达到分工的效果。

而对于当前的日志例子而言,我们只需要做到:当消费者连接到MQ服务时,fanout就开始推送日志信息给该消费者,当消费者从MQ服务断开连接时,MQ就不再推送日志信息给消费者了。就像收音机接收电台信号那样,连接上就收信号,断开就不收信号。

为了做到这个效果,我们需要保证以下2点:

  1. 一旦消费者连接到MQ,就随之创建一个新的、空的队列来接收fanout交换机发来的日志
  2. 一旦消费者断开与MQ的连接,就删除这个队列

所以我们采用了临时队列:

String queueName = channel.queueDeclare().getQueue();

当消费者连接上MQ服务时,就创建一个临时队列,并让这个临时队列与fanout交换机绑定,当消费者从MQ断开时,临时队列也就被自动删除了。fanout交换机也就不必继续给一个不存在的队列发送消息了。

8. Routing

在上个例子中,我们创建了一个简单的日志系统,我们可以把日志广播给多个消费者。在本例中,我们将有针对性地发送日志消息,比如,我们把所有日志发送给另外一个消费者的同时,再把错误消息单独发送给某一个专门特殊处理错误日志(存盘)的消费者。模型如下:

1. RabbitMQ基础_java_25

8.1 绑定

绑定是交换机与队列之间的关系。为了将一个队列绑定到交换机上,我们需要一个绑定键。如上图中的error、info、warning,都是绑定键。以下就是创建绑定键的api:

channel.queueBind(queueName, EXCHANGE_NAME, "black");

另外,绑定键如何发挥作用,还与交换机的类型有关。比如fanout类型的交换机会直接忽略绑定键,因为fanout类型的交换机要广播消息给每一个与他绑定的队列。而这本例中要使用的direct类型的交换机,则会拿着路由键与绑定键进行等值比较,比较相等的情况下,就会将消息投递给队列。

8.2 生产者

package com.gao.d_routing;

import com.gao.z_utils.RabbitMQUtils;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import java.io.IOException;

import java.util.Random;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/06/30 16:38:47

*/

public class Producer {

public static void main(String[] args) throws IOException, TimeoutException {

Connection connection = RabbitMQUtils.newConnection();

Channel channel = connection.createChannel();

参数1:交换机名称

参数2:交换机类型

channel.exchangeDeclare("direct_logs", "direct");

发送消息

for (int i = 0; i < 10; i++) {

String severity = getSeverity();

String msg = "Eason" + i;

级别:" + severity + ", 内容:" + msg);

channel.basicPublish("direct_logs", severity, null, msg.getBytes());

}

释放资源

RabbitMQUtils.release(channel, connection);

}

private static String getSeverity() {

int r = new Random().nextInt(3);

String severity = null;

switch (r) {

case 0:

severity = "info";

break;

case 1:

severity = "warning";

break;

case 2:

severity = "error";

break;

default:

}

return severity;

}

}

8.2 消费者

消费者1:处理错误日志

package com.gao.d_routing;

import com.gao.z_utils.RabbitMQUtils;

import com.rabbitmq.client.*;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/06/30 18:05:16

*/

public class Consumer {

public static void main(String[] args) throws IOException, TimeoutException {

Connection connection = RabbitMQUtils.newConnection();

Channel channel = connection.createChannel();

channel.exchangeDeclare("direct_logs", "direct");

声明一个临时队列,该临时队列的属性是:

非持久化

排他

自动删除

String queueName = channel.queueDeclare().getQueue();

channel.queueBind(queueName, "direct_logs", "error");

channel.basicConsume(queueName, true, new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

String msg = new String(body);

处理错误日志: " + msg);

}

});

System.out.println("Enter...");

System.in.read();

RabbitMQUtils.release(channel, connection);

}

}

消费者2: 处理所有日志

package com.gao.d_routing;

import com.gao.z_utils.RabbitMQUtils;

import com.rabbitmq.client.*;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/06/30 18:05:16

*/

public class Consumer2 {

public static void main(String[] args) throws IOException, TimeoutException {

Connection connection = RabbitMQUtils.newConnection();

Channel channel = connection.createChannel();

channel.exchangeDeclare("direct_logs", "direct");

声明一个临时队列,该临时队列的属性是:

非持久化

排他

自动删除

String queueName = channel.queueDeclare().getQueue();

channel.queueBind(queueName, "direct_logs", "info");

channel.queueBind(queueName, "direct_logs", "warning");

channel.queueBind(queueName, "direct_logs", "error");

channel.basicConsume(queueName, true, new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

String msg = new String(body);

接收所有日志: " + msg);

}

});

System.out.println("Enter...");

System.in.read();

RabbitMQUtils.release(channel, connection);

}

}

生产者,生产了10条不同级别的日志

1. RabbitMQ基础_erlang_26

消费者1只处理error日志

1. RabbitMQ基础_erlang_27

消费者2处理所有日志

1. RabbitMQ基础_java_28

注意,如果路由键与绑定键都不匹配,则消息不会入队列。

9. Topic

类型的交换机足以应对大部分需求了,但仍然有其短板。如果我们想只记录来自A生产者的error日志信息,而不想记录来自A生产者的info和warning日志信息;同时想记录来自B生产者的所有日志信息的话,direct是无法满足这个需求的。因为direct类型的交换机无法区分一个info类型的日志信息究竟是来自A生产者还是来自B生产者。

为了实现像这样的类似的需求,我们需要学习稍微复杂一点的Topic类型的交换机,模型如下,其中 * 匹配一个单词, # 匹配零到多个单词:

1. RabbitMQ基础_erlang_29

在这个例子中,我们准备发送所有关于动物的消息,这些消息的路由键会由3部分组成:

<speed>.<color>.<species>

模型中有3个绑定键,这些绑定键可以这么描述:

1. Q1队列对所有颜色为orange的动物感兴趣

2. Q2队列对所有兔子感兴趣,同时也对所有lazy animals感兴趣

生产者投递消息时,附带的路由键如下,分析以下消息会投递到哪个队列

1. quick.orange.rabbit

2. lazy.orange.elephant

3. quick.orange.fox

4. lazy.brown.fox

5. lazy.pink.rabbit

6. quick.brown.fox

如果我们打破路由键由3部分组成的规则呢?

1. orange

2. quick.orange.male.rabbit

3. lazy.orange.male.rabbit

前俩个消息不会匹配任何绑定,并且会丢失这些消息。第三个消息会进入Q2

9.1 生产者

package com.gao.e_topic;

import com.gao.z_utils.RabbitMQUtils;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import java.io.IOException;

import java.util.Random;

import java.util.Scanner;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2021/06/30 16:38:47

*/

public class Producer {

public static void main(String[] args) throws IOException, TimeoutException {

try (Connection connection = RabbitMQUtils.newConnection();

Channel channel = connection.createChannel()) {

参数1:交换机名称

参数2:交换机类型

channel.exchangeDeclare("topic_logs", "topic");

发送消息

Scanner in = new Scanner(System.in);

请输入宠物昵称:");

String msg = in.nextLine();

请描述您的宠物:<speed>.<color>.<species>");

String routingKey = in.nextLine();

channel.basicPublish("topic_logs", routingKey, null, msg.getBytes());

}

}

}

9.2 消费者

消费者1

package com.gao.e_topic;

import com.gao.z_utils.RabbitMQUtils;

import com.rabbitmq.client.*;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2022/01/10 17:28:14

*/

public class Consumer {

public static void main(String[] args) throws IOException, TimeoutException {

try (Connection connection = RabbitMQUtils.newConnection();

Channel channel = connection.createChannel()) {

channel.exchangeDeclare("topic_logs", "topic");

channel.queueDeclare("Q1", false, false, false, null);

channel.queueBind("Q1", "topic_logs", "*.orange.*");

channel.basicConsume("Q1", true, new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

System.out.println("A recv: " + new String(body));

}

});

System.out.println("Enter...");

System.in.read();

}

}

}

消费者2

package com.gao.e_topic;

import com.gao.z_utils.RabbitMQUtils;

import com.rabbitmq.client.*;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

/**

* @author gao

* @time 2022/01/10 17:28:14

*/

public class Consumer2 {

public static void main(String[] args) throws IOException, TimeoutException {

try (Connection connection = RabbitMQUtils.newConnection();

Channel channel = connection.createChannel()) {

channel.exchangeDeclare("topic_logs", "topic");

channel.queueDeclare("Q2", false, false, false, null);

channel.queueBind("Q2", "topic_logs", "*.*.rabbit");

channel.queueBind("Q2", "topic_logs", "lazy.#");

channel.basicConsume("Q2", true, new DefaultConsumer(channel) {

@Override

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

System.out.println("B recv: " + new String(body));

}

});

System.out.println("Enter...");

System.in.read();

}

}

}

Topic类型的交换机是非常强大的,它可以表现得和其他交换机一样:

☐ 当一个队列使用 # 作为绑定键,该队列就会接收所有消息,无论消息附带的路由键是什么。就像fanout类型的交换机那样。

☐ 当绑定键中没有使用