RabbitMQ入门到精通

898 阅读24分钟

第一章 RabbitMQ简介

1.1 什么是消息中间件

1.消息(Message):指在应用之间传递的数据。

2.消息中间件(Message Queue Middleware):利用高效可靠的消息传递机制在应用间进行与平台无关的数据交流,并基于数据通信进行分布式系统的集成。

3.常见的开源消息中间件:RabbitMQ, ActiceMQ, Kafaka,RocketMQ等

1.2 消息中间件的作用

1.冗余(存储):消息中间件把消息进行持久化,直到消息已经被完全处理,从而保证数据被安全的保存直到使用完毕。

2.解耦:消息中间件在处理过程中插入了隐含的,基于数据的接口层,两边的数据处理都需要实现该接口,允许你独立的扩展和修改两边的处理过程。

3.扩展性:消息中间件解耦了消息的处理过程,很容易提高消息的入队和处理效率。

4.削峰:访问量突增的情况下,消息中间件能减少关键组件的压力,不会因为突发的超负荷请求而崩溃。

5.可恢复性:当一部分组件失效后,不会影响到整个系统,即使一个消息的进程挂掉,加入消息中间件的消息仍然可以在系统恢复后进行处理。

6.缓冲:消息中间件通过缓冲层帮助消息高效的的执行。

7.异步通信:很多应用不想也不需要立即处理程序,消息中间件提供了异步处理机制,允许把消息放在消息中间件中,但不是立即处理它,在之后需要的时候再慢慢处理。

8.顺序保证:消息中间支持在一定程度的顺序保证。(RabbitMQ的顺序保证不是绝对的,在一定条件在无法保证消息的顺序性)

1.3 RabbitMQ的安装和使用(Windows环境)

1.RabbitMQ的底层语言使用Erlang语言实现,所以需要在电脑上安装Erlang。

2.Erlang的安装
官网下载地址:www.erlang.org/downloads
由于访问外网,下载速度较慢,建议在网上查找驱动程序下载安装,如RabbitMQ推荐Erlang安装网址:www.erlang-solutions.com/resources/d…

3.RabbitMQ安装 官网下载地址:www.rabbitmq.com/#getstarted

4.开启RabbitMQ的Web管理界面 找到RabbitMQ的安装源文件

双击打开该文件,在命令行中敲入“rabbitmq-plugins enable rabbitmq_management”后回车,开启RabbitMQ的web插件管理

此时访问网址http://127.0.0.1:15673,可以打开RabbitMq的web管理界面,默认的登录用户名和密码均是guest。

1.4 生产和消费消息

1.本节使用RabbitMQ的Java客户端演示如何生产和消费消息,主要使用的IDE工具是IDEA,使用maven构建普通的项目。
2.目前最新的RabbitMQ Java客户端版本为5.8.0,引入下面的依赖项

<dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.25</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

3.默认情况下,访问RabbitMQ的用户名和密码都是guest,可以在登录RabbitMQ web管理界面后手动添加用户名和密码并分配权限,下面的代码演示中均采用用户名root和密码123456演示。

4.创建连接工具类(连接RabbitMQ服务器)

package com.mq.util;

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

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

/**
 * 创建MQ的连接
 */
public class ConnectionUtil {

    //要连接的mq主机,此处为本地连接
    private static final String HOST = "127.0.0.1";
    //mq的端口号默认为5672
    private static final int PORT = 5672;
    //访问mq的用户名
    private static final String USER_NAME = "root";
    //用户密码
    private static final String PASSWORD = "123456";

    public static Connection getConnection() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(HOST);
        factory.setPort(PORT);
        factory.setUsername(USER_NAME);
        factory.setPassword(PASSWORD);
        Connection connection = factory.newConnection();
        return connection;
    }
}

5.生产者客户端代码:

package com.mq.simple;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;

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

/**
 * 简单队列的消息生产者
 */
public class SendSimple {

