Loading

RabbitMQ 学习笔记 - 基础

本文代码内容较长, 查看总结内容可以直接拉到文章末尾

RabbitMQ

AMQP: Advanced Message Queuing Protocol

同步 RPC, 异步 MQ
RPC: 关注处理,及时获得调用结果,具有强一致性结果,关心业务调用处理结果
MQ : 只关注通知

优点

  1. 解耦, 生产者和消费者之间完全解耦, 互不影响, 使消费者的扩展和生产者无关, 反之亦然
  2. 异步调用, 提高系统响应速度
  3. 流量削峰, 实现高并发场景需求

安装

docker run --name rabbitmq -p 15672:15672 -p 5672:5672 -d rabbitmq:3-management

使用流程

Rabbit-Using

相关名词

Message

消息, 数据的载体

Producer

生产者, 生产 Message 的客户端

Consumer

消费者, 使用 Message 的客户端

Broker

消息队列服务的提供者, 即运行在 5672 端口上的 RabbitMQ Server

Virtual Host

虚拟主机, 提供命名空间上的隔离作用

Exchange

交换机, 用于将消息推入到正确的队列中

Queue

消息的容器, 存放交换机发来的消息, 并提供给消费者使用, 队列是一种先进先出的数据结构

Binding

绑定关系/路由规则, 交换机根据与相连队列的 Binding 关系确定消息的目标队列, 并将消息推入进去

Connection

连接, 通信的通道

Channel

信道, 频繁的通过 TCP 建立 Connection 连接是一笔巨额的开销, 因此 Connection 划分出了 Channel 这种轻量级通信通道

Routing Key

路由标记, 用于指定消息的目标存放队列, Exchange 与 Queue 之间的 Routing Key 应该叫做 Binding Key/Routing Rule

Qos

服务质量 (quality of service)

  • prefetchCount: 信道内允许堆积的最大未应答消息数量 (0 表示无数量限制)
  • prefetchSize: 信道内允许堆积的最大未应答消息大小 (以 bit 作为单位, 0 表示无大小限制)

多消费者状态下队列按照消费者列表轮询分发消息, 当遇到达到最大消息积压限制的消费者时将跳过该消费者, 对下一个消费者进行检查分发消息

七种运行模式

准备工作

pom 依赖

<dependencies>
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.9.0</version>
    </dependency>

    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.6</version>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.8.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

为了方便查看运行结果, 自定义日志打印样式: logback.xml

<configuration>
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>[%thread]|%-5level|%logger{35} => %green(%msg) %n</pattern>
      </encoder>
   </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

新建一个连接工具类

