Rabbitmq基本使用以及与springboot集成简单示例

RabbitMQ 是采用 Erlang 语言实现 AMQP (Advanced Message Queuing Protocol ,高级消息
队列协议)的消息中间件,它最初起源于金融系统,用于在分布式系统中存储转发消息。 

windows安装Rabbitmq

安装erlang

因为RabbitMQ采用Erlang语言实现,所以要先安装Erlang。Erlang下载地址Downloads - Erlang/OTP。选择windows版本下载即可。下载的文件名为otp_win64_xxx.exe,其中xxx为版本号。注意要用管理员身份安装erlang

Rabbitmq基本使用以及与springboot集成简单示例_第1张图片

安装完成后需要设置erlang环境变量ERLANG_HOME,这样后面rabbitmq就能知道erlang安装在了哪里。

安装rabbitmq

接下来下载rabbitmq,下载地址 Installing on Windows — RabbitMQ,进入下载页面后,直接选择如下exe文件下载即可。下载后双击进行安装。
Rabbitmq基本使用以及与springboot集成简单示例_第2张图片

 安装完成后会自动生成一个windows服务。

在%rabbitmq安装目录%\rabbitmq_server-3.10.4\sbin目录下,有rabbitmqctl.bat,可通过这个bat脚本连接rabbitmq。在cmd窗口运行rabbitmqctl.bat status,查看rabbitmq是否运行成功。

Rabbitmq基本使用以及与springboot集成简单示例_第3张图片

 安装web管理插件

为了方便后面查看数据,我们先安装rabbitmq的web管理插件,这样就可以通过web界面来查看rabbitmq的数据。

查看rabbitmq安装了哪些插件。

rabbitmq_server-3.10.4\sbin\rabbitmq-plugins list

其中标有[E*]和[e*]的表示已经安装的插件,大写E表示显示启动,小写e表示隐式启动。

Rabbitmq基本使用以及与springboot集成简单示例_第4张图片

启用rabbitmq web管理页面插件

rabbitmq_server-3.10.4\sbin\rabbitmq-plugins enable rabbitmq_management

# 如果想要关闭某个插件,则把enable换成disable即可。

Rabbitmq基本使用以及与springboot集成简单示例_第5张图片

安装完插件后,重启rabbitmq,然后访问localhost:15672,输入用户名密码登录,即可进入管理页面。rabbitmq默认用户名密码都是guest。

Rabbitmq基本使用以及与springboot集成简单示例_第6张图片

 Rabbitmq基本使用以及与springboot集成简单示例_第7张图片

概念介绍

rabbitmq中有交换器exchange和队列,生产者不会直接把消息发往队列,而是发往交换器,再由交换器转发到队列,而消费者是直接从列队中消费消息。RabbitMQ 不支持队列层面的广播消费,如果需要广播消费,需要在其上进行二次开发,处理逻辑会变得异常复杂,同时也不建议这么做。

一个交换可以绑定多个队列,在绑定时会指定一个绑定键(BindingKey),而生产者在向交换发送消息时,会指定一个路由键(RoutingKey),当路由键和绑定键匹配时,交换就会把消息发送到匹配的列队上。可以通过下图来进行理解。列队1和交换进行绑定,绑定键为key1,生产者向交换发送一条消息,内容为abc,路由键为key1,因为队列1绑定的键也是key1,于是将消息发向了队列1。注意交换在向队列发送消息是,会丢弃路由键,只发送消息本身。路由键只是在查找绑定的队列时起作用。

上面我提到了RoutingKey和BindingKey,是为了方便说明问题作了区分,很多时候,并不会区分这两个,都叫routingKey,比如spring集成的rabbtimq中定义的方法参数名一般都叫routingKey。

Rabbitmq基本使用以及与springboot集成简单示例_第8张图片

上面只是消息转发的一种情况。在rabbitmq中,交换器一共有4种类型,分别为fanout, direct, topic, headers。下面分别说明。

fanout

这种类型交换器会把发送到该交换器的消息路由到所有与该交换器绑定的队列中,此时绑定键没有作用了。

direct

这种类型交换器只会把消息发送到RoutingKey和BindingKey完全匹配的队列中。也就是我们前面举例的情况。

topic

