拼命加载中~

RabbitMQ入门指南

简介

  • 本篇将仔细地入门RabbitMQ消息队列。

疑点梳理

  • RabbitMQ本质上是一个队列,一个FIFO的数据结构。
  • 消息队列之间传输的是字符串,而承载数据的pojo对象是可以转为JSON形式的字符串的,反过来JSON形式的字符串也可以转为对应的Map对象,这便是对象数据如何在消息队列中传输的要点。

入门实例

  • 入门实例是最简单的消息队列示例,生产者发布消息,通过消息队列,由消费者进行接收。

  • 本篇所有代码需要在已安装RabbitMQ的情况下运行。
  • 关于创建用户user及虚拟机virtual hosts的操作:

  • 最后为指定的用户dylan赋予访问指定虚拟机/lesson_01的权限:

1. 导入依赖

  • 创建普通的Maven项目即可。
<!-- rabbitmq消息队列 -->
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.9.0</version>
</dependency>
<!-- 连接日志门面slf4j -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.13.3</version>
</dependency>
<!-- 日志实现log4j2 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.13.3</version>
</dependency>

2. 相关配置

  • 日志实现log4j2配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="OFF" monitorInterval="5">
    <properties>
        <property name="pattern">[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l%c{36} - %m%n</property>
        <property name="logFile">d:/logs</property>
    </properties>

    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L - %m%n"/>
        </Console>

        <!-- 实际上未使用到文件输出 -->
        <File name="file" fileName="${logFile}/log4j2.log">
            <ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${pattern}"/>
        </File>
    </Appenders>

    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="console"/>
            <!--<AppenderRef ref="file"/>-->
        </Root>
    </Loggers>
</Configuration>
  • 消息队列RabbitMQ连接参数文件:
# 可以选择性将rabbitmq的连接参数放置在.properties中
host=127.0.0.1
port=5672
username=dylan
password=123456
virtualHost=/lesson_01

3. 工具类

  • 由于获取消息队列连接对象Connection的代码比较繁琐,我们可以使用RabbitmqUtils的方式获取,编写代码如下:
package cn.dylanphang.rabbitmq.util;

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

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

/**
 * @author dylan
 * @date 2020/12/13
 */
public class RabbitmqUtils {

    private static ConnectionFactory factory;

    static {
        try (InputStream is = RabbitmqUtils.class.getClassLoader().getResourceAsStream("rabbitmq.properties")) {

            Properties properties = new Properties();
            properties.load(is);

            String host = properties.getProperty("host");
            Integer port = Integer.valueOf(properties.getProperty("port"));
            String username = properties.getProperty("username");
            String password = properties.getProperty("password");
            String virtualHost = properties.getProperty("virtualHost");

            factory = new ConnectionFactory();
            factory.setHost(host);
            factory.setPort(port);
            factory.setUsername(username);
            factory.setPassword(password);
            factory.setVirtualHost(virtualHost);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() {
        try {
            return factory.newConnection();
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void close(Channel channel, Connection connection) {
        if (channel != null) {
            try {
                channel.close();
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4. 测试运行

  • 消息发布者provider
    1. 使用连接对象connection获取到的通道channel,其使用queueDeclare的方式声明了一个队列:
      1. 参数-1:队列名称,如果该队列不存在,会创建该队列,如果存在同名队列,会检查该队列是否符合要求,否则报错;
      2. 参数-2:是否可持久化,该参数决定了重启RabbitMQ服务器后,队列是否仍然存在;
      3. 参数-3:是否排他,即是否允许多个消费者连接到此队列,多数情况下使用false
      4. 参数-4:是否自动删除,当队列中不再有消息且无消费者连接到此队列时,是否自动删除此队列;
      5. 参数-5:未知。
    2. 其中basicPublish方法进行了消息的发布操作:
      1. 参数-1:交换机名称,如果不存在交换机exchange,则传入空字符串;
      2. 参数-2:队列名称;
      3. 参数-3:消息属性,指定发布消息的属性;
      4. 参数-4:以byte[]形式传输的消息。
package cn.dylanphang.rabbitmq.provide;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @author dylan
 * @date 2020/12/13
 */
public class SimpleProvider {
    public static void main(String[] args) throws IOException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();
        channel.queueDeclare("hello", false, false, false, null);

        // 3.发布消息
        channel.basicPublish("", "hello", null, "Here is lesson one.".getBytes(StandardCharsets.UTF_8));

        // 4.释放资源
        RabbitmqUtils.close(channel, connection);
    }
}
  • 消息接收者consumer
    1. 在此consumer中保留了原始为获取连接所需要编写的代码,作为参考;
    2. 使用queueDeclare时需要保证获取消息的队列hello拥有相同的参数配置;
    3. 其中basicConsume方法进行了消息的接收操作:
      1. 参数-1:队列名称;
      2. 参数-2:是否自动确认消息;
      3. 参数-3:消费者对象,用于完成处理消息等后续操作。
    4. 对于消费者来说,一般不需要对通道channel和连接connection进行资源释放操作。
package cn.dylanphang.rabbitmq.consume;

import com.rabbitmq.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * @author dylan
 * @date 2020/12/13
 */
public class Consumer {
    public final static Logger LOGGER = LoggerFactory.getLogger(Consumer.class);

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.获取连接工厂,并使用连接工厂设置主机地址、端口、用户名、密码及虚拟主机名称等信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        factory.setUsername("dylan");
        factory.setPassword("123456");
        factory.setVirtualHost("/lesson_01");

        // 2.使用连接工厂获取连接
        Connection connection = factory.newConnection();

        // 3.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();
        channel.queueDeclare("hello", false, false, false, null); // 当确认存在名称为hello的队列时,可以省略此行代码

        // 4.消费消息
        channel.basicConsume("hello", true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                LOGGER.info("Message that received: " + new String(body));
            }
        });

        // 5.释放资源,但对于消费者来说,一般不需要关闭资源,否则无法接收后续消息了
        // channel.close();
        // connection.close();
    }
}

5. SpringBoot

  • 每个小节末尾都会展示如何在SpringBoot中去完成本节的范例。

  • 项目依赖,在创建SpringBoot的时候勾选RabbitMQ的依赖,或手动添加:

    • 注:其中排除了原有的logging日志模块采用了log4j2,因此需要自行添加log4j2配置文件。
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
        <version>2.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
  • 以下附上log4j2.xmlapplication.yml文件的配置详情:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="OFF" monitorInterval="5">
    <properties>
        <property name="pattern">[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l%c{36} - %m%n</property>
        <property name="logFile">d:/logs</property>
    </properties>

    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%-5level] %c{36}:%L - %m%n"/>
        </Console>
    </Appenders>

    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="console"/>
        </Root>
    </Loggers>
</Configuration>
  • 入门案例中,不需要进行过多的配置,只需要配置rabbitmq的属性即可,务必自行创建虚拟机/rabbitmq-springboot
spring:
  application:
    name: rabbitmq-quickstart

  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: dylan
    password: 123456
    virtual-host: /rabbitmq-springboot
  • 消息生产者测试类:
    1. 其中RabbitTemplate模板对象是SpringBoot提供给我们的,可以直接使用该对象进行消息的发布;
    2. 方法convertAndSend中参数1为队列名称,参数2为消息主体。
package cn.dylanphang.rabbitmq;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void testQuickstart() {
        this.rabbitTemplate.convertAndSend("hello", "Quick Start.");
    }

}
  • 消息消费者:
    1. 注解@RabbitListener可以作用于类或方法上,用于队列声明或交换机绑定,目前只起到了队列声明的作用;
    2. 务必保证虚拟机中没有同名的队列存在,如果同名则需要保证相关配置参数一致,否则都会出现错误;
    3. 注解@RabbitHandler标注在方法上,表明该方法是一个消费者;
    4. 消费者方法中的形参String message为该消费者接收到的信息,这里不仅仅只能传入一个形参。
package cn.dylanphang.rabbitmq.listener.quickstart;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 使用注解@RabbitListener创建的队列默认情况下是持久化的、非独占的、非自动删除的队列。
 * 在确保没有同名队列存在的情况下,可以更改队列的参数。如果存在同名队列,会去使用该队列并忽视配置的参数。
 *
 * @author dylan
 * @date 2020/12/15
 */

@RabbitListener(queuesToDeclare = @Queue(name = "hello", durable = "true", exclusive = "false", autoDelete = "true"))
@Component
public class QuickStart {

    private static final Logger LOGGER = LoggerFactory.getLogger(QuickStart.class);

    /**
     * 如果类中仅有一个用于处理消息的消费者,可以将@RabbitListener的注解放置在类上,同时使用@Rabbithandler去标记该消费者。
     *
     * @param message 接收的消息
     */
    @RabbitHandler
    public void receive(String message) {
        LOGGER.info("Receive message: " + message);
    }
}
  • 测试结果:

工作队列(Work Queue)

  • 入门案例中,有且仅存在一个消费者。而消息队列允许多个消费者存在,它们就像是工人一样,会有序地在队列中获取消息。

  • 需要明确,每一个消费者Consumer都会有序地从队列中获取消息,消息是不会被重复分配到不同的消费者中的,即消息有且仅会被一个消费者消费。
  • 相关Maven依赖不再赘述,与此前一致,以下将直接展示相关RabbitMQ的代码。

1. 消息生产者

  • 消费者测试类代码如下:
    1. 关注到创建或获取队列hello时,我们使用的是可持久化队列,即参数2true
    2. 其次在basicPublish中的参数3设置为MessageProperties.PERSISTENT_TEXT_PLAIN,即消息文本为可持久化的;
    3. 以上两点必须同时设置,才能保证在重启RabbitMQ后,队列与队列中承载的所谓未处理消息仍然存在。
package cn.dylanphang.rabbitmq.provide;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @author dylan
 * @date 2020/12/13
 */
public class ProviderTest {
    private Connection connection;
    private Channel channel;

    private static final int RUN_TIMES = 20;

    @Test
    public void run() throws IOException {
        for (int i = 1; i <= RUN_TIMES; i++) {
            // *.发布消息
            channel.basicPublish(
                    "",
                    "hello",
                    MessageProperties.PERSISTENT_TEXT_PLAIN,
                    ("Here is ProviderTest." + i).getBytes(StandardCharsets.UTF_8)
            );
        }
    }

    @Before
    public void init() throws IOException {
        // 1.获取连接对象
        this.connection = RabbitmqUtils.getConnection();
        if (null == this.connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        this.channel = this.connection.createChannel();
        this.channel.queueDeclare("hello", true, false, false, null);
    }

    @After
    public void destroy() {
        // *.释放资源
        RabbitmqUtils.close(this.channel, this.connection);
    }
}
  • 运行代码后,可以看到RabbitMQ的管理中心Management里,队列选项卡中所示:
    1. 关注到features中被标注了字母D,表明该队列是持久化durable队列;
    2. 其次Ready中为20,即当前队列中存在20条未处理消息。

  • 但本次实验是需要先开启消费者端,如果先开启生产者端,积攒的消息会在任一消费者上线后被立即消费。实验存在两个消费者,如果前者消费速度较快,则后启动的消费者即使上线了,也可能无法获取到消息。
  • 另外是关于消息自动确认机制,后续会提及。消息的自动确认机制,会导致先上线的消费者即使消费速度较慢的情况下,也会获取当前队列中所有消息的处理权。

2. 消息消费者

  • 本次将使用ConsumerAConsumerB作为消息的消费者,代码如下:
    1. 关于channel.queueDeclare()的使用,如果此前已经存在了指定的队列hello,则在生产者或消费者的代码中,可以不显式地声明队列hello,生产者或消费者会默认去寻找同名队列并使用它;
    2. 假如在生产者或消费者运行前,不存在相关的同名队列,则需要显式地声明队列及其属性;
    3. 如果相关同名队列的属性不符合消费者的要求,则需要考虑让队列使用其他的名字。
package cn.dylanphang.rabbitmq.consume;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * @author dylan
 * @date 2020/12/13
 */
public class ConsumerX {
    public final static Logger LOGGER = LoggerFactory.getLogger(ConsumerA.class);

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();
        
        // 在确认存在符合要求的队列hello的情况下,可以省略队列声明代码
        // channel.queueDeclare("hello", true, false, false, null);

        // 3.消费消息
        channel.basicConsume("hello", true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                LOGGER.info("Message that received: " + new String(body));
            }
        });
    }
}
  • 两个消费者的代码是完全一致的,启动ConsumerAConsumerB,之后再运行ProviderTest,观察控制台输出:

  • 可以看到,消息被平均地发布到了ConsumerAConsumerB上。
  • RabbitMQ默认的消息分配机制是平均分配,即当有两个消费者在线上时,消息会平均且有序地分配到消费者中。

3. 处理数量与手动确认

  • 由于默认采用平均的方式分配消息,同时队列开启了自动确认消息已被处理的功能,会导致两个问题:
    1. 一旦某个消费者处理消息缓慢,则达不到最高的效率;
    2. 一旦某个消费者处理消息中宕机,因为已经自动确认消息被处理了,会导致消息数据的丢失。
  • 解决方法,需要同时关闭自动确认消息的功能且限制消费者一次处理消息的数量,同时由于关闭了消息的自动确认功能,需要在代码中编写手动的确认消息的代码:
    1. channel.basicQos(1);:限制消费者一次处理的消息数量;
    2. channel.basicConsume("hello", false, ...):关闭消息自动确认;
    3. channel.basicAck(envelope.getDeliveryTag(), false);:开启消息手动确认。
  • 修改消费者代码,其中ConsumerA中模拟处理缓慢的情况,而ConsumerB不变,代码如下:
    1. 如果只开启了一次处理消息数量的限制,但未关闭消息自动确认,则消费者仍会在消息收到的一瞬间自动确认该消息已被处理并准备好接收下一条消息,相当于没有进行任何设置;
    2. 如果只关闭了自动确认,同时开启手动确认,由于未限制消费者处理消息的数据量,RabbitMQ仍然会根据平均的原则,将消息分配给消费者,即Ready显示为0,控制台中会看到Unacked数目缓慢降低的情况,即处理缓慢;
    3. 综上,即需要同时限制消费者一次处理消息的数量,同时关闭自动确认功能,开启手动确认,才能达到最高的效率,此时控制台中表现为ReadyUnacked均为0的情况,而实际中性能较好的消费者处理了更多的消息。
package cn.dylanphang.rabbitmq.consume;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * @author dylan
 * @date 2020/12/13
 */
public class ConsumerA {
    public final static Logger LOGGER = LoggerFactory.getLogger(ConsumerA.class);

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();

        // 3.设置此消费者每次仅处理一条消息
        channel.basicQos(1);

        // 4.消费消息
        channel.basicConsume("hello", false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // *.模拟处理缓慢的情况
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LOGGER.info("Message that received: " + new String(body));

                // 5.手动确认
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });
    }
}
package cn.dylanphang.rabbitmq.consume;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * @author dylan
 * @date 2020/12/13
 */