    //交换器名称
    private static final String EXCHANGE = "exchange.simple";
    //队列名称
    private static final String QUEUE = "queue.simple";
    //路由键
    private static final String ROUTING_KEY = "routing.key.simple";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接
        Connection connection = ConnectionUtil.getConnection();
        //创建信道
        Channel channel = connection.createChannel();

        //声明一个类型为“direct”,持久化,非自动删除的交换器
        channel.exchangeDeclare(EXCHANGE, "direct", true, false, null);
        //声明一个持久化的,排他的,非自动删除的队列
        channel.queueDeclare(QUEUE, true, false, false, null);
        //绑定交换器和队列
        channel.queueBind(QUEUE, EXCHANGE, ROUTING_KEY);

        //消息
        String message = "This is the simple message!";
        //发送消息
        channel.basicPublish(EXCHANGE, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

        System.out.println("Send the message: " + message);

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

6.消费者客户端代码

package com.mq.simple;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.*;

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

/**
 * 简单队列的消费者
 */
public class RecvSimple {

    //消费者要监听的队列名称
    private static final String QUEUE = "queue.simple";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接
        Connection connection = ConnectionUtil.getConnection();
        //创建信道
        final Channel channel = connection.createChannel();

        //消费者
        Consumer consumer = new DefaultConsumer(channel) {
            //消费者消费消息时执行的代码块
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body);
                System.out.println("Simple message: " + message);
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        //消费消息
        channel.basicConsume(QUEUE, false, consumer);
    }
}

1.5 小结

本章首先针对消息中间件做了一个摘要性的介绍,包括什么是消息中间件、消息中间件的作用及消息中间件的特点等。之后引入RabbitMQ ,对其历史做一个简单的阐述,比如RabbitMQ 具备哪些特点。本章后面的篇幅介绍了RabbitMQ 的安装及简单使用,通过演示生产者生产消息,以及消费者消费消息来给读者一个对于RabbitMQ 的最初的印象,为后面的探 索过程打下基础。




第二章 RabbitMQ入门

2.1 相关概念介绍

  1. RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消息传递的过程想象成:当你将一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人的手上,RabbitMQ就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面来说,RabbitMQ 模型更像是一种交换机模型。

2.2 连接(Connection)

  1. Connection: 连接,生产者和消费者都需要与RabbitMQ通信,具体来说是与Broker建立连接。Connection即是一条TCP连接。

2.3 信道(Channel)

  1. Channel: 信道,信道是建立在Connection之上的虚拟连接,所有的指令都是在信道上完成的。
  2. 为什么需要信道,而不是直接通过Connection操作指令?多个线程需要消费消息的时候,如果使用Connection,每次都会建立和销毁TCP连接,此过程的开销比较昂贵,所以RabbitMQ使用TCP连接复用,减少性能开销。

2.4 生产者(Producer)

  1. Producer:生产者,投递消息,生产消息的一方。
  2. 生产者创建消息,然后发布到RabbitMQ中。消息一般包含两个部分:消息体(payload)和标签(label),如上图所示。消息体(payload)是带有业务逻辑的数据,消息的标签(label)用来描述这条消息,比如交换器的名称和路由键。RabbitMQ之后会根据标签把消息投递给感兴趣的消费者。

2.5 消费者(Consumer)

  1. Consumer: 消费者,接收消息的一方。
  2. 消费者连接到RabbitMQ服务器,并订阅到队列上。消费者只是消费消息的消息体(payload),消息在路由的过程中,标签(label)信息会丢弃,存入到RabbitMQ中的只有消息体,消费者也没有必要关注消息的生产者来源。

2.6 服务节点(Broker)

  1. Broker: 消息的服务节点。
  2. 可以将一个RabbitMQ Broker看做一个服务节点,大多数情况下,也可以将Broker看做是一台RabbitMQ服务器。

2.7 队列(Queue)