这种类型交换器也是要拿RoutingKey和BindingKey做比较,匹配成功的才会发往对应在队列,但是比较不再像是direct交换器那样,需要两个key严格相等,而是有一定的匹配规则。规则如下:

  • RoutingKey为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如“com.rabbitmq.client”;
  • BindingKey和RoutingKey一样也是点号“.”分隔的字符串。
  • BindingKey中可以存在两种特殊的字符串“*”和“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多规格单词(可以是零个)。

以下图中的绑定关系为例:

Rabbitmq基本使用以及与springboot集成简单示例_第9张图片

路由键为“com.rabbitmq.client”的消息会路由到队列1和队列2;

路由键为“com.baidu.map”的消息只会路由到队列2;

路由键为“abc.rabbitmq.client”的消息只会路由到队列1。

headers

headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中headers 属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时,RabbitMQ会获取到该消息的 headers (也是一个键值对的形式) ,对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列 headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。

通过java进行消息生产与消费

前面我们已经安装了rabbitmq,接下来示例如何通过java客户端来发送接收数据。注意下面的示例代码只是一个简单的示例,代码的目的是保证可以走通生产与消息消息即可,不作为实际开发参考。

首先引入rabbitmq java客户端依赖


    com.rabbitmq
    amqp-client
    5.14.2

消费者代码

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        /* ****建立连接**** */
        ConnectionFactory factory = new ConnectionFactory();
        // rabbitmq默认用户名:guest
        factory.setUsername("guest");
        // rabbitmq默认密码:guest
        factory.setPassword("guest");
        factory.setHost("127.0.0.1");
        // rabbitmq默认端口号:5672
        factory.setPort(5672);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        String queueName = "queueDemo1";
        // 创建名为queueDemo1的队列,该方法后面几个参数含义见后文
        channel.queueDeclare(queueName, true, false, true, null);
        // 消费消息
        // autoAck用于设置自动确认消息,如果不自动确认消息,需要手动调用channel.basicAck来确认消息
        boolean autoAck = true;
        channel.basicConsume(queueName, autoAck, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                System.out.println("consumerTag is: " + consumerTag);
                System.out.println("routingKey is: " + envelope.getRoutingKey());
                System.out.println("exchange is: " + envelope.getExchange());
                System.out.println("message is: " + new String(body));
            }
        });
        // sleep 30s,等待消息到来
        TimeUnit.SECONDS.sleep(30);
        // 关闭连接,应当在finally中关闭,此处只是简单示例。
        channel.close();
        connection.close();
    }
}

生产者代码

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        /* ****建立连接**** */
        ConnectionFactory factory = new ConnectionFactory();
        // rabbitmq默认用户名:guest
        factory.setUsername("guest");
        // rabbitmq默认密码:guest
        factory.setPassword("guest");
        factory.setHost("127.0.0.1");
        // rabbitmq默认端口号:5672
        factory.setPort(5672);
        Connection connection = factory.newConnection();

        /* ****创建交换器和队列,并将交接器和队列进行绑定**** */
        Channel channel = connection.createChannel();
        String exchangeName = "exchangeDemo1";
        String queueName = "queueDemo1";
        // 创建名为exchangeDemo1,类型为direct的交换器
        // exchangeDeclare有很多重载的方法,这里使用的是最简单的一种,其他方法及参数含义后文讲解。
        channel.exchangeDeclare(exchangeName, "direct");
        // 将交换器和队列绑定,队列queueDemo1我们在创建消息者时已经创建。
        String routingKey = "routingKeyDemo1";
        channel.queueBind(queueName, exchangeName, routingKey);

        /* ****发送消息**** */
        String msg = "Hello RabbitMQ";
        channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());

        /* ****关闭连接**** */
        // 应该在finally中关闭,此处只是简单示例。
        channel.close();
        connection.close();
    }
}

我们先启动消费者,再启动生产者,可以看到消费者打印出如下数据,说明消息发送接收成功。

Rabbitmq基本使用以及与springboot集成简单示例_第10张图片

同时我们进入rabbitmq管理界面,可以看到我们创建的queue。注意我们上面创建队列时指定了队列自动删除,所以请在消费者代码运行时查看队列,否则消费者代码结束时队列就自动删除了。