public class ConsumerB {
    public final static Logger LOGGER = LoggerFactory.getLogger(ConsumerB.class);

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();

        // 3.设置此消费者每次仅处理一条消息
        channel.basicQos(1);

        // 4.消费消息
        channel.basicConsume("hello", false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                LOGGER.info("Message that received: " + new String(body));

                // 5.手动确认
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });
    }
}
  • 再次启动ConsumerAConsumerB,之后运行ProviderTest,观察控制台输出:
    • 注:此处修改了控制台日志输出的格式,去除了线程名称信息的显示。

  • 可以看到,消息不再是平均分配,并且效率更高的ConsumerB处理了更多的消息。

4. 本节小结

  • 关于队列的声明queueDeclare(),如果在生产者或消费者运行前,存在同名且符合要求的队列,则在代码中可以省略该声明;如果存在的同名队列不符合要求或不存在同名队列,则需要进行显式地队列声明,队列不可与已存在的队列同名。
  • 工作队列拥有多个消费者Consumer,默认情况下Provider会根据公平的原则进行消息的分配。
  • 如果希望更有效率地进行消息的消费,则需要进行配置:
    1. 让消费者每次仅处理一条消息;
    2. 关闭消息自动确认,改为手动确认。

5. SpringBoot

  • 此小小节将演示公平分配的情况下SpringBoot如何配置,以及追求效率的情况下SpringBoot应该如何配置。