  1. Queue: 队列,是RabbitMQ的内部对象,RabbitMQ中的所有消息都只存在队列中。
  2. 多个消费者可以订阅同一个队列,此时队列中的消息会被消费者平均分摊,即采用轮询的机制。换句话说,每条消息只能被一个消费者处理,而不是每个消费者收到所有的消息并处理。(后面会详细解释轮询策略以及轮询策略可能带来的弊端)

2.8 交换器(Exchange)、路由键(RoutingKey)、绑定键(BindKey)

  1. Exchange: 交换器,生产者发布消息后,消息经过交换器,然后被路由到不同的消息队列中。交换器只是起到路由的作用,本身不存储消息,也不存在存储消息的能力。

  2. BindKey: 绑定键,RabbitMQ通过绑定键(BindKey)将交换器和队列绑定起来。

  3. RoutingKey: 路由键,生产者将消息发送给交换器的时候,一般会指定一个RoutingKey,该RountingKey与BindKey联合使用,指定消息的路由规则,当满足某种关系时,交换器就能将消息发送到满足条件的消息队列中。(具体的规则和Exchange的类型有关)

2.9 交换器类型

交换器共有四种类型,分别是fanout, direct, topic, header。

1.fanout

不考虑RountingKey和BindKey的关系,交换器会将消息发布到所有与其绑定的消息队列中。

1.1 结构图

1.2 解析

当交换器的类型为fanout时,会直接忽略路由键和绑定键的关系(均不起作用),此时无需指定键,交换器会将信息发送到所有与交换器绑定的队列中,即消息message会被发送到Queue1和Queue2中。

1.3 生产者代码

package com.mq.fanout;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;

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

/**
 * fanout类型的交换器测试
 */
public class SendFanout {

    private static final String EXCHANGE_NAME = "exchange.fanout";
    private static final String QUEUE1 = "queue1";
    private static final String QUEUE2 = "queue2";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        /**
         * 声明fanout类型的交换器
         */
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true, false, null);

        /**
         * 声明队列queue1
         */
        channel.queueDeclare(QUEUE1, true, false, false, null);

        /**
         * 声明队列queue2
         */
        channel.queueDeclare(QUEUE2, true, false, false, null);

        /**
         * 由于交换器类型为fanout,所以此处不需要指定路由键(即绑定键),直接将队列queue1及queue2与交换器绑定
         */
        channel.queueBind(QUEUE1, EXCHANGE_NAME, "");
        channel.queueBind(QUEUE2, EXCHANGE_NAME, "");

        String message = "This is exchange typed fanout!";
        /**
         * 发送消息时同样不需要路由键
         */
        channel.basicPublish(EXCHANGE_NAME, "", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

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

注意:绑定和发布消息时可以不指定路由键,但不能用null表示,可以用空字符串""来表示路由键不存在。

1.4 消费者1代码

package com.mq.fanout;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.*;

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

/**
 * 消费者1,监听队列queue1
 */
public class Recv1 {

    private static final String QUEUE1 = "queue1";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();

        /**
         * 声明队列queue1
         */
        channel.queueDeclare(QUEUE1, true, false, false, null);

        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body);
                System.out.println(message);
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };

        channel.basicConsume(QUEUE1, false, consumer);
    }
}

控制台打印输出如下:

1.5 消费者2代码

package com.mq.fanout;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.*;

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

/**
 * 消费者2,监听队列queue2
 */
public class Recv2 {

    private static final String QUEUE2 = "queue2";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();

        /**
         * 声明队列queue1
         */
        channel.queueDeclare(QUEUE2, true, false, false, null);

        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body);
                System.out.println(message);
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };

        channel.basicConsume(QUEUE2, false, consumer);
    }
}

控制台打印输出如下:

2.direct

交换器将消息发布到RoutingKey和BindKey完全匹配的队列中。

2.1 结构图:

2.2 解析:

交换器通过绑定键“ROUTING_KEY_WARNING”和“ROUTING_KEY_INFO”与消息队列Queue1绑定。通过“ROUTING_KEY_INFO”和消息队列Queue2绑定。由于交换器的类型为direct,message1在发布时指定路由键ROUTING_KEY_WARNING,所以只发送到消息队列Queue1上,message2在发布时指定路由键为ROUTING_KEY_INFO,可以发送到Queue1和Queue2上,因此,消费者1能接收到两条消息,消费者2只能接收到一条消息。

2.3 生产者代码如下所示:

package com.mq.direct;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;

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

/**
 * 使用Direct类型的交换器
 */
public class SendDirect {

    private static final String EXCHANGE = "exchange.direct";
    private static final String QUEUE1 = "queue1";
    private static final String QUEUE2 = "queue2";
    private static final String ROUTING_KEY_WARNING = "warning";
    private static final String ROUTING_KEY_INFO = "info";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        //声明一个direct类型的队列
        channel.exchangeDeclare(EXCHANGE, "direct", true, false, null);
        channel.queueDeclare(QUEUE1, true, false, false, null);
        channel.queueDeclare(QUEUE2, true, false, false, null);

        /**
         * 交换器和队列1通过ROUTING_KEY_WARNING/ROUTING_KEY_INFO两个绑定键进行绑定
         */
        channel.queueBind(QUEUE1, EXCHANGE, ROUTING_KEY_WARNING);
        channel.queueBind(QUEUE1, EXCHANGE, ROUTING_KEY_INFO);

        /**
         * 交换器和队列2只通过绑定键ROUTING_KEY_INFO进行绑定
         */
        channel.queueBind(QUEUE2, EXCHANGE, ROUTING_KEY_INFO);

        /**
         * message1,将会发送到通过ROUTING_KEY_WARNING绑定的队列中(一个队列:Queue1)
         */
        String message1 = "The message1 from the direct exchange has been published!";
        channel.basicPublish(EXCHANGE, ROUTING_KEY_WARNING, MessageProperties.PERSISTENT_TEXT_PLAIN, message1.getBytes());

        /**
         * message2,发送到通过ROUTING_KEY_INFO绑定的队列中(两个队列:Queue1和Queue2)
         */
        String message2 = "The message2 from the direct exchange has been published";
        channel.basicPublish(EXCHANGE, ROUTING_KEY_INFO, MessageProperties.PERSISTENT_TEXT_PLAIN, message2.getBytes());

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

2.4 消费者1的代码

package com.mq.direct;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.*;

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

/**
 * 监听warning路由键队列的消费者
 */
public class Recv1 {

    private static final String QUEUE1 = "queue1";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();

        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body);
                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("Recv message: " + message);
            }
        };

        /**
         * 消费者监听队列1(能收到两条消息)
         */
        channel.basicConsume(QUEUE1, false, consumer);
    }
}

控制台打印输出如下:

2.5 消费者2的代码

package com.mq.direct;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.*;

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

/**
 * 监听Info路由键队列的消费者
 */
public class Recv2 {

    private static final String QUEUE2 = "queue2";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();

        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body);
                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("Recv message: " + message);
            }
        };

        /**
         * 消费者监听队列2(只能收到一条消息)
         */
        channel.basicConsume(QUEUE2, false, consumer);
    }
}

控制台打印输出如下:

3.topic

由于类型为direct的交换器,在发布消息时,需要路由键和绑定键完全匹配,适用的范围较窄。topic类型的交换器解决了此种问题,类似的可以理解成不需要完全匹配,而是模糊匹配。

3.1 匹配约定

  1. RoutingKey和BindKey都是由“.”号分割的字符串,被“.”分割的字符串时一个独立的单词,如com.rabbitmq.client。
  2. BindKey可以用特殊字符串“*”和“#”作模糊匹配。
  3. “*”可以用来匹配一个单词。
  4. “#”可以用来匹配多个单词(也可以是0个)。

3.2 结构图

