写在前面
为什么选择RabbitMQ?
市场上消息队列的组件有很多,ActiveMQ、RabbitMQ、RocketMQ、kafka等。这里我只是选择其中一种消息队列进行学习,毕竟菜鸡要长得肥一点才好吃。如果你也在犹豫到底我要选择哪一个?可以看看这个
为什么用Docker?
因为作为菜鸡,用惯了Windows的可视化界面,对Linux的生态体系真的很陌生。直接将MQ安装到物理机上,不免有些为难,可能会耗费太久的时间。而Docker对我这种小白就很友好,将环境和软件一起打包,开箱即用,非常方便,它不需要我懂太多,只要傻瓜式的下载启用就好。当然如果安装到本机上可能会让自己对Linux有更加深入的认识,也对Rabbit MQ有一定的认识。 凡事没有对错,在于自己怎么取舍 。怎样都能学到东西。
RabbitMQ安装
镜像检索以及下载
- 使用docker的搜索命令
docker search rabbitmq
在docker仓库中搜索RabbitMQ ,会搜索到如下结果: - 我们选择第一个official的官方版本下载使用,不加后面的版本号就会下载最新的镜像
- 下载命令:
docker pull rabbitmq:3.7.15
- 查看我们下载的镜像:
docker images
启动RabbitMQ
启动命令:
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 -v `pwd`/data:/var/lib/rabbitmq --hostname myRabbit -e RABBITMQ_DEFAULT_VHOST=my_vhost -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin 这里放你的MQimageId
参数解释:
- -d 表示的是后台运行容器
- --name 给容器指定名字。这个是给自己看的,docker ps的时候展示的名字
- 第一个 -p 指定的是服务运行的端口。(提供服务的端口)
- 第二个 -p 指定的是web管理端的端口。接下来会看到管理端的界面
- -v 映射目录或者文件。这里是把你当前所在目录下的data文件夹映射到容器的
/var/lib/rabbitmq
- --hostname 指定主机名称。该名称在集群的名称中使用。
- -e 指定环境变量
- RABBITMQ_DEFAULT_VHOST:默认虚拟机名
- RABBITMQ_DEFAULT_USER:默认的用户名
- RABBITMQ_DEFAULT_PASS:默认用户名的密码
注意:
- 服务端口与web端口指定的顺序不重要,前后颠倒也是可以的。因为docker内部RabbitMQ的端口是固定的
- 端口的指定格式: 主机(宿主)端口:容器端口
- -v命令的指定格式:主机目录:容器目录
- 命令中的`pwd`相当于是一个变量取值的操作,与$的能力类似,但是用途不太一样。其实就是执行了pwd的命令,然后把结果替换掉`pwd`。如果我说的不明白 可以看看这里
- 最好是确保你的主机目录下有你指定的映射目录、容器中的无所谓,会自动创建
- RabbitMQ的hostname 最好指定。Rabbit MQ的有一个重要的注意事项是它根据节点名称来存储数据,默认就是主机名、而上面说道hostname就是集群中的节点名称。如果不指定hostname,那么docker在启动的时候会自动生成hostname,如此一来可能每次生成的hostname都不一样,那么就会导致数据丢失。
- 第二次启动RabbitMQ使用命令
docker start (containerId或者第一次启动时--name指定的名字)
即可 - docker run的命令 看看这里
查看是否启动成功
使用命令docker ps
查看一下是否启动成功了,可以看到我们指定的名字 rabbitmq 已经起来了
当然也有一种可能,你发现列出来的镜像是没有我们启动时用--name
指定的rabbitmq
,那我们们要先来查看一下刚才创建的rabbitmq
。使用命令docker ps -a
来查看所有创建的容器。这个里面一定有我们刚才创建的rabbitmq
只是启动失败了,那我们复制一下他的CONTAINER ID
然后使用命令:docker logs 复制的CONTAINER ID
来查看时什么问题。
从图中可以看到,是权限问题,怎么办呢?可以看看这里。我选择在启动的时候增加一条命令:--privileged=true
来给容器加上特定权限。那要实现这个,就要先把之前创建失败的删掉,使用命令:docker rm 刚复制的CONTAINER ID
删掉启动失败的container,然后使用更改之后的启动命令来启动:
sudo docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 -v pwd/data:/var/lib/rabbitmq --hostname myRabbit --privileged=true -e RABBITMQ_DEFAULT_VHOST=my_vhost -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin b3639fca0afd
再次使用docker ps
来查看container,发现这次启动起来了。接下来我们就可以愉快的玩耍了。
阳光温热,岁月静好,你还不来,我怎敢老去。——张爱玲
RabbitMQ的管理界面
访问RabbitMQ的管理端界面
我的是无可视化界面的centos7所以使用ip addr
查看一下虚拟机的IP地址:
接下来在自己的电脑上,访问虚拟机上docker部署的RabbitMQ。访问地址:http://192.168.50.254:15672/
这个时候可能会访问不到,比较尴尬,那试试下面的操作:
- 检查一下自己的虚拟机防火墙是不是关闭了,使用命令
systemctl stop firewalld.service
关闭防火墙 - 检查一下是不是打开了MQ的管理界面。使用命令
docker exec -it rabbitmq /bin/bash
进入docker容器内部,然后执行命令rabbitmq-plugins enable rabbitmq_management
打开RabbitMQ的管理界面插件,可以看到如下结果,代表插件启动成功了 - 当然了这样其实有点麻烦,启动完了还得进来手动启动管理界面,你还可以这样做,在启动的时候最后不指定RabbitMQ的ImageID而是用 rabbitmq:management 来代替,指定了安装rabbitmq的Web管理插件版本,这样就不用再进入容器内部开启插件了。(但是需要提醒的是: 因为上面我下载的不是rabbitmq:management 所以在执行下面的命令的时候,他会重新去仓库拉取rabbitmq:management 版本的镜像)那么启动命令就变成了下面这样:
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 -v `pwd`/data:/var/lib/rabbitmq --hostname myRabbit -e RABBITMQ_DEFAULT_VHOST=my_vhost -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin rabbitmq:management
那现在再次在自己的电脑上刷新页面,发现已经可以访问到管理端页面了(最好不要用微软低版本的Edge访问,我搞了一下午,一直访问不到,后来换了Google就成功访问到了,回来用Edge依然是不行,谁能告诉我为什么?):
输入我们启动时指定的用户名和密码(admin/admin)进行登录,进入管理界面。如果启动时没有指定用户名和密码,使用默认账户密码guest/guest登陆。
接下来要做的是创建一个用户,然后创建一个Virtual Hosts,最后把用户和Virtual Hosts关联起来,程序中就可以使用了。当然如果说你想直接使用我们启动时指定的用户名和密码 admin/admin 以及 my_vhost 那就不用做下面的操作了,因为docker启动的时候已经帮我们把他们关联起来了。添加一个用户
添加一个Virtual Hosts
为mquser指定Virtual Hosts
返回Users那一栏,点击刚刚创建的用户名mquser,看到如下界面:选择创建的mq_virtual_host
好了下面就进入紧张刺激的代码测试阶段了。
我看过大海、也可过繁星。但唯独遇见你,是我长久以来所有奔赴的意义。
RabbitMQ简单使用
JavaClient的简单使用
使用之前需要先去maven仓库下载RabbitMQ Javaclient,访问maven官网 搜索RabbitMQ Java Client 点进去以后选择一个版本的客户端,这里我选择的是3.6.5 我使用的是gradle来管理jar包,所以复制以下代码到我的gradle文件compile group: 'com.rabbitmq', name: 'amqp-client', version: '3.6.5'
下面正式进入敲代码阶段:
单生产者-多消费者
首先创建一个类RabbitMQClientTest
在这个类中测试一下单生产者、多消费者的情况。单生产单消费的情况就不多说了,这里写的单生产多消费的情况,就只是启动两个线程去消费,实际上和单消费的代码是一样的。好,我们直接看代码:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class RabbitMQClientTest {
private static final Logger logger = LoggerFactory.getLogger(RabbitMQClientTest.class)
private static final String QUEUE_NAME = "queue_name";
/**
* 全局连接、生产消费都用它
*/
private static Connection connection;
static {
// 获取连接
getConnection();
}
public static Connection getConnection(){
if (connection != null){
return connection;
}
/**
* 连接工厂
*/
ConnectionFactory factory = new ConnectionFactory();
//定义连接--
factory.setHost("192.168.50.254");
//端口、在启动docker时指定的端口
factory.setPort(5672);
//定义vhost信息
factory.setVirtualHost("mq_virtual_host");
factory.setUsername("mquser");
factory.setPassword("mquser");
try {
connection = factory.newConnection();
} catch (Exception e) {
logger.info("----获取连接出错了:{}----", e.getMessage(), e);
}
return connection;
}
/**
* 发送多条消息
*/
private static void publishMultipleMessage() {
if (connection == null) {
throw new RuntimeException("连接信息为空了");
}
try {
//创建连接
Channel channel = connection.createChannel();
//声明创建队列【参数说明:参数一:队列名称,参数二:是否持久化;参数三:是否独占模式;参数四:消费者断开连接时是否删除队列;参数五:消息其他参数】
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//循环发送消息
for (int i = 0; i < 100; i++) {
channel.basicPublish("", QUEUE_NAME, null, ("multiple Message:" + i).getBytes());
Thread.sleep(i);
}
//不要忘记关闭通道哦
channel.close();
} catch (IOException | InterruptedException e) {
logger.info("-----发送消息出错了:{}--", e.getMessage(), e);
} catch (TimeoutException e) {
e.printStackTrace();
}
}
static class ConsumeThread extends Thread {
@Override
public void run() {
if (connection == null) {
throw new RuntimeException("连接为空了");
}
try {
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
QueueingConsumer consumer = new QueueingConsumer(channel);
/**
* 第二个参数:
* auto ACK: 消息消费的模式,自动确认和手动确认
* 自动确认:消费者从消息队列获取消息后,服务端就认为该消息已经成功消费。
* 手动确认:消费者成功消费后需要手动将状态返回到服务端
* 手动确认代码:channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
*/
channel.basicConsume(QUEUE_NAME, true, consumer);
/**
* 这里指的是,在消费者接收到消息之后,服务器在没有收到ACK确认的时候,该消息不会被队列删除,还是存在于队列当中。
* 并且服务器不会发送新的消息给该消费者,因为这里参数设置为1,
* 如果设置为3,则允许同时发送三个消息给消费者
*/
channel.basicQos(1);
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
logger.info("--线程:{}--消费消息:{}--",this.getName(), new String(delivery.getBody()), StandardCharsets.UTF_8);
Thread.sleep(2000);
}
} catch (IOException | InterruptedException e) {
logger.info("------报错了---:{}--",e.getMessage(), e);
}
}
}
//消费多条消息
private static void consumeMultipleMessage() {
for (int i= 0;i<3 ;i++){
ConsumeThread thread = new ConsumeThread();
thread.setName("thread" + i);
// 这里别写错了,不要写成thread.run();否则就不是线程了、不受操作系统调度,就只是个普通的方法调用
thread.start();
}
while (true){
publishMultipleMessage();
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
public static void main(String[] args) throws IOException {
try {
//单生产者,多消费者
consumeMultipleMessage();
do {
//保证主线程一直在。否则连接就被关闭了。
Thread.sleep(4000);
} while (true);
} catch (InterruptedException e) {
logger.info("-----中断出错了---");
e.printStackTrace();
} finally {
//关闭连接
if (connection != null) {
connection.close();
}
}
}
}
上面的消费者线程ConsumeThread中的channel.basicQos(prefetchCount)
一般是与channel.basicAck(long deliveryTag, boolean multiple)
方法一起使用,因为上面说了,只有服务器收到确认之后才会给这个消费者新的消息。所以要有一个确认的步骤,当然也可以自动确认channel.basicConsume(QUEUE_NAME, true, consumer);
我这里就是自动确认的。执行效果如下:
上面的代码中、只是简单的测试了一下 单个生产者、多消费者 的情况,没有做过多的设置,算是个基础款,实现了最基本的消息传递,但是细心地你一定发现上面的代码中,好多地方传参是null
和空串""
,没有涉及到一些专业术语、比如 交换器(Exchange)、绑定(Binding)、队列(Queue)、路由键(RoutingKey) 等的设置 。那下面我们就在使用的过程中,了解一下这几个东西,梳理一下他们之间的关系。
一花一世界,一叶一追寻。一曲一场叹,一生为一人
在开始之前,我们先来看一个简易的内部图,了解一下整体的流程结构:
从图中可以看到,生产者生产了消息之后,先发送消息到交换器Exchange上,然后Exchange通过Binding将消息发送到相应的队列Queue上,最后队列中的消息通过通道Channel发送给消费者。从图中可以看到,所有的通道Channel是共用的一个TCP连接,因为我们知道创建、关闭HTTP连接是一个非常耗时的操作。三次握手的相识、依依不舍的四次挥手别离。所以抽象出信道这样的虚拟的连接,这样就节省掉了频繁创建和关闭连接的资源消耗。不知道大家有没有设置过路由器,有一些路由器在设置的过程中就会有选择信道的步骤,选择一个好的信道就会让你的网络稳定流畅且快速,以前不知道信道是什么,现在看了MQ的信道,突然明白了。
有一个大体的认识之后,我们下面来看看交换器Exchange
交换器Exchange
交换器有四种,我们可以在MQ的管理端依次点击:Exchanges---->Add a new Exchange 我们可以看到type有四种分别是:
- headers
- direct (默认)
- fanout
- topic
我们来具体的看一下这几个类型的交换器,看看他们有什么区别?
Fanout订阅模式
从这个名字fanout就可以看出,有一种广播的意思在里面,实际上也就是这样,当生产者发送一条消息的时候,首先消息进入到Exchange里面,然后Fanout类型的交换器会把消息广播到所有绑定到这个 Exchange 上的队列里。相当于说,在清晨的时候,送报纸的人,会给所有订购了报纸的人送上一份最新的报纸。 Fanout 类型转发消息是最快的。(听别人说的,大概是因为不处理路由键routingKey?)。
那我们什么时候会使用这种交换器呢?
比如说对于用户下单这件事,下单的时候,我们要做什么呢?生成订单主信息、扣减库存、核销优惠券、给店长发送备货通知等等。现在我想在下单完成之后,再给用户发送一条短信,那么就可以在不改变原来代码的基础上,增加一条消息订阅就好了。实现了代码的解耦,同时我们把一些操作转移到消息队列当中来做,其实也是相当于异步处理了,那么可以大幅度提升下单的响应速度。
空说可能有点懵,我们还是来看看代码吧:
还是老样子,第一步建立连接:(下面代码中使用的Connection都是这个)
private static Connection connection;
private static final String EXCHANGE_FANOUT = "exchange_fanout";
private static final String EXCHANGE_DIRECT = "exchange_direct";
private static final String EXCHANGE_TOPIC = "exchange_topic";
static {
ConnectionFactory factory = new ConnectionFactory();
//定义连接
factory.setHost("192.168.182.167");
factory.setPort(5672);
//定义vhost信息
factory.setVirtualHost("mq_virtual_host");
factory.setUsername("mquser");
factory.setPassword("mquser");
try {
connection = factory.newConnection();
} catch (Exception e) {
logger.info("----获取连接出错了:{}----", e.getMessage(), e);
}
}
主流程,发布和消费消息的方法,这个方法会在main方法中调用:
/**
* 订阅模式测试
*/
private static void exchangeFanOutTest() throws Exception {
//启动消费者
fanoutConsumer();
//持续发布消息
while (true) {
fanoutProducer();
Thread.sleep(6000);
}
}
Fanout生产者代码:
private static final String EXCHANGE_FANOUT = "exchange_fanout";
/**
* 订阅模式生产者
*/
private static void fanoutProducer() throws IOException, TimeoutException {
Channel channel = connection.createChannel();
//定义交换器--可以发现这里是声明交换器
channel.exchangeDeclare(EXCHANGE_FANOUT, "fanout");
//发送消息
for (int i = 0; i < 50; i++) {
channel.basicPublish(EXCHANGE_FANOUT, "", null, ("fanoutProducer" + i).getBytes());
}
//关闭通道
channel.close();
}
Fanout 消费者代码:
private static final String EXCHANGE_FANOUT = "exchange_fanout";
/**
* 订阅模式消费者
*/
private static void fanoutConsumer() {
class FanoutThread extends Thread {
private String currentThread = this.getName();
private String queueName = "fanout_queue_" + currentThread;
@Override
public void run() {
try {
call();
} catch (Exception e) {
e.printStackTrace();
}
}
private void call() throws Exception {
Channel channel = connection.createChannel();
//声明队列----参数说明:参数一:队列名称,参数二:是否持久化;
//参数三:是否独占模式;参数四:消费者断开连接时是否删除队列;参数五:消息其他参数
channel.queueDeclare(queueName, false, false, false, null);
//绑定队列到交换器上
channel.queueBind(queueName, EXCHANGE_FANOUT, "");
//每次取得消息数
channel.basicQos(1);
//声明Consumer
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
logger.info("------准备消费了:{}", System.currentTimeMillis());
//消费消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
logger.info("--线程:{}--消费消息:{}", currentThread, new String(delivery.getBody()));
logger.info("");
Thread.sleep(2000);
}
}
}
for (int i = 0; i < 2; i++) {
new FanoutThread().start();
}
}
主要的main函数就不在这里贴出了,只是调用一下这个exchangeFanOutTest
方法,并且保证主线程一直在运行。可以看到启动了两个线程,分别命名为不同的队列(fanout_queue_+当前线程名),但都绑定到了相同的交换器Exchange上,所以两个线程都可以消费。
突然想到一个问题:是两个收到一样的,还是不一样的?(也就是说两个线程消费的是相同的消息吗?)按道理来说两个队列收到相同的消息,我们来看一下结果,可以看到就是猜想的那样,收到的都是相同的消息
我梦江南好,征辽亦偶然。你携万水赴千山,不料浮生总是飘摇聚散。但求青山永在,不负伊人蹒跚。
Direct 路由模式
Direct 模式匹配的规则比较简单,他是一种完全匹配。只有路由键RoutingKey
和绑定BindingKey
完全匹配的时候,才会收到消息。
那一般什么时候使用呢? 通常在我们需要将消息发给唯一一个节点时使用这种模式。接下来还是来看一下代码,还是主线程保持开启状态,一个线程定时发布消息,另外启动线程来消费消息。
路由模式测试方法
/**
* 路由模式测试
*/
private static void exchangeDirectTest() throws Exception {
//启动消费者
directConsumer();
//持续发布消息
while (true) {
directProcucer();
Thread.sleep(6000);
}
}
路由模式生产者
/**
* Direct(路由)模式生产者
* 消息完全匹配,这里就要用到routingKey和bindingKey
* 当这两个对上的时候才能收到消息
*/
private static void directProcucer() throws IOException, TimeoutException {
if (connection == null) {
throw new RuntimeException("链接信息为空");
}
logger.info("准备生产了");
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_DIRECT, "direct");
String routingKey = "correct";
for (int i = 0; i < 50; i++) {
channel.basicPublish(EXCHANGE_DIRECT, routingKey, null, ("directMessage:" + i).getBytes());
}
channel.close();
logger.info("生产结束");
}
路由模式消费者:
/**
* Direct(路由)模式消费者
*/
private static void directConsumer() {
class ConsumerThread extends Thread {
//routingKey默认是correct
private String routingKey;
private ConsumerThread(String routingKey) {
this.routingKey = routingKey;
}
@Override
public void run() {
if (connection == null) {
throw new RuntimeException("链接信息为空");
}
Channel channel = null;
try {
channel = connection.createChannel();
String queueName = "direct_queue_" + this.getName();
channel.queueDeclare(queueName, false, false, false, null);
//将队列绑定到交换器上
channel.queueBind(queueName, EXCHANGE_DIRECT, routingKey);
//允许同时发送的消息数量
channel.basicQos(1);
logger.info("---线程:{}--准备消费了---", this.getName());
//消费消息
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
logger.info("----线程:{}--消费消息:{}---", Thread.currentThread().getName(), new String(delivery.getBody(), StandardCharsets.UTF_8));
Thread.sleep(1000);
}
} catch (Exception e) {
logger.error("----消费线程出错了-:{}--", e.getMessage(), e);
} finally {
try {
assert channel != null;
channel.close();
} catch (Exception e) {
logger.error("---通道关闭出错了:{}---", e.getMessage(), e);
}
}
}
}
//创建线程,两个消费线程
new ConsumerThread("correct").start();
new ConsumerThread("correct.123").start();
}
从上面的代码可以看到,生产者指定的RoutingKey
是correct
。消费者启动了两个线程去消费消息,并且分别指定BindKey
为correct
和 correct.123
,那么由于是完全匹配,所以只能有一个线程消费消息。我们来看一下执行结果:从图中可以看到,结果确实如此,一直都是只有一个 Thread-1 在消费
十里平湖霜满天,寸寸青丝愁华年。对月形单望相互,只羡鸳鸯不羡仙。
Topic 主题消费模式
Topic模式也是在RoutingKey
和BindingKey
相匹配的时候才能收到消息。但是这里的匹配规则又和Direct模式不同。这里的匹配不再是精确匹配,而是 模糊匹配 。
Topic模式中,约定RoutingKey
和BindingKey
可以用 “.” 来进行分割,同时BindingKey
中可以使用特殊符号 “*” 和 “#” 来进行模糊匹配,以达到匹配的多样性。那么这两个特殊符号的作用是什么呢? 我们通过代码来探寻一下。
主题模式主方法:
/**
* 主题模式测试
* 启动消费者,并且持续定时发布消息
* @throws Exception
*/
private static void exchangeTopicTest() throws Exception {
//启动消费者
topicConsumer();
//持续发布消息
while (true) {
topicProducer();
Thread.sleep(6000);
}
}
主题模式生产者:
/**
* 主题模式(Topic)生产者
* RoutingKey 设定为:software.language.java
*
*/
private static void topicProducer() throws IOException, TimeoutException {
if (connection == null) {
throw new RuntimeException("连接信息为空了");
}
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_TOPIC, "topic");
//发布消息
for (int i = 0; i < 50; i++) {
channel.basicPublish(EXCHANGE_TOPIC, "software.language.java", null, ("topic_message_" + i).getBytes());
}
//关闭通道
channel.close();
}
主题模式消费者:
/**
* 主题模式消费者
*/
private static void topicConsumer() {
class TopicThread extends Thread {
private String routingKey;
private TopicThread(String routingKey) {
this.routingKey = routingKey;
}
@Override
public void run() {
String queueName = "topic_queue_" + this.getName();
if (connection == null) {
throw new RuntimeException("链接信息为空了");
}
Channel channel;
try {
channel = connection.createChannel();
//声明队列-并绑定
channel.queueDeclare(queueName, false, false, false, null);
channel.queueBind(queueName, EXCHANGE_TOPIC, routingKey);
channel.basicQos(1);
//消费消息
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
/*循环消费消息*/
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
logger.info("----线程:{}--BindingKey:{}--消费消息:{}---", this.getName(), this.routingKey, new String(delivery.getBody()));
Thread.sleep(1000);
}
} catch (IOException | InterruptedException e) {
logger.error("----{}-消费消息报错了:{}---", this.getName(), e.getMessage(), e);
}
}
}
/*新建消费线程并且指定路由键*/
/*这里指定的实际上是bindingKey,routingKey是在生产者的时候指定的,一个点号"." 分割的字符串,如:com.ai.example , com.exam.math*/
/*而bindingKey是在消费者这边指定的。BindingKey与RoutingKey一样也是点号"." 分割的字符串。
BindingKey中可以存在两种特殊的字符串"*" 和 "#" , 用于模糊匹配,其中 "*"用于匹配一个单词,"#"用于匹配多个单个(可以是零个)。*/
new TopicThread("software.language.java").start();
new TopicThread("software.#").start();
new TopicThread("software.*").start();
new TopicThread("software").start();
new TopicThread("software.language.#").start();
new TopicThread("software.language.*").start();
}
上面的消费者代码中,我们分别启动了六个线程,通过指定不同的BindingKey
来进行匹配消费。那么都哪几个能够成功的匹配成功并消费呢?我们来看一下RoutingKey
与BindingKey
的匹配结果:
从图中我们可以看到,有四个线程消费了消息。分别是 BindingKey software.language.java
、software.#
、software.language.*
和software.language.#
而且同时消费了相同的消息。也就是说这四个BindingKey
匹配成功了。所以这两个特殊符号的作用也就清晰了,我们只要记住路由匹配规则中的 特殊符号* 和 # 的作用就好:
- * 的作用是匹配一个单词
- # 的作用是匹配一个或者多个单词(可以是0个)
山河沦陷星空沉寂。我带着春天奔向最好的你
总结
通过上面的学习,我了解到了Rabbit MQ的一些特性、使用场景、Rabbit MQ的一个基本结构、以及一些简单的API使用。这次学习让我对MQ有了一个初步的认知,尤其是对 交换器 Exchange
的类型及使用有了初步的认识。但是想要深入理解还需要在项目中使用,在不断的使用中,才会有更加深入的认知。同时我学到了一些Docker的安装使用方法,对docker有了进一步的认识。
这一篇写的是安装及基本API的简单使用,没有集成到实际的项目当中,所以下一步准备将RabbitMQ集成到Springboot上。
项目地址