1. 默认分配

  • RabbitMQ默认情况下的消息分配是采用平均的机制,关于日志配置等不再赘述,以下将直接贴代码及部分讲解排雷。
  • application.yml配置如下:
spring:
  application:
    name: rabbitmq-quickstart

  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: dylan
    password: 123456
    virtual-host: /rabbitmq-springboot
  • 消息生产者测试类:
    1. 务必保证名为work的队列是存在的,如果不存在则会默认创建该队列,队列默认是可持久化、非排它、非自动删除队列;
    2. @Test中,如果不存在该队列,是不会自动创建的,实际生产过程中生产者是不会在测试类中的。
package cn.dylanphang.rabbitmq;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void testWorkQueue() {
        for (int i = 1; i <= 20; i++) {
            this.rabbitTemplate.convertAndSend("work", "Work Queue." + i);
        }
    }

}
  • 消息消费者:
    • 当存在两个以上消费者时,点对点的消息发布,注解@RabbitListener需要写在方法上。
package cn.dylanphang.rabbitmq.listener.workqueue;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 使用工作队列模式,需要将@RabbitListener标记在方法上。
 *
 * @author dylan
 * @date 2020/12/15
 */
@Component
public class WorkQueue {

    private static final Logger LOGGER = LoggerFactory.getLogger(WorkQueue.class);

    @RabbitListener(queuesToDeclare = @Queue("work"))
    public void workerA(String message) {
        LOGGER.info("WorkerA receive message: " + message);
    }

    @RabbitListener(queuesToDeclare = @Queue("work"))
    public void workerB(String message) {
        LOGGER.info("WorkerB receive message: " + message);
    }
}
  • 测试结果:

2. 手动确认

  • 从上小节中,不难看出默认分配是遵循轮询原则的。
  • 此时如果WorkerA中存在延时3秒的操作,我们希望在空闲时WorkerB能够继续工作,则需要手动确认消息。
  • 此时需要更改application.yml配置如下:
    1. prefetch:每次从生产者中取用消息的条目;
    2. acknowledge-mode:消息确认模式,选择手动manual
spring:
  application:
    name: rabbitmq-quickstart

  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: dylan
    password: 123456
    virtual-host: /rabbitmq-springboot

    listener:
      simple:
        prefetch: 1
        acknowledge-mode: manual
  • 消息生产者测试类:
    • 同样务必保证work2队列的可用性。