package com.xtyuns.util;

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 RabbitUtil {
    private static final ConnectionFactory connFactory = new ConnectionFactory();
    private static Connection connection;

    static {
        connFactory.setHost("vm1");
        connFactory.setPort(5672);
        connFactory.setUsername("u-mq");
        connFactory.setPassword("p-mq");
        connFactory.setVirtualHost("/v-vhost");
    }

    public synchronized static Connection getConnection() {
        if (null == connection) {
            try {
                connection = connFactory.newConnection();
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
        return connection;
    }

    public static Channel getChannel() throws IOException {
        return getConnection().createChannel();
    }

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

简单模式

Hello World

package com.xtyuns.mode;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.xtyuns.util.RabbitUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.TimeoutException;

public class AHelloWorld {
    private static final String MODE_HELLO_QUEUE_NAME = "mode-hello";
    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    public AHelloWorld() {
        initExchangeAndQueue();
    }

    /**
     * 简单模式下使用默认交换机即可, 因此不需要在虚拟主机中新建交换机
     * 这里需要声明一个队列, 为后续消息的发送做准备
     * 如果声明的队列在虚拟主机中不存在则新建该队列; 如果队列已存在, 且队列配置信息一致则直接返回该队列, 否则抛出异常
     * 因为所有的队列都会绑定到默认交换机上(Routing Key 为队列名), 所以这里不再显式的将队列与交换机进行绑定
     */
    private void initExchangeAndQueue() {
        try(Channel channel = RabbitUtil.getChannel()) {
            // durable: broker 是否将队列数据写入磁盘
            // exclusive: 是否只允许当前 Connection 读写该队列 (Connection 关闭后自动删除该队列)
            // autoDelete: 队列为空时否则自动删除该队列
            // arguments: 队列附加信息, 没有则设置为 null
            AMQP.Queue.DeclareOk declareOk = channel.queueDeclare(MODE_HELLO_QUEUE_NAME, false, false, false, null);
            logger.info("[init]连接信息初始化成功, queue: {}", declareOk.getQueue());
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
            throw new NullPointerException("连接信息初始化失败!");
        }
    }

    /**
     * 通过默认交换机和指定的 Routing Key 向队列中发送一条消息
     * 此时默认交换机上只有一个 Routing Key 与队列名相同的队列, 因此消息会被默认交换机分配到这个队列上
     * @throws IOException IOException
     */
    public void sendMsg() throws IOException {
        try(Channel channel = RabbitUtil.getChannel()) {
            String msg = "msg-" + UUID.randomUUID().toString();
            channel.basicPublish("", MODE_HELLO_QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
            logger.info("[send]msg = {}", msg);
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }

    /**
     * 从指定的队列中取出消息
     * @return 当前信道对象, 关闭后将无法继续处理消息
     * @throws IOException IOException
     */
    public Channel receiveMsg() throws IOException {
        Channel channel = RabbitUtil.getChannel();
        channel.basicConsume(MODE_HELLO_QUEUE_NAME, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                logger.info("[rec]consumerTag = {}, body = {}", consumerTag, new String(body));
                getChannel().basicAck(envelope.getDeliveryTag(), false);
                logger.info("[ack]consumerTag = {}, ack = {}", consumerTag, true);
            }
        });

        return channel;
    }
}

测试类

package com.xtyuns.mode;

import com.rabbitmq.client.Channel;
import com.xtyuns.util.RabbitUtil;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeUnit;


class AHelloWorldTest {
    private final AHelloWorld aHelloWorld = new AHelloWorld();

    @Test
    void sendMsg() throws IOException {
        // 发送一条消息, 更清晰的查看消息的生产 -> 消费
        // 这里改为发送 30 条消息, 可以验证未确认的消息会重回队列
        for (int i = 0; i < 1; i++) {
            aHelloWorld.sendMsg();
        }
    }

    @Test
    void receiveMsg() throws IOException {
        Channel channel = aHelloWorld.receiveMsg();
        try {
            // 睡眠结束后关闭 Channel 将无法处理/应答消息, 未应答的消息将会重新放回队列中
            TimeUnit.MILLISECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        RabbitUtil.clear(channel, false);
    }
}

运行结果

hello_send

hello_receive

负载均衡模式

Work queues

package com.xtyuns.mode;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.xtyuns.util.RabbitUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.TimeoutException;

public class BWorkQueues {
    private static final String MODE_WORK_QUEUE_NAME = "mode-work";
    private static final Integer CONSUMER_SIZE = 2;
    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    public BWorkQueues() {
        initExchangeAndQueue();
    }

    /**
     * 负载均衡模式和简单模式类似, 只是有多个消费者连接到了同一个队列上
     * 这里同样使用默认交换机和在虚拟主机中新建的队列 (Routing Key 为队列名)
     */
    private void initExchangeAndQueue() {
        try(Channel channel = RabbitUtil.getChannel()) {
            AMQP.Queue.DeclareOk declareOk = channel.queueDeclare(MODE_WORK_QUEUE_NAME, false, false, false, null);
            logger.info("[init]连接信息初始化成功, queue: {}", declareOk.getQueue());
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
            throw new NullPointerException("连接信息初始化失败!");
        }
    }

    /**
     * 通过默认交换机和指定的 Routing Key 向队列中发送一条消息
     * @throws IOException IOException
     */
    public void sendMsg() throws IOException {
        try(Channel channel = RabbitUtil.getChannel()) {
            String msg = "msg-" + UUID.randomUUID().toString();
            channel.basicPublish("", MODE_WORK_QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
            logger.info("[sed]sendMsg = {}", msg);
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }

    /**
     * 多个消费者消费同一个队列中的消息
     * @throws IOException IOException
     */
    public void receiveMsg() throws IOException {
        // 开启多个消费者
        for (int i = 0; i < CONSUMER_SIZE; i++) {
            Channel channel = RabbitUtil.getChannel();
            // 防止消息全部积压到第一个连接到队列的信道上, 这里设置每个信道上最多允许存在 1 条未应答的消息, 同时信道必须关闭消息自动应答
            // 队列按照消费者列表轮询分发消息, 当遇到达到最大消息积压数量的消费者时将跳过该消费者, 对下一个消费者进行检查分发消息
            channel.basicQos(1);
            // lambda 表达式内要读取当前信道的序号
            final int finalI = i;
            channel.basicConsume(MODE_WORK_QUEUE_NAME, new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    logger.info("[rec-{}]consumerTag = {}, body = {}", finalI, consumerTag, new String(body));
                    getChannel().basicAck(envelope.getDeliveryTag(), false);
                    logger.info("[ack-{}]consumerTag = {}, ack = {}", finalI, consumerTag, true);
                }
            });
        }
    }
}

测试类

package com.xtyuns.mode;

import org.junit.jupiter.api.Test;
import java.io.IOException;

class BWorkQueuesTest {
    private final BWorkQueues bWorkQueuesTest = new BWorkQueues();

    @Test
    void sendMsg() throws IOException {
        for (int i = 0; i < 4; i++) {
            bWorkQueuesTest.sendMsg();
        }
    }

    @Test
    void receiveMsg() throws IOException {
        bWorkQueuesTest.receiveMsg();
    }
}

运行结果

work_send

work_receive

由于 rec-0 先被创建出来, 而 rec-1 还并未创建, 因此刚开始时 rec-0 连续接收到了两条消息。

广播模式

Publish/Subscribe

广播模式需要借助 fanout 类型的交换机来实现

package com.xtyuns.mode;

import com.rabbitmq.client.*;
import com.xtyuns.util.RabbitUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.TimeoutException;

public class CPubSub {
    private static final String MODE_PUB_SUB_EXCHANGE = "demo.fanout";
    private static final String MODE_PUB_SUB_QUEUE_PREFIX = "mode-pub-sub";
    private static final Integer QUEUE_SIZE = 3;
    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    public CPubSub() {
        initExchangeAndQueue();
    }

    /**
     * 广播模式(发布/订阅模式) 需要使用 fanout 类型的交换机实现
     * 首先在虚拟主机中声明一个 fanout 类型的交换机为后续的发送消息做准备
     * 接下来在虚拟主机中声明一系列的队列并将它们与新建的 fanout 交换机进行绑定, 同样为后续的发送消息做准备
     */
    private void initExchangeAndQueue() {
        try(Channel channel = RabbitUtil.getChannel()) {
            channel.exchangeDeclare(MODE_PUB_SUB_EXCHANGE, BuiltinExchangeType.FANOUT);
            for (int i = 0; i < QUEUE_SIZE; i++) {
                String queueName = MODE_PUB_SUB_QUEUE_PREFIX + i;
                channel.queueDeclare(queueName, false, false, false, null);
                channel.queueBind(queueName, MODE_PUB_SUB_EXCHANGE, "", null);
                logger.info("[bind]queue: {}, exchange: {}", queueName, MODE_PUB_SUB_EXCHANGE);
            }
            logger.info("[init]连接信息初始化成功, queue size: {}", QUEUE_SIZE);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
            throw new NullPointerException("连接信息初始化失败!");
        }
    }

    /**
     * 向指定的交换机发送消息, 交换机再根据消息的 Routing Key 将消息存入对应绑定关系的队列中
     * @throws IOException IOException
     */
    public void sendMsg() throws IOException {
        try(Channel channel = RabbitUtil.getChannel()) {
            String msg = "msg-" + UUID.randomUUID().toString();
            channel.basicPublish(MODE_PUB_SUB_EXCHANGE, "", null, msg.getBytes(StandardCharsets.UTF_8));
            logger.info("[sed]sendMsg = {}", msg);
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }

    /**
     * 为每个队列都设置一个消费者, 它们将收到相同的消息
     * @throws IOException IOException
     */
    public void receiveMsg() throws IOException {
        for (int i = 0; i < QUEUE_SIZE; i++) {
            Channel channel = RabbitUtil.getChannel();
            String queueName = MODE_PUB_SUB_QUEUE_PREFIX + i;
            final int finalI = i;
            channel.basicConsume(queueName, new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    logger.info("[rec-{}]consumerTag = {}, body = {}", finalI, consumerTag, new String(body));
                    getChannel().basicAck(envelope.getDeliveryTag(), false);
                    logger.info("[ack-{}]consumerTag = {}, ack = {}", finalI, consumerTag, true);
                }
            });
        }
    }
}

测试类

package com.xtyuns.mode;

import org.junit.jupiter.api.Test;
import java.io.IOException;

class CPubSubTest {
    private final CPubSub cPubSub = new CPubSub();

    @Test
    void sendMsg() throws IOException {
        cPubSub.sendMsg();
    }

    @Test
    void receiveMsg() throws IOException {
        cPubSub.receiveMsg();
    }
}

运行结果

broadcast_send

实际上交换机和队列的声明以及绑定操作运行一次就够了, 重复的操作是多余的, 但是没有影响

broadcast_receive

路由模式

Routing

路由模式需要借助 direct 类型的交换机来实现

package com.xtyuns.mode;

import com.rabbitmq.client.*;
import com.xtyuns.util.RabbitUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.TimeoutException;

public class DRouting {
    private static final String MODE_ROUTING_EXCHANGE = "demo.direct";
    private static final String MODE_ROUTING_QUEUE_PREFIX = "mode-routing-vip";
    private static final String MODE_ROUTING_ROUTING_PREFIX = "vip";
    private static final Integer QUEUE_SIZE = 2;
    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    public DRouting() {
        initExchangeAndQueue();
    }

    /**
     * 路由模式(Routing) 需要使用 direct 类型的交换机实现, 可以实现消息的分类入队, 即不同的消息被路由到了不同的队列中
     * 首先在虚拟主机中声明一个 direct 类型的交换机为后续的发送消息做准备
     * 接下来在虚拟主机中声明一系列的队列并使用不同的 Routing Key 将它们与新建的 direct 交换机进行绑定, 从而实现消息的分类入队
     */
    private void initExchangeAndQueue() {
        try(Channel channel = RabbitUtil.getChannel()) {
            channel.exchangeDeclare(MODE_ROUTING_EXCHANGE, BuiltinExchangeType.DIRECT);
            for (int i = 0; i < QUEUE_SIZE; i++) {
                int no = i + 1;
                String queueName = MODE_ROUTING_QUEUE_PREFIX + no;
                channel.queueDeclare(queueName, false, false, false, null);
                // 高等级的会员可以接受到低等级会员权限的的消息, 但是低等级会员不能接受的到高等级会员权限的信息
                for (int j = 0; j < no; j++) {
                    String routingKey = MODE_ROUTING_ROUTING_PREFIX + (j + 1);
                    channel.queueBind(queueName, MODE_ROUTING_EXCHANGE, routingKey, null);
                    logger.info("[bind]routingKey: {}, queue: {}, exchange: {}", routingKey, queueName, MODE_ROUTING_EXCHANGE);
                }
            }
            logger.info("[init]连接信息初始化成功, queue size: {}", QUEUE_SIZE);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
            throw new NullPointerException("连接信息初始化失败!");
        }
    }

    /**
     * 向指定的交换机发送消息, 交换机再根据消息的 Routing Key 将消息存入对应绑定关系的队列中
     * @throws IOException IOException
     */
    public void sendMsg() throws IOException {
        try(Channel channel = RabbitUtil.getChannel()) {
            for (int i = 0; i < QUEUE_SIZE; i++) {
                String routingKey = MODE_ROUTING_ROUTING_PREFIX + (i + 1);
                String msg = "msg-"  + routingKey + "-" + UUID.randomUUID().toString();
                channel.basicPublish(MODE_ROUTING_EXCHANGE, routingKey, null, msg.getBytes(StandardCharsets.UTF_8));
                logger.info("[sed]sendMsg = {}", msg);
            }
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }

    /**
     * 为每个队列都设置一个消费者, 它们将分等级接收到不同的消息
     * @throws IOException IOException
     */
    public void receiveMsg() throws IOException {
        for (int i = 0; i < QUEUE_SIZE; i++) {
            Channel channel = RabbitUtil.getChannel();
            String queueName = MODE_ROUTING_QUEUE_PREFIX + (i + 1);
            final int finalI = i;
            channel.basicConsume(queueName, new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    logger.info("[rec-{}{}]consumerTag = {}, body = {}", MODE_ROUTING_ROUTING_PREFIX, finalI + 1, consumerTag, new String(body));
                    getChannel().basicAck(envelope.getDeliveryTag(), false);
                    logger.info("[ack-{}{}]consumerTag = {}, ack = {}", MODE_ROUTING_ROUTING_PREFIX, finalI + 1, consumerTag, true);
                }
            });
        }
    }
}

测试类

package com.xtyuns.mode;

import org.junit.jupiter.api.Test;
import java.io.IOException;

class DRoutingTest {
    private final DRouting routing = new DRouting();

    @Test
    void sendMsg() throws IOException {
        routing.sendMsg();
    }

    @Test
    void receiveMsg() throws IOException {
        routing.receiveMsg();
    }
}

运行结果

routing_send

高等级的会员可以接受到低等级会员权限的的消息, 但是低等级会员不能接受的到高等级会员权限的信息

routing_receive

主题模式

Topics

package com.xtyuns.mode;

import com.rabbitmq.client.*;
import com.xtyuns.util.RabbitUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;

public class ETopics {
    private static final String MODE_TOPICS_EXCHANGE = "demo.topic";
    private static final String MODE_TOPICS_QUEUE_DEBUG = "mode-topics-debug";
    private static final String MODE_TOPICS_BIND_ROUTING_KEY_PATTERN_LOG_DEBUG = "log.*.debug";
    private static final String MODE_TOPICS_SEND_ROUTING_KEY_PATTERN_LOG_DEBUG = "log.app.debug";
    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    public ETopics() {
        initExchangeAndQueue();
    }

    /**
     * 主题模式(Topics) 需要使用 topic 类型的交换机实现, 可以实现队列和交换机绑定时的模糊路由规则, 从而达到在一条绑定关系上匹配到多个 Routing Key 的效果
     * 首先在虚拟主机中声明一个 topic 类型的交换机为后续的发送消息做准备
     * 接下来在虚拟主机中声明一个队列并使用含有通配符的路由规则将队列与新建的 direct 交换机进行绑定
     * pattern 中的通配符: `*` 匹配一个单词, `#` 匹配零个或多个单词
     */
    private void initExchangeAndQueue() {
        try(Channel channel = RabbitUtil.getChannel()) {
            // 定义 topic 交换机
            channel.exchangeDeclare(MODE_TOPICS_EXCHANGE, BuiltinExchangeType.TOPIC);
            // 定义 mode-topics-debug 队列
            channel.queueDeclare(MODE_TOPICS_QUEUE_DEBUG, false, false, false, null);

            // 通过 pattern 类型的路由规则来绑定队列与交换机
            channel.queueBind(MODE_TOPICS_QUEUE_DEBUG, MODE_TOPICS_EXCHANGE, MODE_TOPICS_BIND_ROUTING_KEY_PATTERN_LOG_DEBUG);
            logger.info("[bind]routingKey: {}, queue: {}, exchange: {}", MODE_TOPICS_BIND_ROUTING_KEY_PATTERN_LOG_DEBUG, MODE_TOPICS_QUEUE_DEBUG, MODE_TOPICS_EXCHANGE);

            logger.info("[init]连接信息初始化成功: queue: {}", MODE_TOPICS_QUEUE_DEBUG);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
            throw new NullPointerException("连接信息初始化失败!");
        }
    }

    /**
     * 发送一条 Routing Key 符合 pattern 规则的日志信息到交换机, 交换机再使用自己的路由表和消息的 Routing Key 进行匹配, 并将消息存入到对应的队列中
     * 同理可以为交换机和队列设置多个路由规则, 然后发送多个符合路由规则的消息, 来实现更为复杂的路由关系
     * @throws IOException IOException
     */
    public void sendMsg() throws IOException {
        try(Channel channel = RabbitUtil.getChannel()) {
            String routingKey = MODE_TOPICS_SEND_ROUTING_KEY_PATTERN_LOG_DEBUG;
            String msg = "这是来自【" + routingKey + "】的信息, 它应该被【" + MODE_TOPICS_BIND_ROUTING_KEY_PATTERN_LOG_DEBUG + "】所匹配, 最终放入到【" + MODE_TOPICS_QUEUE_DEBUG + "】队列中";
            channel.basicPublish(MODE_TOPICS_EXCHANGE,
                    routingKey, null,
                    msg.getBytes(StandardCharsets.UTF_8));
            logger.info("[send]msg: {}", msg);
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }

    /**
     * 从指定的队列中取出消息
     * @throws IOException IOException
     */
    public void receiveMsg() throws IOException {
        String queue_debug = MODE_TOPICS_QUEUE_DEBUG;

        Channel channel = RabbitUtil.getChannel();
        channel.basicConsume(queue_debug, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                logger.info("[rec-{}]-[{}]consumerTag = {}, body = {}", queue_debug, envelope.getRoutingKey(), consumerTag, new String(body));
                getChannel().basicAck(envelope.getDeliveryTag(), false);
                logger.info("[ack-{}]-[{}]consumerTag = {}, ack = {}", queue_debug, envelope.getRoutingKey(), consumerTag, true);
            }
        });
    }
}

测试类

package com.xtyuns.mode;

import org.junit.jupiter.api.Test;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;

class ETopicsTest {
    private final ETopics topics = new ETopics();

    @Test
    void sendMsg() throws IOException {
        topics.sendMsg();
    }

    @Test
    void receiveMsg() throws IOException {
        topics.receiveMsg();
    }
}

运行结果

topics_send

topics_receive

发布确认模式

Publisher Confirms

发布确认模式是保证消息的可靠性的一种机制, 这里以简单模式的场景为例演示发布确认机制的使用

  • 发布确认机制可以结合其他模式使用
  • 发布确认机制只作用在生产者方面

交换机确认接收消息

channel.confirmSelect();

package com.xtyuns.confirm;

import com.rabbitmq.client.*;
import com.xtyuns.util.RabbitUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.TimeoutException;

public class AConfirmHelloWorld {
    private static final String MODE_HELLO_QUEUE_NAME = "mode-confirm-hello";
    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    public AConfirmHelloWorld() {
        initExchangeAndQueue();
    }

    private void initExchangeAndQueue() {
        try(Channel channel = RabbitUtil.getChannel()) {
            AMQP.Queue.DeclareOk declareOk = channel.queueDeclare(MODE_HELLO_QUEUE_NAME, false, false, false, null);
            logger.info("[init]连接信息初始化成功, queue: {}", declareOk.getQueue());
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
            throw new NullPointerException("连接信息初始化失败!");
        }
    }

    /**
     * 查看发送确认机制运行结果
     * @throws IOException IOException
     */
    public void sendMsg() throws IOException {
        try(Channel channel = RabbitUtil.getChannel()) {
            // 在当前信道上启用发送确认机制
            channel.confirmSelect();
            
            // 异步回调, 这里的回调方法调用时要保证 channel 还没有被关闭 (因为下面使用了阻塞等待确认, 所以这里一定会执行)
            channel.addConfirmListener(new ConfirmListener() {
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    logger.info("[confirm-ack]deliveryTag = {}, multiple = {}", deliveryTag, multiple);
                }

                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    logger.info("[confirm-nack]deliveryTag = {}, multiple = {}", deliveryTag, multiple);
                }
            });
            
            String msg = "msg-" + UUID.randomUUID().toString();
            channel.basicPublish("", MODE_HELLO_QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
            
            // 阻塞等待 Broker 响应结果, 如果不开启发布确认机制这里会报错: Confirms not selected
            boolean b = channel.waitForConfirms();
            // 如果 Broker 是非 nack 导致的消息无法入队, 如: 交换机不存在, 上面其实会直接抛出异常
            if (b) {
                // 发送成功
                logger.info("[send-ok]msg = {}", msg);
            } else {
                // 发送失败
                logger.info("[send-error]msg = {}", msg);
            }
        } catch (TimeoutException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 从指定的队列中取出消息
     * @return 当前信道对象
     * @throws IOException IOException
     */
    public Channel receiveMsg() throws IOException {
        Channel channel = RabbitUtil.getChannel();
        channel.basicConsume(MODE_HELLO_QUEUE_NAME, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                logger.info("[rec]consumerTag = {}, envelope = {}, body = {}", consumerTag, envelope, new String(body));
                getChannel().basicAck(envelope.getDeliveryTag(), false);
                logger.info("[ack]consumerTag = {}, ack = {}", consumerTag, true);
            }
        });

        return channel;
    }
}
测试代码
package com.xtyuns.confirm;