Rabbitmq基本使用以及与springboot集成简单示例_第11张图片

同时可以看到交换器和队列的绑定关系,也需要在消费者代码运行时查看。

Rabbitmq基本使用以及与springboot集成简单示例_第12张图片

快问快答

1. 消费者能消费到启动之前的消息吗?

可以。对于未消费的消息,rabbitmq会进行缓存,等到消费者消费后,再从缓存中删除。对于还未消费的消息,我们可以从管理界面看到。如下图,Ready的数量即还未被消费者消费的消息,等到有消费者接入进来后,rabbitmq就会把消息发送给消费者。

Rabbitmq基本使用以及与springboot集成简单示例_第13张图片

2. 创建队列时有哪些参数可以设置?

队列创建时有如下参数可以设置

Queue.DeclareOk queueDeclare(String queue, 
                             boolean durable, 
                             boolean exclusive, 
                             boolean autoDelete, 
                             Map arguments) 
throws IOException;

各参数说明如下:

queue: 队列名称

durable: 是否持久化,持久化的队列会存盘,rabbitmq服务器重启后队列还在。如果不是持久化的,重启后队列就不存在了。

exclusive: 是否排他。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意三点:1. 排他队列是基于连接( Connection) 可见的,同一个连接的不同信道 (Channel)是可以同时访问同一连接创建的排他队列; 2. "首次"是指如果一个连接己经声明了排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同;3. 即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。

autoDelete: 是否自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。不能把这个参数错误地理解为: "当连接到此队列的所有客户端断开时,这个队列自动删除",因为生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。

auguments: 其他一些结构化参数,此参数请参考相关资料,本文不再详解。

注意区分下持久化和自动删除,持久化是为了rabbitmq重启后队列信息不丢失,也就是队列信息会保存在硬盘中,即使是持久化的队列,如果设置了自动删除,在满足自动删除条件时,队列也会被删除。

3. 创建交换器时有哪些参数可以设置?

创建交换器我们使用的是exchangeDeclare方法,该方法有很多重载的形式,其中参数最多的一个方法签名如下:

Exchange.DeclareOk exchangeDeclare(String exchange,
                                   String type,
                                   boolean durable,
                                   boolean autoDelete,
                                   boolean internal,
                                   Map arguments) throws IOException;

各参数说明如下:

exchange: 交换器名称

type: 交换器类型,也就是我们前面提到的4种类型之一

durable: 是否持久化。持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。

autoDelete: 设置是否自动删除。 autoDelete设置为true表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。注意不能错误地把这个参数理解为 "当与此交换器连接的客户端都断开时,RabbitMQ会自动删除本交换器”。

如果要测试autoDelete参数的作用,可以如下操作: 1.声明一个交换器,并将autoDelete设置为true;2.声明一个队列,并绑定到这个交换器;3. 将队列与交换器解绑(调用queueUnbind方法),看交换器是否被自动删除。

以下给出了一个简单的测试示例

private static void testExchangeAutoDelete() throws IOException, TimeoutException, InterruptedException {
    /* ****建立连接**** */
    ConnectionFactory factory = new ConnectionFactory();
    // rabbitmq默认用户名:guest
    factory.setUsername("guest");
    // rabbitmq默认密码:guest
    factory.setPassword("guest");
    factory.setHost("127.0.0.1");
    // rabbitmq默认端口号:5672
    factory.setPort(5672);
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();

    /* ****测试自动删除**** */
    // 声明交换器
    String exchangeName = "exchange01";
    channel.exchangeDeclare(exchangeName, "direct", true, true, false, null);
    // 声明队列
    String queueName = "queue01";
    channel.queueDeclare(queueName, true, false, false, null);
    // 绑定交换器和队列
    String routingKey = "routingKey01";
    channel.queueBind(queueName, exchangeName, routingKey);
    // sleep 30s
    TimeUnit.SECONDS.sleep(30);
    System.out.println("sleep结束");
    // 在sleep结束前,通过管理页面查看exchange存在,
    // sleep结束,解绑后,再查看exchange已经自动删除了。
    channel.queueUnbind(queueName, exchangeName, routingKey);
    channel.close();
    connection.close();
}

internal: 设置是否是内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式(交接器不仅可以路由到队列,也可以路由到另一个交换器)。