package cn.dylanphang.rabbitmq;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void testWorkQueue2() {
        for (int i = 1; i <= 20; i++) {
            this.rabbitTemplate.convertAndSend("work2", "Work Queue." + i);
        }
    }

}
  • 消息消费者:
    1. WorkerA中我们设置了程序睡眠3000毫秒,模拟处理缓慢的情况;
    2. 此时消费者的形参发生了变化,因为手动确认需要使用Channel对象,因此形参中添加了Channel channel
    3. channel对象使用basicAck需要获取deliveryTay,通过在形参中添加Message message对象获取;
    4. 此时消息主体为String msg,方法basicAck的使用与此前无异;
    5. 所有的Consumer都务必在开启了手动确认消息的时候,进行手动确认消息代码的编写。如果无手动确认,RabbitMQ将任务该消息没有被处理,在消费者结束后,消息将回到生产者中。
package cn.dylanphang.rabbitmq.listener.workqueue;

import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.tags.MessageTag;

import java.io.IOException;

/**
 * 使用工作队列模式,需要将@RabbitListener标记在方法上。
 *
 * @author dylan
 * @date 2020/12/15
 */
@Component
public class WorkQueue2 {

    private static final Logger LOGGER = LoggerFactory.getLogger(WorkQueue2.class);

    @RabbitListener(queuesToDeclare = @Queue("work2"))
    public void workerA(Message message, Channel channel, String msg) throws InterruptedException, IOException {
        Thread.sleep(3000);
        LOGGER.info("WorkerA receive message: " + msg);

        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }

    @RabbitListener(queuesToDeclare = @Queue("work2"))
    public void workerB(Message message, Channel channel, String msg) throws IOException {
        LOGGER.info("WorkerB receive message: " + msg);

        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}
  • 测试结果:

发布订阅(Publish/Subscribe)

  • 发布订阅模型类似于消费者去订阅生产者的消息,只有订阅的消息的消费者,才能接收到消息。

  • 此时,消息发布者不再直接将消息发布到队列中,取而代之将消息发送到交换机中exchange,而消费者只需要订阅相关的交换机,并创建自己的临时队列用于从交换机中接收消息即可。

  • 本节需要明确三点:
    1. 所有订阅了交换机exchange的消费者Consumer都能收到生产者Provider发布到交换机exchage中的消息,即在发布订阅模型中,在线的所有消费者在生产者发布了一条消息后,都能收到该消息,消息会被多个消费者进行消费;
    2. 生产者Provider只会将消息发布到交换机exchange中;
    3. 消费者Consumer消费的消息,需要从交换机exchange中获取,消费者需要提供临时队列用于消息的接收。

1. 消息生产者

  • 消费者测试类代码如下:
    1. 由于生产者不再直接与队列进行联系,取而代之的是与交换机进行联系,因此在@Before中的队列声明代码修改为了交换机声明代码,同样的如果已存在符合要求的交换机,那么exchageDeclare可以省略;
    2. 方法exchageDeclare中的参数2为交换机类型,选择的类型为fanout模式,使用发布订阅模型需要使用该模式;
    3. basicPublish中,参数1需要提供目标交换机名称,而参数2中的routingKey留空即可,在FANOUT模式中,其值无任何影响。
package cn.dylanphang.rabbitmq.provide;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @author dylan
 * @date 2020/12/13
 */
public class ProviderTest {
    private Connection connection;
    private Channel channel;

    private static final int RUN_TIMES = 10;

    @Test
    public void run() throws IOException {
        for (int i = 1; i <= RUN_TIMES; i++) {
            // *.发布消息
            channel.basicPublish(
                    "orders",
                    "",
                    null,
                    ("Here is FANOUT." + i).getBytes(StandardCharsets.UTF_8)
            );
        }
    }

    @Before
    public void init() throws IOException {
        // 1.获取连接对象
        this.connection = RabbitmqUtils.getConnection();
        if (null == this.connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的交换机exchange
        this.channel = this.connection.createChannel();
        this.channel.exchangeDeclare("orders", BuiltinExchangeType.FANOUT);
    }

    @After
    public void destroy() {
        // *.释放资源
        RabbitmqUtils.close(this.channel, this.connection);
    }
}

2. 消息消费者

  • 本次将使用ConsumerAConsumerB作为消息的消费者,代码如下:
    1. 发布订阅模型中,消费者需要使用临时的队列,不需要再显式地声明一个队列,channel.queueDeclare().getQueue()可以创建一个临时队列,并返回该临时队列的名称;
    2. 由于一般情况下是先从消费者开始运行的,因此channel.exchangeDeclare("orders", BuiltinExchangeType.FANOUT);交换机声明代码一般需要编写,在实际中如果存在该符合要求的交换机,可以省略该代码;
    3. 当交换机exchange与临时队列queue均准备完毕后,需要将它们进行绑定的操作,使用方法channel.queueBind()并提供临时队列的名称及交换机的名称即可,参数3routingKey,在FANOUT模式中无作用,留空即可;
    4. 最后,消费消息的方法basicCousume中需要传入的队列名称为临时队列的名称queueName
package cn.dylanphang.rabbitmq.consume;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
 * @author dylan
 * @date 2020/12/13
 */
public class ConsumerA {
    public final static Logger LOGGER = LoggerFactory.getLogger(ConsumerA.class);

    public static void main(String[] args) throws IOException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();

        // 3.创建exchange交换机
        channel.exchangeDeclare("orders", BuiltinExchangeType.FANOUT);

        // 4.创建临时队列并获取队列名称
        final String queueName = channel.queueDeclare().getQueue();

        // 5.将临时队列绑定到指定的exchange中
        channel.queueBind(queueName, "orders", "");

        // 6.设置此消费者每次仅处理一条消息
        channel.basicQos(1);

        // 7.消费消息
        channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                LOGGER.info("Message that received: " + new String(body));

                // 8.手动确认
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });
    }
}
  • 注意,消费者CousmerB中没有显式声明交换机,因为CousmerA会先启动,此时RabbitMQ中已经存在符合要求的且其名称为order的交换机,ConsumerB会自动使用该交换机,并绑定它的临时队列。
package cn.dylanphang.rabbitmq.consume;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
 * @author dylan
 * @date 2020/12/13
 */
public class ConsumerB {
    public final static Logger LOGGER = LoggerFactory.getLogger(ConsumerB.class);

    public static void main(String[] args) throws IOException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();

        // 3.创建临时队列并获取队列名称
        final String queueName = channel.queueDeclare().getQueue();

        // 4.将临时队列绑定到指定的exchange中
        channel.queueBind(queueName, "orders", "");