3.3 匹配规则

  1. 路由键为“com.rabbitmq.client”的消息会同时路由到Queue1和Queue2
  2. 路由键为“com.hidden.client”的消息只会路由到Queue2
  3. 路由键为“com.hidden.demo”的消息只会路由到Queue2
  4. 路由键为“java.rabbitmq.demo”的消息只会路由到Queue1
  5. 路由键为“java.util.collection”的消息不会路由到任何的队列,因为没有匹配任何的路由键,要么返回给生产者,要么被丢弃

4.header

header类型的交换器不根据路由键的匹配规则路由消息,而是根据要发送的消息内容的header属性,一般不使用,不作过多介绍。

2.10 AMQP协议简介

1.AMQP协议的三层架构

  1. Module Layer: 位于协议的最高层,定义了供客户端调用的命令,客户端使用命令实现自己的业务逻辑。
  2. Session Layer: 位于中间层,将客户端的命令发送给服务器,再将服务器的应答返回给客户端。
  3. Transpory Layer: 位于协议的最底层,主要用于传输二进制数据流。

2.AMQP协议生产者运行流程

  1. 生产者运行流程图

  2. 生产者运行过程解析
    2.1 生产者连接到RabbitMQ Broker,建立连接(Connection),开启信道(Channel);
    2.2 生产者声明交互器,并指明交换器的属性(类型,持久化,自动删除等);
    2.3 生产者声明队列,并指明队列的属性(名称,持久化,排他,自动删除等);
    2.4 生产者将交换器和队列进行绑定;
    2.5 生产者发送消息至RabbitMQ Broker,携带交换器,路由键等信息;
    2.6 相应的交换器根据路由键找到对应的队列;
    2.7 如果找到相应的队列,就将消息放入到队列中;
    2.8 如果找不到匹配的队列,要么将消息返回给生产者,要么将消息丢失;
    2.9 关闭信道;
    2.10 关闭连接

3.AMQP协议消费者运行流程

  1. 消费者运行流程图

  2. 消费者运行过程解析
    2.1 消费者连接到RabbitMQ Broker,建立连接,开启信道;
    2.2 消费者请求消费队列中的消息;
    2.3 RabbitMQ Broker投递消息,消费者消费;
    2.4 消费者确认收到的消息(ack);
    2.5 RabbitMQ从队列中删除已经被消费者应答的消息;
    2.6 关闭信道;
    2.7 关闭连接

2.11 小结

本章主要讲述的是RabbitMQ 的入门知识,首先介绍了生产者C Producer ) 、消费者 CConsumer) 、队列CQueue) 、交换器CExchange) 、路由键C RoutingKey )、绑定C Binding) 、 连接C Connection) 和信道CChannel) 等基本术语,还介绍了交换器的类型: fanout、direct、topic 和headers 。之后通过介绍RabbitMQ 的运转流程来加深对基本术语的理解。




第三章 API详解

3.1 Conection newConnection();

  1. 用来创建RabbitMQ Client到RabbitMQ Broker的连接,通过创建ConnectionFactory类的对象,然后通过连接工厂创建连接。

3.2 Channel createChannel()

  1. 通过connection.newChannel()方法创建信道。前面说过,所有RabbitMQ上的操作都是通过信道完成的。

3.3 Exchange.DeclareOk exchangeDeclare(String exchange, String type, boolean durable,boolean autoDelete, boolean internal, Map<String, Object> arguments)

  1. 方法用来定义和创建交换器
  2. 该方法的返回值Exchange.DeclareOk,用来表示成功声明一个交换器
  3. exchange:交换器的名称
  4. type:交换器的类型,包括fanout, direct, topic, header四种,其中较为常用的是fanout和topic
  5. durable:是否将该交换器持久化,经过持久化的交换器,将会存储到磁盘上,即使RabbitMQ服务器重启,也不会引起交换器的消失。反之,每次重启服务器后,交换器都会消失
  6. autoDelete:是否自动删除,设置为true表示会自动删除。自动删除的前提是:至少有一个队列或者交换器与该交换器进行绑定,之后所有与该交换器绑定的交换器或者队列都与其解绑
  7. internal: 设置为true表示该交换器是内置的。客户端(生产者)不能直接发送消息到此交换器上,只能通过交换器路由到交换器这种方式,也就是说:只能通过其他交换器将消息路由到该交换器
  8. arguments:交换器相关的一些参数,后面会对部分参数作具体介绍
  9. 注意:生产者和消费者都能声明交换器,如果生产者和消费者声明的交换器的参数完全一致,即同一个交换器,RabbitMQ将会在第二个声明时,什么也不做。反之,如果两个交换器的名称相同,但其他任一参数不同,后声明的交换器在创建时将会抛出异常