import org.junit.jupiter.api.Test;
import java.io.IOException;

class AConfirmHelloWorldTest {
    private final AConfirmHelloWorld confirmHelloWorld = new AConfirmHelloWorld();

    @Test
    void sendMsg() throws IOException {
        confirmHelloWorld.sendMsg();
    }

    @Test
    void receiveMsg() throws IOException {
        confirmHelloWorld.receiveMsg();
    }
}
运行结果

exchange-confirm

交换机分发消息失败通知

mandatory=true

package com.xtyuns.confirm;

import com.rabbitmq.client.*;
import com.xtyuns.util.RabbitUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class AConfirmHelloWorld {
    // 这里设置一个不存在的队列, 并且启动时不创建该队列, 使默认交换机无法投递消息
    private static final String MODE_HELLO_QUEUE_NAME = "mode-confirm-hello-111";
    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    // 启动时不再创建队列
    public AConfirmHelloWorld() {
    }

    /**
     * 查看交换机投递消息失败的通知
     * @throws IOException IOException
     */
    public void sendMsg() throws IOException {
        try(Channel channel = RabbitUtil.getChannel()) {
            // 触发这个回调的条件有两个: 生产者发送消息时指定 mandatory=true (强制投递消息)、交换机无法完成消息投递
            channel.addReturnListener(returnMessage -> {
                // 打印投递失败消息的详细信息
                logger.info("[{}]{}, {}-{}, {}, {}",
                        returnMessage.getReplyCode(), returnMessage.getReplyText(),
                        returnMessage.getExchange(), returnMessage.getRoutingKey(),
                        new String(returnMessage.getBody()),
                        returnMessage.getProperties().toString()
                );
            });

            String msg = "msg-" + UUID.randomUUID().toString();
            // 生产者发送消息
            channel.basicPublish("", MODE_HELLO_QUEUE_NAME, true, null, msg.getBytes(StandardCharsets.UTF_8));
            // 这里阻塞一下, 因为 Channel 关闭后无法看到上面的回调方法的调用情况
            TimeUnit.SECONDS.sleep(1);
        } catch (TimeoutException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 从指定的队列中取出消息
     * @return 当前信道对象
     * @throws IOException IOException
     */
    public Channel receiveMsg() throws IOException {
        Channel channel = RabbitUtil.getChannel();
        channel.basicConsume(MODE_HELLO_QUEUE_NAME, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                logger.info("[rec]consumerTag = {}, envelope = {}, body = {}", consumerTag, envelope, new String(body));
                getChannel().basicAck(envelope.getDeliveryTag(), false);
                logger.info("[ack]consumerTag = {}, ack = {}", consumerTag, true);
            }
        });

        return channel;
    }
}