        // 5.设置此消费者每次仅处理一条消息
        channel.basicQos(1);

        // 6.消费消息
        channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                LOGGER.info("Message that received: " + new String(body));

                // 7.手动确认
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });
    }
}
  • 先启动ConsumerA后在启动ConsumerB,最后启动ProviderTest查看控制台输出:

  • 与预期一致,ConsumerAConsumerB都能接收到来自于ProviderTest所发布的消息。

3. 临时队列

  • 在发布订阅模型中,消费者使用的是临时队列,在RabbitMQ的管理界面中,可以看到该临时队列的信息:

  • 此时可以留意到队列中的Features
    • ADauto-delete: true,在没有任何消费者使用此队列的时候,该队列将自动删除;
    • Exclexclusive: true,该队列有且仅允许一个连接。
  • 进入该临时队列查看相关的Bindings信息:

  • 可以看到该队列是与名为orders的交换机进行绑定的。
  • 那么,是否可以手动创建一个可持久化的队列呢?这样即使在Consumer不在线的情况下,消息也能被发布到其队列中,在Consumer上线时便可以获取“历史消息”了。
  • 修改ConsumerB中的代码,不再获取临时队列,使用队列声明创建一个可持久化的、排它的、不自动删除的队列:
package cn.dylanphang.rabbitmq.consume;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
 * @author dylan
 * @date 2020/12/13
 */
public class ConsumerB {
    public final static Logger LOGGER = LoggerFactory.getLogger(ConsumerB.class);

    public static void main(String[] args) throws IOException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();

        // 3.创建持久化队列,但最终不会影响durableQueue是一个临时队列的事实
        String queueName = "durableQueue";
        channel.queueDeclare(queueName, false, true, false, null);

        // 4.将临时队列绑定到指定的exchange中
        channel.queueBind(queueName, "orders", "");

        // 5.设置此消费者每次仅处理一条消息
        channel.basicQos(1);

        // 6.消费消息
        channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                LOGGER.info("Message that received: " + new String(body));

                // 7.手动确认
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });
    }
}
  • 此时启动ConsumerAConsumerB,观察RabbitMQ管理界面中的队列详情:

  • 我们创建的队列似乎已经成功实现了不被自动删除的特性,此时关闭ConsumerB,观察队列详情:

  • 队列居然还是被自动删除了。
  • 实验证明自定义的队列即使声明了队列不会被自动删除,但如果队列连接的是一个交换机模式为fanoutexchange,那么该队列仍具有auto-delete: true的特性,而无视实际中代码的设定。
  • 另外,queueDeclare中的durable参数的设定对队列也是没有影响的,无论怎样设置队列,该队列都是一个临时队列。

4. 本节小结

  • 发布订阅模型中,交换机的类型为FANOUT
  • 不同于工作队列,发布订阅模型中所有在线且订阅了exchange的消费者都能接收到来自于生产者的消息。
  • 关于交换机声明exchageDeclare,如果存在符合要求的同名交换机,可以省略交换机声明。
  • 关于队列声明queueDeclare,其中的参数设定不会影响该队列最终仍是一个临时队列的事实。

5. SpringBoot

  • 本小节及此后的SpringBoot范例将不采用手动确认的方式以减少繁琐的代码。

  • 此时application.yml文件配置如下:

spring:
  application:
    name: rabbitmq-quickstart

  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: dylan
    password: 123456
    virtual-host: /rabbitmq-springboot
  • 消息生产者测试类:
    1. 参数1为交换机名称;
    2. 参数2routingKey,在fanout模式中不需要routingKey,即留空;
    3. 参数3为消息主体。
package cn.dylanphang.rabbitmq;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void testBroadcast() {
        for (int i = 1; i <= 10; i++) {
            this.rabbitTemplate.convertAndSend("logs", "", "Broadcast fanout." + i);
        }
    }
}
  • 消息消费者:
    1. 临时队列的创建使用无参数的注解@Queue即可;
    2. 交换机的声明使用注解@Exchange
    3. 绑定队列与交换机则使用注解@QueueBingding,在fanout模式下不需要提供routingKey
package cn.dylanphang.rabbitmq.listener.broadcast;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @author dylan
 * @date 2020/12/15
 */
@Component
public class Broadcast {
    private static final Logger LOGGER = LoggerFactory.getLogger(Broadcast.class);
    private static final String EXCHANGE_NAME = "logs";

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue, // 无参数表示为临时队列
                    exchange = @Exchange(name = EXCHANGE_NAME, type = ExchangeTypes.FANOUT)
            )
    })
    public void receiverA(String message) {
        LOGGER.info("ReceiverA receive message: " + message);
    }

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue, // 无参数表示为临时队列
                    exchange = @Exchange(name = EXCHANGE_NAME, type = ExchangeTypes.FANOUT)
            )
    })
    public void receiverB(String message) {
        LOGGER.info("ReceiverB receive message: " + message);
    }
}
  • 测试结果:

路由模式(Routing)

  • 路由模式在发布订阅模型的基础上,通过routingKey的方式完成了消费者的筛选接收,即消费者可以通过指定routingKey的方式选择性接收消息,生产者也可以通过指定routingKey的方式选择性地向部分消费者发送消息。
  • 此时使用的exchangeType不再是FANOUT,而是DIRECT
    1. 生产者需要在发布消息的时候指定该消息的routingKey
    2. 消费者需要在绑定交换机的时候指定接收的消息应具备的routingKey,同一个消费者可以绑定多个routingKey

  • 本节需要明确三点:
    1. 路由模式所使用的交换机类型为DIRECT
    2. 生产者需要在消息发布的时候,需要指定routingKey
    3. 消费者在绑定交换机的时候,需要需要指定routingKey,同一个消费者可以绑定多个routingKey

1. 消息生产者

  • 消费者测试类代码如下:
    1. 代码模拟日志记录的同时,将相关日志发送到交换机中,消费者将根据不同等级的routingKey接收并处理相应的消息,例如将日志信息写入ElasticSearch中,用于后续展示;
    2. 需要注意的地方是,声明交换机exchangeDeclare时,所选用的exchangeTypeDIRECT
    3. 其次,每次发布消息时,需要指定参数2routingKey值,消费者将根据该值判断是否接收处理此条消息。
package cn.dylanphang.rabbitmq.provide;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Random;

/**
 * @author dylan
 * @date 2020/12/15
 */