3.4 Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)

  1. 方法用来定义和创建消息队列
  2. 该方法的返回值Queue.DeclareOK,用来表示成功声明一个消息队列
  3. queue:声明的队列的名称
  4. durable:设置为true表示将消息队列持久化,服务器的重启不会引起队列的消失
  5. exclusive:设置为true表示是一个排他的队列。排他队列只对声明该队列的连接(Connection)可见,其他连接不可访问该队列,但声明该队列的连接下的多个信道,是可以访问该排他队列的
  6. autoDelete:设置为true表示自动删除。自动删除的前提是:至少有一个消费者与该队列连接,之后所有与该队列连接的消费者都与其断开连接
  7. arguments:与队列相关的参数,部分参数后面将会具体介绍
  8. 注意:生产者和消费者都能声明队列,如果生产者和消费者声明的队列的参数完全一致,即同一个队列,RabbitMQ将会在第二个声明时,什么也不做。反之,如果两个队列的名称相同,但其他任一参数不同,后声明的队列在创建时将会抛出异常

3.5 Queue.BindOk queueBind(String queue, String exchange, String routingKey)

  1. 方法用来将队列和交换器通过路由键进行绑定
  2. queue:队列名称
  3. exchange:交换器名称
  4. routingKey:将队列和交换器绑定的路由键

3.6 Exchange.BindOk exchangeBind(String destination, String source, String routingKey)

  1. 该方法用来将交换器和交换器进行绑定,就比如将一个非内置的交换器与一个内置的交换器进行绑定
  2. destination:消息转发到(to)该交换器。可以是一种内置的交换器等
  3. source:消息从(from)该交换器转发
  4. routintKey:两个交换器绑定的路由键

3.7 void basicPublish(String exchange, String routingKey, boolean mandatory,BasicProperties props, byte[] body)

  1. 该方法由消息的生产者调用,将消息发布到交换器,进而路由到队列中
  2. exchange:消息发布到交换器的名称
  3. routingKey:发送消息时指定的路由键,交换器和路由键一起确定消息要发布的那个队列
  4. mandatory:设置为true表示消息发送失败时,回调给生产者处理,否则消息直接丢失
  5. prop:消息的一些属性配置,后面将会详细介绍部分配置
  6. body:消息体,以字节数组的方式传递

3.8 String basicConsume(String queue, boolean autoAck, Consumer callback)

  1. 该方法由消费者调用,RabbitMQ将消息推送到消费者供消费
  2. queue:消费者监听的队列的名称
  3. autoAck:设置为true表示自动确认。RabbitMQ将消息发出后就会将队列中的消息删除,不管消费者是否已经接收并消费了消息,此种方式可能会造成消息的丢失,对于重要数据建议修改为false,消费者提供手动应答
  4. callback:消费者对象

3.9 小结

本节主要介绍了关系连接RabbitMQ Broker,创建信道,声明交换器,声明队列,交换器和队列的绑定,生产者发布消息以及消费者消费消息的基本api使用。熟悉上述api后就能基本实现从消息发布到消费的整个流程。




第四章 RabbitMQ的高阶用法

4.1 消息的来去

简单的生产消费模型,生产者将消息发布到队列中,消费者从队列中获取消息并消费,这只是消息成功发送和成功接收的情况,在最理想的情况下。实际上,可能会遇到生产者发送消息失败,生产者发送消息后未收到RabbitMQ的消息应答,还有可能遇到消费者未能成功的接收消息,消费者没有对已接收的消息做消息确认(ack)等。

