写在前面

为什么选择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 已经起来了

docker重新安装 mac_经验分享

当然也有一种可能,你发现列出来的镜像是没有我们启动时用--name指定的rabbitmq,那我们们要先来查看一下刚才创建的rabbitmq。使用命令docker ps -a 来查看所有创建的容器。这个里面一定有我们刚才创建的rabbitmq只是启动失败了,那我们复制一下他的CONTAINER ID 然后使用命令:docker logs 复制的CONTAINER ID 来查看时什么问题。

docker重新安装 mac_docker重新安装 mac_02


从图中可以看到,是权限问题,怎么办呢?可以看看这里。我选择在启动的时候增加一条命令:--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重新安装 mac_docker重新安装 mac_03


接下来在自己的电脑上,访问虚拟机上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依然是不行,谁能告诉我为什么?):

docker重新安装 mac_docker重新安装 mac_04


输入我们启动时指定的用户名和密码(admin/admin)进行登录,进入管理界面。如果启动时没有指定用户名和密码,使用默认账户密码guest/guest登陆。

接下来要做的是创建一个用户,然后创建一个Virtual Hosts,最后把用户和Virtual Hosts关联起来,程序中就可以使用了。当然如果说你想直接使用我们启动时指定的用户名和密码 admin/admin 以及 my_vhost 那就不用做下面的操作了,因为docker启动的时候已经帮我们把他们关联起来了。添加一个用户

docker重新安装 mac_rabbitmq_05


添加一个Virtual Hosts

docker重新安装 mac_rabbitmq_06


为mquser指定Virtual Hosts

返回Users那一栏,点击刚刚创建的用户名mquser,看到如下界面:选择创建的mq_virtual_host

docker重新安装 mac_经验分享_07


好了下面就进入紧张刺激的代码测试阶段了。

我看过大海、也可过繁星。但唯独遇见你,是我长久以来所有奔赴的意义。

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'

docker重新安装 mac_java_08

下面正式进入敲代码阶段:


单生产者-多消费者

首先创建一个类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);我这里就是自动确认的。执行效果如下:

docker重新安装 mac_docker重新安装 mac_09

上面的代码中、只是简单的测试了一下 单个生产者、多消费者 的情况,没有做过多的设置,算是个基础款,实现了最基本的消息传递,但是细心地你一定发现上面的代码中,好多地方传参是null 和空串"",没有涉及到一些专业术语、比如 交换器(Exchange)、绑定(Binding)、队列(Queue)、路由键(RoutingKey) 等的设置 。那下面我们就在使用的过程中,了解一下这几个东西,梳理一下他们之间的关系。

一花一世界,一叶一追寻。一曲一场叹,一生为一人

在开始之前,我们先来看一个简易的内部图,了解一下整体的流程结构:

docker重新安装 mac_rabbitmq_10

从图中可以看到,生产者生产了消息之后,先发送消息到交换器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上,所以两个线程都可以消费。

突然想到一个问题:是两个收到一样的,还是不一样的?(也就是说两个线程消费的是相同的消息吗?)按道理来说两个队列收到相同的消息,我们来看一下结果,可以看到就是猜想的那样,收到的都是相同的消息

docker重新安装 mac_经验分享_11

我梦江南好,征辽亦偶然。你携万水赴千山,不料浮生总是飘摇聚散。但求青山永在,不负伊人蹒跚。

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();
}

从上面的代码可以看到,生产者指定的RoutingKeycorrect。消费者启动了两个线程去消费消息,并且分别指定BindKeycorrectcorrect.123,那么由于是完全匹配,所以只能有一个线程消费消息。我们来看一下执行结果:从图中可以看到,结果确实如此,一直都是只有一个 Thread-1 在消费

docker重新安装 mac_java_12

十里平湖霜满天,寸寸青丝愁华年。对月形单望相互,只羡鸳鸯不羡仙。

Topic 主题消费模式

Topic模式也是在RoutingKeyBindingKey相匹配的时候才能收到消息。但是这里的匹配规则又和Direct模式不同。这里的匹配不再是精确匹配,而是 模糊匹配
Topic模式中,约定RoutingKeyBindingKey可以用 “.” 来进行分割,同时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 来进行匹配消费。那么都哪几个能够成功的匹配成功并消费呢?我们来看一下RoutingKeyBindingKey的匹配结果:

docker重新安装 mac_java_13


从图中我们可以看到,有四个线程消费了消息。分别是 BindingKey software.language.javasoftware.#software.language.*software.language.#而且同时消费了相同的消息。也就是说这四个BindingKey匹配成功了。所以这两个特殊符号的作用也就清晰了,我们只要记住路由匹配规则中的 特殊符号* 和 # 的作用就好:

  • * 的作用是匹配一个单词
  • # 的作用是匹配一个或者多个单词(可以是0个)

山河沦陷星空沉寂。我带着春天奔向最好的你

总结

通过上面的学习,我了解到了Rabbit MQ的一些特性、使用场景、Rabbit MQ的一个基本结构、以及一些简单的API使用。这次学习让我对MQ有了一个初步的认知,尤其是对 交换器 Exchange 的类型及使用有了初步的认识。但是想要深入理解还需要在项目中使用,在不断的使用中,才会有更加深入的认知。同时我学到了一些Docker的安装使用方法,对docker有了进一步的认识。
这一篇写的是安装及基本API的简单使用,没有集成到实际的项目当中,所以下一步准备将RabbitMQ集成到Springboot上。

项目地址