public class ProviderTest {
    private static final Logger LOGGER = LoggerFactory.getLogger(ProviderTest.class);
    private static final int RUN_TIMES = 30;
    private static final String EXCHANGE_NAME = "logs";

    private Connection connection;
    private Channel channel;

    @Test
    public void run() throws IOException {
        final Random random = new Random();
        for (int i = 1; i <= RUN_TIMES; i++) {
            final int num = random.nextInt(3);

            if (num == 0) {
                this.sendMessage("info");
            } else if (num == 1) {
                this.sendMessage("warn");
            } else if (num == 2) {
                this.sendMessage("error");
            }
        }
    }

    private void sendMessage(String level) throws IOException {
        // 1.判断记录并记录日志
        String transferMessage = "";
        String transferLevel = "";

        if (Level.INFO.getLevel().equalsIgnoreCase(level)) {
            // *.需要发布的信息
            transferMessage = "[INFO ] Something happen.";
            transferLevel = Level.INFO.getLevel();
            // *.记录日志
            LOGGER.info("Something happen. Record {} message. Same time send to MQ.", "[INFO ]");

        } else if (Level.WARN.getLevel().equalsIgnoreCase(level)) {
            // *.需要发布的信息
            transferMessage = "[WARN ] Something happen.";
            transferLevel = Level.WARN.getLevel();
            // *.记录日志
            LOGGER.warn("Something happen. Record {} message. Same time send to MQ.", "[WARN ]");

        } else if (Level.ERROR.getLevel().equalsIgnoreCase(level)) {
            // *.需要发布的信息
            transferMessage = "[ERROR] Something happen.";
            transferLevel = Level.ERROR.getLevel();
            // *.记录日志
            LOGGER.error("Something happen. Record {} message. Same time send to MQ.", "[ERROR]");

        }

        // 2.发布消息
        this.channel.basicPublish(
                EXCHANGE_NAME,
                transferLevel,
                null,
                transferMessage.getBytes(StandardCharsets.UTF_8)
        );
    }

    @Before
    public void init() throws IOException {
        // 1.获取连接对象
        this.connection = RabbitmqUtils.getConnection();
        if (null == this.connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的交换机exchange
        this.channel = this.connection.createChannel();
        this.channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
    }

    @After
    public void destroy() {
        // *.释放资源
        RabbitmqUtils.close(this.channel, this.connection);
    }

    public enum Level {
        /**
         * 日志级别,Level.INFO返回的是一个Level对象,该对象使用构造器创建,参数即为括号中的默认日志级别字符串。
         */
        INFO("info"), WARN("warn"), ERROR("error");

        private final String level;

        Level(String level) {
            this.level = level;
        }

        public String getLevel() {
            return this.level;
        }
    }
}

2. 消息消费者

  • 本次将使用ConsumerAConsumerB作为消息的消费者,代码如下:
    1. 消费者ConsumerA中,queueBind()方法中指定了三个不同的routingKey,表明该Consumer将处理INFOWARNERROR等消息;
    2. 消费者ConsumerB中,queueBind()方法中仅指定了一个routingKey,表明该Consumer仅处理ERROR的消息。
package cn.dylanphang.rabbitmq.consume;

import cn.dylanphang.rabbitmq.provide.ProviderTest;
import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @author dylan
 * @date 2020/12/15
 */
public class ConsumerA {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();

        // 3.创建exchange交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        // 4.创建临时队列并获取队列名称
        final String queueName = channel.queueDeclare().getQueue();

        // 5.将临时队列绑定到指定的exchange中,并指定routingKey的值
        channel.queueBind(queueName, EXCHANGE_NAME, ProviderTest.Level.INFO.getLevel());
        channel.queueBind(queueName, EXCHANGE_NAME, ProviderTest.Level.WARN.getLevel());
        channel.queueBind(queueName, EXCHANGE_NAME, ProviderTest.Level.ERROR.getLevel());

        // 6.设置此消费者每次仅处理一条消息
        channel.basicQos(1);

        // 7.消费消息
        channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println(("Message that received: " + new String(body)) + " Might handle by some actions.");

                // 8.手动确认
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });
    }
}
package cn.dylanphang.rabbitmq.consume;

import cn.dylanphang.rabbitmq.provide.ProviderTest;
import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @author dylan
 * @date 2020/12/15
 */
public class ConsumerB {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();

        // 3.创建临时队列并获取队列名称
        final String queueName = channel.queueDeclare().getQueue();

        // 4.将临时队列绑定到指定的exchange中,并指定routingKey的值
        channel.queueBind(queueName, EXCHANGE_NAME, ProviderTest.Level.ERROR.getLevel());

        // 5.设置此消费者每次仅处理一条消息
        channel.basicQos(1);

        // 6.消费消息
        channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println(("Message that received: " + new String(body)) + " Might handle by some actions.");

                // 7.手动确认
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });
    }
}
  • 先启动ConsumerA后在启动ConsumerB,最后启动ProviderTest查看控制台输出:
  • ProviderTest中可以看到所有的日志输出信息:

  • ConsumerA订阅的三种类型INFOWARNERROR消息:

  • ConsumerB订阅的一种类型ERROR消息:

  • 不难看出,通过routingKey的方式,可以实现生产者对发布消息的筛选或消费者对接收消息的筛选。

3. 本节小结

  • 路由模式中,交换机的类型为DIRECT
  • 其主要是在发布订阅模型的基础上,让生产者可以使用routingKey进行消息的定向发布,让消费者可以使用routingKey进行消息的定向接收。
  • 关于队列声明queueDeclare,其中的参数设定不会影响该队列最终仍是一个临时队列的事实。

4. SpringBoot

  • application.yml文件配置:
spring:
  application:
    name: rabbitmq-quickstart

  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: dylan
    password: 123456
    virtual-host: /rabbitmq-springboot
  • 消息生产者测试类:
package cn.dylanphang.rabbitmq;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void testRouter() {
        for (int i = 1; i <= 10; i++) {
            this.rabbitTemplate.convertAndSend("logs2", "info", "[INFO ]Router direct." + i);
        }
        for (int i = 1; i <= 10; i++) {
            this.rabbitTemplate.convertAndSend("logs2", "warn", "[WARN ]Router direct." + i);
        }
        for (int i = 1; i <= 10; i++) {
            this.rabbitTemplate.convertAndSend("logs2", "error", "[ERROR]Router direct." + i);
        }
    }

}
  • 消息消费者:
    • 当交换机exchange模式为direct的时候,可以省略@Exchange的参数type