1. mandatory

  1. 当生产者发布消息时,交换器可能根据自身类型找不到一个与绑定键对应的队列。将mandatory参数设置为true时,RabbitMQ会调用Basic.Return指令,将消息返回给生产者。若mandatory设置为false,无法发布到队列中的消息将会直接丢失
  2. mandatory参数表示将消息发送到与交换器绑定的至少一个队列中才不会返回给生产者。如果消息返回给了生产者,那么这条消息肯定没有进入过队列(这是与immediate参数本质的区别)

2. immediate(已弃用)

  1. 生产者成功发布消息到队列上后,如果没有任何消费者对此队列监听时,消息会被返回给生产者
  2. immediate使消息进入到了至少一个队列

3. 备份交换器

  1. 结构图

  2. 原理解析
    2.1 生产者发送消息到指定的交换器normalExchange,交换器不能与消息携带过来的路由键确定到某一个队列上时,在没有设置mandatory参数时,消息会直接丢失。在设置了备份交换器后,原本将会丢失的消息会被路由到备份交换器中,再被发送到与备份交换器绑定的队列中。
    2.2 一般会将备份交换器的类型设置为fanout。
    2.3 为交换器设置备份交换器,在声明交换器时的Map类型arguments参数中设置,key为固定的“alternate-exchange”,值为备份交换器的名称

        /**
         * 声明normal交换器,设置normal交换器的参数,将其绑定一个备份交换器
         * 将交换器与队列进行绑定
         */
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("alternate-exchange", EXCHANGE_ALTER);
        channel.exchangeDeclare(EXCHANGE_NORMAL, "direct", true, false, map);
  1. 代码演示
    3.1 生产者代码
package com.mq.alter;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;

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

/**
 * 测试备份交换器的使用
 */
public class SendAlternate {

    private static final String EXCHANGE_NORMAL = "normalExchange";
    private static final String EXCHANGE_ALTER = "alterExchange";
    private static final String ROUTING_KEY = "normal";
    private static final String QUEUE_NORMAL = "queueNormal";
    private static final String QUEUE_ALTER = "queueAlter";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        /**
         * 声明normal交换器,设置normal交换器的参数,将其绑定一个备份交换器
         * 将交换器与队列进行绑定
         */
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("alternate-exchange", EXCHANGE_ALTER);
        channel.exchangeDeclare(EXCHANGE_NORMAL, "direct", true, false, map);
        channel.queueDeclare(QUEUE_NORMAL, true, false, false, null);
        channel.queueBind(QUEUE_NORMAL, EXCHANGE_NORMAL, ROUTING_KEY);

        /**
         * 声明备份交换器并绑定一个队列
         */
        channel.exchangeDeclare(EXCHANGE_ALTER, "fanout", true, false, null);
        channel.queueDeclare(QUEUE_ALTER, true, false, false, null);
        channel.queueBind(QUEUE_ALTER, EXCHANGE_NORMAL, "");

        /**
         * 发送消息到normal交换器,该消息能发送到normal队列
         */
        String message1 = "Test normal exchange...";
        channel.basicPublish(EXCHANGE_NORMAL, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message1.getBytes());

        /**
         * 发送消息到normal交换器,该消息不能发送到normal队列,会被传送到alternate队列
         */
        String message2 = "Test alternate exchange...";
        channel.basicPublish(EXCHANGE_NORMAL, "", MessageProperties.PERSISTENT_TEXT_PLAIN, message2.getBytes());

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

RabbitMQ管理界面:

3.2 监听normal队列的消费者1代码

package com.mq.alter;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.*;

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

/**
 * 监听与normal交换器绑定的队列
 */
public class RecvNormal {

    private static final String QUEUE_NORMAL = "queueNormal";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NORMAL, true, false, false, null);
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body);
                System.out.println(message);
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        channel.basicConsume(QUEUE_NORMAL, false, consumer);
    }
}