argument:其他一些结构化参数,此参数请参考相关资料,本文不再详解。

4. 队列或交换器重复声明会怎么样?

如果声明时参数完全一样,rabbitmq就什么也不做。但是如果同一个队列或交换器声明多次,且参数不一样,就会报错。

如下,我声明了两次“queue01”,参数不一样,就会报错 “channel error; protocol method: #method(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'queue01' in vhost '/': received 'true' but current is 'false', class-id=50, method-id=10)”

String queueName = "queue01";
channel.queueDeclare(queueName, false, false, true, null);
// 两次声明,参数不一样,会报错
channel.queueDeclare(queueName, true, false, true, null);

5. 何时创建

因为生产者要向交换器发送消息,所以毫无疑问,生产者在生产消息之前要创建好交换器。但是一般生产者不知道消费者是谁,所以队列可以由消费者自己创建,然后消费者将自己的队列绑定到交换器上来消费消息。当然生产者也可以创建队列,这个可以根据实际业务需求来。

springboot集成rabbitmq简单示例

首先通过Springboot Initializr 创建web应用,并选择spring for rabbitmq。

Rabbitmq基本使用以及与springboot集成简单示例_第14张图片

然后编写生产者和消费者代码,相关说明我已经放在注释,直接看如下代码。

1. 定义用来处理消息的类

/**
 * 定义用来处理消息的类
 */
@Component
public class Receiver {

    /**
     * 处理消息的方法,方法名可以随便起,只要在后面配置时告诉spring调用哪个方法即可。
     * 这里为了演示方法名可以随便起,用了个比较随意的名字。
     */
    public void someMethod(String message) {
        System.out.println("收到消息: " + message);
    }
}

2. 消费者配置

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConsumerConfig {

    static final String exchangeName = "exchangeDemo01";

    static final String queueName = "queueDemo01";

    /**
     * 定义队列,程序启动后spring会自动创建好对应的队列。
     */
    @Bean
    Queue queue() {
        //public Queue(String name, boolean durable, boolean exclusive,
        // boolean autoDelete, @Nullable Map arguments)
        // 这里定义queue需要的参数我们前面已经讲解过
        return new Queue(queueName, false, false, true, null);
    }

    /**
     * 定义交换器,程序启动后spring会自动创建好对应的交换器。
     */
    @Bean
    TopicExchange exchange() {
        // public TopicExchange(String name, boolean durable,
        // boolean autoDelete, Map arguments)
        // 这里定义exchange需要的参数我们前面已经讲解过
        // 这里定义的交换器类型为topic,如果要定义direct类型,用DirectExchange
        return new TopicExchange(exchangeName, false, true, null);
    }

    /**
     * 绑定,参数queue和exchange就是我们上面定义的两个,spring会自动注入。
     */
    @Bean
    Binding binding(Queue queue, TopicExchange exchange) {
        String routingKey = "foo.bar.#";
        return BindingBuilder.bind(queue).to(exchange).with(routingKey);
    }

    /**
     * 定义适配器,因为前面我们自定义的Receiver可以随便写,所以需要一个适配器,
     * 告诉spring调用哪个方法来处理消息。
     */
    @Bean
    MessageListenerAdapter listenerAdapter(Receiver receiver) {
        return new MessageListenerAdapter(receiver, "someMethod");
    }
    /**
     * 定义消费者容器,可以指定要订阅哪些队列
     */
    @Bean
    SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames(queueName);
        container.setMessageListener(listenerAdapter);
        return container;
    }

}

3. 生产者代码

@RestController
public class SendMsgTestController {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @RequestMapping("/send")
    public String send() {
        // public void convertAndSend(String exchange, String routingKey, final Object object)
        rabbitTemplate.convertAndSend(ConsumerConfig.exchangeName,
                "foo.bar.abc", "hello RabbitMQ");
        return "success";
    }

}

最后,我们浏览器访问/send,就可以成功发送消息,并且可以看到控制台打印出了收到的消息。

参考资料

《RabbitMQ实战指南》 朱忠华

Getting Started | Messaging with RabbitMQ (spring.io)

你可能感兴趣的:(java-rabbitmq,rabbitmq,spring,boot)