package cn.dylanphang.rabbitmq.listener.router;

import cn.dylanphang.rabbitmq.listener.broadcast.Broadcast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @author dylan
 * @date 2020/12/15
 */
@Component
public class Router {

    private static final Logger LOGGER = LoggerFactory.getLogger(Broadcast.class);
    private static final String EXCHANGE_NAME = "logs2";

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue, // 无参数表示为临时队列
                    exchange = @Exchange(name = EXCHANGE_NAME),
                    key = {"info", "warn", "error"}
            )
    })
    public void receiverA(String message) {
        LOGGER.info("ReceiverA receive message: " + message);
    }

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue, // 无参数表示为临时队列
                    exchange = @Exchange(name = EXCHANGE_NAME),
                    key = {"error"}
            )
    })
    public void receiverB(String message) {
        LOGGER.info("ReceiverB receive message: " + message);
    }
}
  • 测试结果:

动态路由(Topic)

  • 动态路由顾名思义,是一种相较于路由模式来说的更为灵活的订阅方式。
  • 动态路由使用的交换机类型为TOPIC

  • 它提供两种类型的通配符:
    1. *:匹配一个单词;
    2. #:匹配一个或多个单词。
  • 动态路由更倾向于消费者中的routingKey配置,举例当交换机类型为DIRECT时:
    • 生产者向交换机发布了三条routingKey分别为user.saveinfo.deleteuser.find.getUsername时,如果存在一个Consumer需要同时订阅这三条消息,则需要编写三条不同的queueBind()代码。
  • 此时如果使用动态路由,交换机类型修改为TOPIC,则:
    • 生产者向交换机发布了三条routingKey分别为user.saveinfo.deleteuser.find.getUsername时,如果存在一个Consumer需要同时订阅这三条消息,此时不需要再编写三条代码;
    • 生产者只需要编写routingKeyuser.#info.deletequeueBind()代码即可。
  • 想象一下,如果存在大量的以user.开头的信息,但某个Consumer需要全部接收处理时,动态路由可以大大压缩代码量。

1. 消息生产者

  • 消费者测试类代码如下:
    1. 代码仍旧使用路由模式中的例子,其中交换机名称更改为logs2,如果不想更改交换机名称,需要进入RabbitMQ管理界面手动清除名为logsexchange后,再运行代码;
    2. 因为exchangeDeclare方法如果再次声明一个名为logs的交换机,将检查是否有同名交换机存在,此时会发现路由模式中使用的模式为DIRECT的交换机,此时模式不一致会默认抛出异常;
    3. 同时对枚举类及sendMessage中的参数进行调整,增加前缀level.
package cn.dylanphang.rabbitmq.provide;

import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Random;

/**
 * @author dylan
 * @date 2020/12/15
 */
public class ProviderTest {
    private static final Logger LOGGER = LoggerFactory.getLogger(ProviderTest.class);
    private static final int RUN_TIMES = 30;
    private static final String EXCHANGE_NAME = "logs2";

    private Connection connection;
    private Channel channel;


    @Test
    public void run() throws IOException {
        final Random random = new Random();
        for (int i = 1; i <= RUN_TIMES; i++) {
            final int num = random.nextInt(3);

            if (num == 0) {
                this.sendMessage("level.info");
            } else if (num == 1) {
                this.sendMessage("level.warn");
            } else if (num == 2) {
                this.sendMessage("level.error");
            }
        }
    }

    private void sendMessage(String level) throws IOException {
        // 1.判断记录并记录日志
        String transferMessage = "";
        String transferLevel = "";

        if (Level.INFO.getLevel().equalsIgnoreCase(level)) {
            // *.需要发布的信息
            transferMessage = "[INFO ] Something happen.";
            transferLevel = Level.INFO.getLevel();
            // *.记录日志
            LOGGER.info("Something happen. Record {} message. Same time send to MQ.", "[INFO ]");

        } else if (Level.WARN.getLevel().equalsIgnoreCase(level)) {
            // *.需要发布的信息
            transferMessage = "[WARN ] Something happen.";
            transferLevel = Level.WARN.getLevel();
            // *.记录日志
            LOGGER.warn("Something happen. Record {} message. Same time send to MQ.", "[WARN ]");

        } else if (Level.ERROR.getLevel().equalsIgnoreCase(level)) {
            // *.需要发布的信息
            transferMessage = "[ERROR] Something happen.";
            transferLevel = Level.ERROR.getLevel();
            // *.记录日志
            LOGGER.error("Something happen. Record {} message. Same time send to MQ.", "[ERROR]");

        }

        // 2.发布消息
        this.channel.basicPublish(
                EXCHANGE_NAME,
                transferLevel,
                null,
                transferMessage.getBytes(StandardCharsets.UTF_8)
        );
    }

    @Before
    public void init() throws IOException {
        // 1.获取连接对象
        this.connection = RabbitmqUtils.getConnection();
        if (null == this.connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的交换机exchange
        this.channel = this.connection.createChannel();
        this.channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
    }

    @After
    public void destroy() {
        // *.释放资源
        RabbitmqUtils.close(this.channel, this.connection);
    }

    public enum Level {
        /**
         * 日志级别,Level.INFO返回的是一个Level对象,该对象使用构造器创建,参数即为括号中的默认日志级别字符串。
         */
        INFO("level.info"), WARN("level.warn"), ERROR("level.error");

        private final String level;

        Level(String level) {
            this.level = level;
        }

        public String getLevel() {
            return this.level;
        }
    }
}

2. 消息消费者

  • 本次将使用ConsumerAConsumerB作为消息的消费者,代码如下:
    1. 其中ConsumerAqueueBind绑定的routingKeylevel.*,而ConsumerB中不变仍旧为level.error
    2. 两个消费者都需要将交换机的名称更改为logs2
package cn.dylanphang.rabbitmq.consume;

import cn.dylanphang.rabbitmq.provide.ProviderTest;
import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @author dylan
 * @date 2020/12/15
 */
public class ConsumerA {
    private static final String EXCHANGE_NAME = "logs2";

    public static void main(String[] args) throws IOException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();

        // 3.创建exchange交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

        // 4.创建临时队列并获取队列名称
        final String queueName = channel.queueDeclare().getQueue();

        // 5.将临时队列绑定到指定的exchange中,并指定routingKey的值
        channel.queueBind(queueName, EXCHANGE_NAME, "level.*");