normal队列中的消息已经被消费: 控制台页面:

3.3 监听alternate队列的消费者2代码

package com.mq.alter;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.*;

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

/**
 * 监听与备份交换器绑定的队列
 */
public class RecvAlter {

    private static final String QUEUE_ALTER = "queueAlter";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_ALTER, true, false, false, null);
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body);
                System.out.println(message);
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        channel.basicConsume(QUEUE_ALTER, false, consumer);
    }
}

alternate队列中的消息已经被消费: 控制台页面:

4. 总结

  1. 备份交换器是为了解决生产者发布消息失败从而导致消息丢失的问题,取代了mandatory参数。其本质就是一个普通的交换器,没有什么特殊之处。
  2. 推荐将备份交换器的类型设置为fanout。
  3. 通过在交换器声明时的Map类型中设置,为交换器添加备份交换器,map中的key固定为“alternate-exchange”。

4.2 过期时间(TTL)

1. 设置队列的过期时间

  1. 通过声明队列时设置Map类型的参数,控制队列的过期时间,此时整个队列中所有消息具有相同的过期时间
  2. 设置队列的TTL属性,一旦消息过期,就会从队列中抹去
  3. Map类型的参数的Key为固定字符串“x-message-ttl”,Value为超时间时间,单位为毫秒
        /**
         * 声明队列,并通过Map类型的参数设置队列的过期时间为10s
         */
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("x-message-ttl", 10000);
        channel.queueDeclare(QUEUE_TTL, true, false, false, map);
  1. 完整代码演示
package com.mq.ttl;

import com.mq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;

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

/**
 * 测试设置队列的过期时间
 */
public class SendTTL {

    private static final String EXCHANGE_TTL = "exchange.ttl";
    private static final String QUEUE_TTL = "queue.ttl";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_TTL, "fanout", true, false, null);

        /**
         * 声明队列,并通过Map类型的参数设置队列的过期时间为10s
         */
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("x-message-ttl", 10000);
        channel.queueDeclare(QUEUE_TTL, true, false, false, map);

        channel.queueBind(QUEUE_TTL, EXCHANGE_TTL, "");

        String message = "test the message ttl";
        channel.basicPublish(EXCHANGE_TTL, "", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

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

2. 设置消息的过期时间

  1. 也可以为放入队列的消息分别设置不同的超时时间
  2. 为消息设置超时时间属性,消息过期后,不会立刻从队列中删除,每条消息是否过期,是在消费者消费之前判定的
  3. 需要在发布消息时,控制消息的properties属性
        /**
         * 设置消息的超时时间为3s
         */
        String message = "test the message ttl";
        AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
        builder.expiration(String.valueOf(3000));
        channel.basicPublish(EXCHANGE_TTL, "", builder.build(), message.getBytes());
  1. 当同时设置队列的超时时间和消息的超时时间后,具体的超时时间由更短的那个时间决定,例如上面的队列超时间时间为10s,消息的超时时间单独设置为3s,那么这条消息的超时时间就由更短的3s决定。

3. 总结

  1. 可以为队列设置超时时间,队列中所有消息共享超时时间,通过map参数“x-message-ttl”实现
  2. 可以为队列中的消息单独设置超时时间,通过Basic.Publish命令时设置消息的属性实现
  3. 当消息在超时时间内没有被消费者消费,该条消息就会变成一条死信,队列中的死信不可能再被消费者消费,会从队列中抹去
  4. 不设置消息的TTL时,表示这条消息永不过期
  5. 设置消息的TTL为0时,除非可以直接将消息投递到消费者,否则会直接丢弃该消息

4.3 死信队列

4.4 延迟队列

4.5 优先级队列

4.6 持久化

4.7 消息丢失的几种情形

4.8 解决消息丢失的模式

1. 事务模式

2. confirm模式

4.9 小结

第五章 消息的幂等性

持续更新中。。。