测试代码同上

运行结果

queue-return

确认机制总结

  • 发布确认机制作用于在 Producer 和 Exchange 之间
  • 交换机分发失败回调作用于 Exchange 和 Queue 之间, 且只在分配失败时回调 ReturnsCallback
  • 如果消息都没有被 Exchange 接收, 那么也根本不会触发 RetuenCallback
  • addConfirmListener 和 addReturnListener 方法既可以接收 ConfirmListener 和 ReturnListener 作为参数, 也可以接收函数式接口口 ConfirmCallback 和 ReturnCallback, 它们的结果是等效的
  • 无论是发布确认机制还是交换机分发失败回调都需要显式的开启, 它们默认都是关闭的
  • 一般情况下不会使用同步阻塞等待 Broker 的响应结果, 因为这样大大的降低了并发效率, 而是采用异步通知回调
  • 消息在未被交换机接收之前 channal 中所有的回调方法都无法获得消息的内容, 因此异步通知回调无法获取丢失的消息内容

远程调用模式

RPC Queue

常见的 RabbitMQ 模式为上面的 6 种, RPC 模式参照官网教程: https://www.rabbitmq.com/tutorials/tutorial-six-java.html
rabbitmq-rpc

内置交换机

RabbitMQ-Exchange

基础内容总结

RabbitMQ

posted @ 2021-11-17 20:15  xtyuns  阅读(42)  评论(0编辑  收藏  举报