        // 6.设置此消费者每次仅处理一条消息
        channel.basicQos(1);

        // 7.消费消息
        channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println(("Message that received: " + new String(body)) + " Might handle by some actions.");

                // 8.手动确认
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });
    }
}
package cn.dylanphang.rabbitmq.consume;

import cn.dylanphang.rabbitmq.provide.ProviderTest;
import cn.dylanphang.rabbitmq.util.RabbitmqUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @author dylan
 * @date 2020/12/15
 */
public class ConsumerB {
    private static final String EXCHANGE_NAME = "logs2";

    public static void main(String[] args) throws IOException {
        // 1.获取连接对象
        Connection connection = RabbitmqUtils.getConnection();
        if (null == connection) {
            return;
        }

        // 2.使用连接对象创建通道,并指定对应的队列queue
        Channel channel = connection.createChannel();

        // 3.创建临时队列并获取队列名称
        final String queueName = channel.queueDeclare().getQueue();

        // 4.将临时队列绑定到指定的exchange中,并指定routingKey的值
        channel.queueBind(queueName, EXCHANGE_NAME, ProviderTest.Level.ERROR.getLevel());

        // 5.设置此消费者每次仅处理一条消息
        channel.basicQos(1);

        // 6.消费消息
        channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println(("Message that received: " + new String(body)) + " Might handle by some actions.");

                // 7.手动确认
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });
    }
}
  • 先启动ConsumerA后在启动ConsumerB,最后启动ProviderTest查看控制台输出:

  • ProviderTest中可以看到所有的日志输出信息:

  • ConsumerA同样完成了三种类型INFOWARNERROR消息的订阅:

  • ConsumerB完成了一种类型ERROR消息的订阅,证明同时也是支持类似于路由模式的订阅模型:

3. 本节小结

  • 动态路由使用的交换机类型为TOPIC
  • 动态路由是路由模式的一种扩展,其主要为了解决消费者对多种消息订阅导致代码冗长的问题。
  • 其提供两种通配符:
    1. *:匹配一个单词;
    2. #:匹配一个或多个单词。

4. SpringBoot

  • application.yml文件配置:
spring:
  application:
    name: rabbitmq-quickstart

  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: dylan
    password: 123456
    virtual-host: /rabbitmq-springboot
  • 消息生产者测试类:
package cn.dylanphang.rabbitmq;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void testDynamicRouting() {
        for (int i = 1; i <= 10; i++) {
            this.rabbitTemplate.convertAndSend("logs3", "info", "[INFO ]Router direct." + i);
        }
        for (int i = 1; i <= 10; i++) {
            this.rabbitTemplate.convertAndSend("logs3", "warn", "[WARN ]Router direct." + i);
        }
        for (int i = 1; i <= 10; i++) {
            this.rabbitTemplate.convertAndSend("logs3", "error", "[ERROR]Router direct." + i);
        }
    }
}
  • 消息消费者:
    1. 将注解@Exchange中的type属性改为topic即可;
    2. 此时@QueueBinding中的key属性支持通配符的形式。
package cn.dylanphang.rabbitmq.listener.dynamic;

import cn.dylanphang.rabbitmq.listener.broadcast.Broadcast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @author dylan
 * @date 2020/12/15
 */
@Component
public class DynamicRouting {

    private static final Logger LOGGER = LoggerFactory.getLogger(Broadcast.class);
    private static final String EXCHANGE_NAME = "logs3";

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue, // 无参数表示为临时队列
                    exchange = @Exchange(name = EXCHANGE_NAME, type = ExchangeTypes.TOPIC),
                    key = {"*"}
            )
    })
    public void receiverA(String message) {
        LOGGER.info("ReceiverA receive message: " + message);
    }

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue, // 无参数表示为临时队列
                    exchange = @Exchange(name = EXCHANGE_NAME, type = ExchangeTypes.TOPIC),
                    key = {"error"}
            )
    })
    public void receiverB(String message) {
        LOGGER.info("ReceiverB receive message: " + message);
    }
}
  • 测试结果:

应用场景

  • 本小节是关于RabbitMQ的应用场景,关于在什么时候使用RabbitMQ和它能带来什么效果。

1. 异步处理

  • 场景说明:用户注册后,需要发送注册邮件和注册短信,传统的做法有两种:
    1. 串行方式;
    2. 并行方式。
  • 串行方式:将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件、短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西。

  • 并行方式:将注册信息写入数据库后,发送邮件的同时发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。

  • 消息队列:引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理。

2. 应用解耦

  • 场景说明:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口。

  • 这种做法有两个缺点:
    1. 当库存系统出现故障时,订单就会失败;
    2. 订单系统和库存系统高耦合。
  • 订单一般都需要确认拥有库存的情况下,成功扣取库存后,再响应用户的请求。在这里如果使用消息队列,可能的场景应该是在Redis中同步了相关商品的库存,通过查询Redis得知是否有库存。
  • 无库存的情况下返回订单成功是不合理的。
  • 此时引入消息队列:

  • 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,并与库存系统无关的库存记录后,响应用户订单成功;
  • 库存系统:订阅下单成功的消息,获取下单消息进行库操作。
  • 个人理解:用户所能看到的库存数量始终不是直接从库存系统上查询获取的。

3. 流量削峰

  • 场景说明:秒杀活动,一般会因为流量过大,导致应用宕机。一般可以在应用前端加入消息队列,缓冲大量的请求。
  • 作用:
    1. 可以控制活动人数,超过一定的阈值,订单将直接丢弃;
    2. 可以防止短时间的高流量压垮应用,应用程序按自己的最大出力能力获取订单。

  • 用户请求:服务器收到之后,将请求写入消息队列,假如消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面;秒杀业务根据消息队列中的请求信息,再做后续处理。

总结

  • 关于消息队列,较为常用的模式是Topic,其可以理解为Dircet模式的增强版。
  • RabbitMQ的特性是会自动使用同名同配置的交换机或临时队列。因此交换机exchange与临时队列queue常常在SpringBoot中被归为Bean对象,以便在系统启动之初,就在RabbitMQ中创建指定名称的交换机和临时队列。
  • 此时如果其他代码需使用交换机或临时队列,只需要提供交换机或临时队列的名称即可。
posted @ 2020-12-19 13:50  phax-ccc  阅读(139)  评论(0编辑  收藏  举报