RabbitMQ基础入门实战

1.前言

特点:生态好,好学习,易于理解,时效性强,支持很多不同语言的客户端,扩展性,可用性都很不错。

学习性价比非常高的消息队列,适用于绝大多数中小规模分布式系统。

学习网站:https://www.rabbitmq.com/

2.基本概念

AMQP协议:AMQP 0-9-1 Model Explained | RabbitMQ

高级消息队列协议(Advanced Message Queue Protocol)

生产者:发消息到某个交换机/队列

消费者:从某个队列中取消息

交换机(Exchange):负责把消息转发到对应的队列。

队列(Queue):存储消息的队列

路由(Routes):转发,就是怎么把消息从一个地方占到另一个地方(比如从生产者转发到某个队列)

3.安装

3.1本地安装

快速开始:RabbitMQ: One broker to queue them all | RabbitMQ

windows安装:Installing on Windows | RabbitMQ

先安装erlang25.3.2(因为RabbitMQ依赖erlang,RabbitMQ是erlang开发的),这个语言的性能非常高。

erlang下载:Otp 25.3.2 - Erlang/OTP

安装完erlang之后,安装RabbitMQ即可

ctrl + r 打开 services.msc(服务菜单),查看RabbitMQ服务是否已启动:

安装RabbitMQ监控面板:

在RabbitMQ安装目录的sbin中执行下述脚本:

java 复制代码
rabbitmq-plugins.bat enable rabbitmq_management

输入以下信息

访问:http://localhost:15672,用户密码都是guest:

如果想要在远程服务器安装访问RabbitMQ管理面板,你要自己创建一个管理员账号,不能使用默认的guest,否则会被拦截(官方处于安全考虑)。

如果被拦截,可用自己创建管理员用户:

参考文档Adding a User:Authentication, Authorisation, Access Control | RabbitMQ

RabbitMQ端口占用:

5672:程序连接的端口

15672:webUI

3.2服务器安装

通过以下文档进行安装RabbitMQ访问RabbitMQ,访问端口是5673映射到了5672,所以进行连接访问使用5673访问。

docker部署rabbitmq消息队列_docker查看rabbitmq状态-CSDN博客

4.JAVA连接RabbitMQ可能会遇到的问题

其实进行使用的时候,除了密码错误就是没有进行创建用户,进行在RabbitMQ管理面板进行新建用户,授予权限即可。

RabbitMQ客户端连接报错原因分析_分布式消息服务RabbitMQ版 (huaweicloud.com)

5.快速入门

MQ官方教程:RabbitMQ Tutorials | RabbitMQ

5.1单向发送

文档:RabbitMQ tutorial - "Hello World!" | RabbitMQ

一个生产者给一个队列发消息,一个消费者从这个队列取出消息,一对一。

首先需要引入消息队列JAVA客户端:

java 复制代码
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.17.0</version>
</dependency>

生产者代码:

java 复制代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;

public class SingleSend {

    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("49.232.17.169");
        factory.setUsername("admin");
        factory.setPassword("123456");
        factory.setPort(5673);
        factory.setConnectionTimeout(30000000);
        factory.setHandshakeTimeout(30000000);

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String message = "HelloWorld";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
            System.out.println(" [x] Send '" + message + "'");
        }
    }
}

Channel频道:理解为操作消息队列client(比如jdbcClient,redisClient),提供了和消息队列server建立通信的传输发方法(为了服用连接,提高传输效率)。程序通过channel操作RabbitMQ(收发消息)。

创建消息队列:

参数:

queueName:消息队列名称(注意,同名称的消息队列,只能用同样的参数创建一次)。

durabale:消息队列重启后,消息是否不会丢失。

exclusive:是否只允许当前这个创建消息队列的连接操作消息队列。

autoDelete:没有人用队列后,是否要删除队列。

执行程序后,可以看到有一条消息:

消费者代码:

java 复制代码
package com.yang.yangbi.mq;

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

import java.nio.charset.StandardCharsets;

public class SingleConsumer {

    public static final String QUEUE_NAME = "hello2";

    public static void main(String[] args) throws Exception {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("49.232.17.169");
        factory.setUsername("admin");
        factory.setPassword("123456");
        factory.setPort(5673);
        factory.setConnectionTimeout(30000000);
        factory.setHandshakeTimeout(30000000);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        // 创建队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C ");
        // 定义了如何进行处理消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        // 消费消息, 会持续阻塞
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}

启动消费者后,可以看到消息被消费了:

5.2多消费者

官方教程:https://www.rabbitmq.com/tutorials/tutorial-two-java.html

场景:多个机器同时去接受并处理任务(尤其是每个机器的处理能力有限)。

一个生产者给一个队列发消息,多个消费者从这个队列取消息,一对多。

5.2.1队列持久化

durable参数设置为true,服务器重启后队列不丢失。

java 复制代码
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

5.2.2消息持久化

指定MessageProperties.PERSISTENT_TEXT_PLAIN参数,服务器重启后队列中的消息不回丢失:

java 复制代码
channel.basicPublish("", TASK_QUEUE_NAME,
        MessageProperties.PERSISTENT_TEXT_PLAIN,
        message.getBytes("UTF-8"));

5.2.3生产者代码

使用Scanner接受用户输入,便于发送消息。

java 复制代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;

import java.util.Scanner;

public class MultiProducer {

    public static final String TASK_QUEUE_NAME = "multi_queue_queue";

    public static void main(String[] args) throws Exception {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("49.232.17.169");
        factory.setUsername("admin");
        factory.setPassword("123456");
        factory.setPort(5673);
        factory.setConnectionTimeout(30000000);
        factory.setHandshakeTimeout(30000000);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String message = scanner.nextLine();
            channel.basicPublish("", TASK_QUEUE_NAME,
                    MessageProperties.PERSISTENT_TEXT_PLAIN,
                    message.getBytes("UTF-8"));
            System.out.println("[x] Sent '" + message + "'");
        }
    }
}

5.2.4消费者代码优化方案

控制单个消费处理任务积压数

每个消费者最多处理一个任务

java 复制代码
channel.basicQos(1);

消息确认机制:

为了保证消息成功被消费(快递成功被取走),RabbitMQ提供了消息确认机制,当消费者接收到消息之后,比如要给一个反馈:

ack:消费成功

nack:消费失败

reject:拒绝

只有告诉RabbitMQ服务器消费成功,服务器才会放心地移除消息。

支持配置autoack,会自动执行ack命令,接收到消息就立刻成功了。

但是还是建议将autoack改为false,根据实际情况,去手动确认。

java 复制代码
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {});

指定确认某条消息

第一个参数是获取消息的唯一标识tag,第二个参数是是否要对当前消息以及之前的都发送ack确认消息,设置true就会对接收到的最后一个消息到之前的消息都进行ack确认,设置false就只会对现在获取到的消息进行ack。

java 复制代码
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

指定拒绝某条消息:

第一个参数是获取消息的唯一标识tag。

第二个参数是是否要对当前消息以及之前的都发送nack确认消息,设置true就会对接收到的最后一个消息到之前的消息都进行nack确认,设置false就只会对现在获取到的消息进行nack。

第三个参数表示是否重新入队,可用于重试。

java 复制代码
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);

5.2.5消费者代码

java 复制代码
package com.yang.yangbi.mq;

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

public class MultiConsumer {

    public static final String TASK_QUEUE_NAME = "multi_queue";

    public static void main(String[] args) throws Exception {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("49.232.17.169");
        factory.setUsername("admin");
        factory.setPassword("123456");
        factory.setPort(5673);
        factory.setConnectionTimeout(30000000);
        factory.setHandshakeTimeout(30000000);
        Connection connection = factory.newConnection();

        for (int i = 0; i < 2; i++) {
            final Channel channel = connection.createChannel();

            channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
            System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

            channel.basicQos(1);

            // 定义了如何处理消息
            int finalI = i;

            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");

                try {
                    // 处理工作
                    System.out.println(" [x] Received '" + "编号:" + finalI + ":" + message + "'");
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    // 暂停20秒, 模拟机器的处理能力有限
                    Thread.sleep(20000);
                } catch (InterruptedException e) {
                   e.printStackTrace();
                   channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
                } finally {
                    System.out.println(" [x] Done ");
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                }
            };
            // 开启消费监听
            channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {});
        }
    }
}

5.2.6两个小技巧

1.使用Scanner接收用户的输入,便于快速发送多条消息

2.使用for循环创建多个消费者,便于快速验证队列模型工作机制。

5.3交换机

一个生产者给多个队列发送消息,一个生产者对多个队列。

交换机的作用:提供消息转发功能,类似网络路由器。

要解决的问题:怎么把消息转发到不同队列上,好让消费者从不同的队列消费?

绑定:交换机和队列关联起来,也可以叫路由,算是一个算法或者转发策略。

绑定代码

java 复制代码
channel.queueBind(queueName, EXCHANGE_NAME, "绑定规则");

教程:RabbitMQ tutorial - Publish/Subscribe | RabbitMQ

交换机有很多种类别:fanout,direct,topic,headers

5.4fanout

扇出,广播

特点:消息会转发到所有绑定到盖交换机的队列

场景:很适用于发布订阅的场景。比如写日志,可以多个系统间共享。

示例场景:

5.4.1生产者代码

java 复制代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.util.Scanner;

public class FanoutProducer {

    public static final String EXCHANGE_NAME = "fanout-exchange";

    public static void main(String[] args) throws Exception {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("49.232.17.169");
        factory.setUsername("admin");
        factory.setPassword("123456");
        factory.setPort(5673);
        factory.setConnectionTimeout(30000000);
        factory.setHandshakeTimeout(30000000);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        // 创建交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String message = scanner.nextLine();
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

5.4.2消费者代码

注意:

1.消费者和生产者要绑定同一个交换机

2.要现有队列,才能进行绑定

所以绑定交换机之间,必须进行创建队列

java 复制代码
package com.yang.yangbi.mq;

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

public class FanoutConsumer {

    public static final String EXCHANGE_NAME = "fanout-exchange";

    public static void main(String[] args) throws Exception {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("49.232.17.169");
        factory.setUsername("admin");
        factory.setPassword("123456");
        factory.setPort(5673);
        factory.setConnectionTimeout(30000000);
        factory.setHandshakeTimeout(30000000);
        Connection connection = factory.newConnection();
        Channel channel1 = connection.createChannel();
        Channel channel2 = connection.createChannel();

        // 声明交换机
        channel1.exchangeDeclare(EXCHANGE_NAME, "fanout");
        // 创建队列,随机分配一个队列名称
        String queueName = "xiaowang_queue";
        channel1.queueDeclare(queueName, true, false, false, null);
        channel1.queueBind(queueName, EXCHANGE_NAME, "");

        String queueName2 = "xiaoli_queue";
        channel2.queueDeclare(queueName2, true, false, false, null);
        channel2.queueBind(queueName2, EXCHANGE_NAME, "");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        // 获取到信息
        DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [小王] Received '" + message + "'");
        };

        DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [小李] Received '" + message + "'");
        };

        // 监听队列进行消费
        channel1.basicConsume(queueName, true, deliverCallback1, consumberTag -> {});
        channel2.basicConsume(queueName2, true, deliverCallback2, consumberTag -> {});
    }
}

效果:所有消费者都可以收到消息。

5.5Direct交换机

官方教程:RabbitMQ tutorial - Routing | RabbitMQ

绑定:可以让交换机及和队列进行关联,可以指定让交换机把什么样的消息发给哪个队列(类似于计算机网络中的两个路由器,或者网络设备相互连接,也可以理解为网线)

routingKey:路由键,控制消息要转发给哪个队列(IP地址)

特点:消息会根据路由键转发到指定的队列。

场景:特定的消息只交给特定的系统(程序)来处理

绑定关系:完全匹配字符串

可以绑定相同的路由键

比如发日志的场景,希望使用独立的程序来处理不同级别的日志,比如C1系统处理error日志,C2系统处理其他级别的日志

示例场景

5.5.1生产者

java 复制代码
package com.yang.yangbi.mq;

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

import java.util.Scanner;

public class DirectProducer {

    public static final String EXCHANGE_NAME = "direct-exchange";

    public static void main(String[] args) throws Exception {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("49.232.17.169");
        factory.setUsername("admin");
        factory.setPassword("123456");
        factory.setPort(5673);
        factory.setConnectionTimeout(30000000);
        factory.setHandshakeTimeout(30000000);
        Connection connection = factory.newConnection();
        Channel channel1 = connection.createChannel();

        // 声明交换机并进行使用
        channel1.exchangeDeclare(EXCHANGE_NAME, "direct");

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String userInput = scanner.nextLine();
            String[] strings = userInput.split(" ");
            if (strings.length < 1) {
                continue;
            }
            String message = strings[0];
            String routingKey = strings[1];

            channel1.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
        }
    }
}

5.5.2消费者

java 复制代码
package com.yang.yangbi.mq;

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

public class DirectConsumer {
    public static final String EXCHANGE_NAME = "direct-exchange";

    public static void main(String[] args) throws Exception {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("49.232.17.169");
        factory.setUsername("admin");
        factory.setPassword("123456");
        factory.setPort(5673);
        factory.setConnectionTimeout(30000000);
        factory.setHandshakeTimeout(30000000);
        Connection connection = factory.newConnection();
        Channel channel1 = connection.createChannel();

        // 声明交换器
        channel1.exchangeDeclare(EXCHANGE_NAME, "direct");

        // 创建队列,随机分配一个队列名称
        String queueName = "xiaoyu_queue";
        channel1.queueDeclare(queueName, true, false, false, null);
        channel1.queueBind(queueName, EXCHANGE_NAME, "xiaoyu");

        // 创建队列,随机分配一个队列名称
        String queueName2 = "xiaopi_queue";
        channel1.queueDeclare(queueName2, true, false, false, null);
        channel1.queueBind(queueName2, EXCHANGE_NAME, "xiaopi");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback xiaoyuDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [xiaoyu] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        DeliverCallback xiaopiDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [xiaopi] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        channel1.basicConsume(queueName, true, xiaoyuDeliverCallback, consumerTag -> {
        });
        channel1.basicConsume(queueName2, true, xiaopiDeliverCallback, consumerTag -> {
        });
    }
}

5.6topic交换机

官方教程:RabbitMQ tutorial - Topics | RabbitMQ

特点:消息会根据一个模糊的路由键转发到指定的队列

场景:特定的一类消息可以交给特定的一类系统(程序)来处理

绑定关系:可以模糊匹配多个绑定

*:匹配一个单词,比如*.orange,那么a.orange,b.orange都能匹配

#:匹配0个或者多个单词,比如a.#,那么a.a,a.b,a.a.a都能匹配

注意:这里的匹配和MySQL的like的%不一样,只能按照单个单词来进行匹,每个.进行分割单词,如果是 '#.',其实可以忽略,匹配 0 个词也 ok

应用场景:

老板要下发一个任务,让多个组进行处理

5.6.1生产者代码

java 复制代码
package com.yang.yangbi.mq;

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

import java.util.Scanner;

public class TopicProducer {

    public static final String EXCHANGE_NAME = "topic-exchange";

    public static void main(String[] args) throws Exception {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("49.232.17.169");
        factory.setUsername("admin");
        factory.setPassword("123456");
        factory.setPort(5673);
        factory.setConnectionTimeout(30000000);
        factory.setHandshakeTimeout(30000000);
        Connection connection = factory.newConnection();
        Channel channel1 = connection.createChannel();

        // 声明交换机
        channel1.exchangeDeclare(EXCHANGE_NAME, "topic");

        // 进行生产消息
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String userInput = scanner.nextLine();
            String[] strings = userInput.split(" ");
            if (strings.length < 1) {
                continue;
            }
            String message = strings[0];
            String routingKey = strings[1];

            channel1.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
        }
    }
}

5.6.2消费者代码

java 复制代码
package com.yang.yangbi.mq;

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

public class TopicConsumer {

    public static final String EXCHANGE_NAME = "topic-exchange";

    public static void main(String[] args) throws Exception {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("49.232.17.169");
        factory.setUsername("admin");
        factory.setPassword("123456");
        factory.setPort(5673);
        factory.setConnectionTimeout(30000000);
        factory.setHandshakeTimeout(30000000);
        Connection connection = factory.newConnection();
        Channel channel1 = connection.createChannel();

        // 声明交换机
        channel1.exchangeDeclare(EXCHANGE_NAME, "topic");

        // 创建队列
        String queueName = "frontend_queue";
        channel1.queueDeclare(queueName, true, false, false, null);
        channel1.queueBind(queueName, EXCHANGE_NAME, "#.前端.#");

        // 创建队列
        String queueName2 = "backend_queue";
        channel1.queueDeclare(queueName2, true, false, false, null);
        channel1.queueBind(queueName2, EXCHANGE_NAME, "#.后端.#");

        // 创建队列
        String queueName3 = "product_queue";
        channel1.queueDeclare(queueName3, true, false, false, null);
        channel1.queueBind(queueName3, EXCHANGE_NAME, "#.产品.#");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback xiaoaDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [xiaoa] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        DeliverCallback xiaobDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [xiaob] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        DeliverCallback xiaocDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [xiaoc] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        channel1.basicConsume(queueName, true, xiaoaDeliverCallback, consumerTag -> {
        });
        channel1.basicConsume(queueName2, true, xiaobDeliverCallback, consumerTag -> {
        });
        channel1.basicConsume(queueName3, true, xiaocDeliverCallback, consumerTag -> {
        });
    }
}

5.7Headers交换机

类似主题和直接交换机,可以根据headers中的内容来指定发送到哪个队列

由于性能比较差,比较复杂,一般不推荐使用。

5.8RPC

支持用消息队列来模拟RPC的调用,但是一般没必要,直接用Dubbo,GRPC等RPC框架就好了。

实现一个场景,总要有更合适的,更专注的技术。

6.核心特性

6.1消息过期机制

官方文档:Time-To-Live and Expiration | RabbitMQ

可以个i每条消息指定一个有效期,一段时间内违背消费者处理,就过期了。

示例场景:消息者(库存系统)挂了,一个订单15分钟还没被库存系统处理,这个订单其实已经失效了,哪怕库存系统再恢复,其实也不用扣减库存。

使用场景:清理过期数据,模拟延迟队列的实现(不开会员就慢速),专门让某个程序处理过期请求即可。

6.1.1给队列中的所有消息进行指定过期时间

时间的设置单位为毫秒

java 复制代码
// 创建队列,指定消息过期参数
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 5000);
// args 指定参数
channel.queueDeclare(QUEUE_NAME, false, false, false, args);

如果在过期时间内,还没有消费者取消息,消息才会过去。

注意,如果消息已经被渠道,但是没确认,是不会过期的,消息会一直存在于队列中。

如果消息处于待消费状态并且过期时间到达后,消息将被标记为过期。但是,如果消息已经被消费者消费,并且正在处理过程中,即使过期时间到达,消息仍然会被正常处理。

6.1.1.1消费者代码
java 复制代码
public class TtlConsumer {

    private final static String QUEUE_NAME = "ttl_queue";

    public static void main(String[] argv) throws Exception {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        // 创建队列,指定消息过期参数
        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-message-ttl", 5000);
        // args 指定参数
        channel.queueDeclare(QUEUE_NAME, false, false, false, args);

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        // 定义了如何处理消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        // 消费消息,会持续阻塞
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }
}
6.1.1.2生产者代码
java 复制代码
public class TtlProducer {

    private final static String QUEUE_NAME = "ttl_queue";

    public static void main(String[] argv) throws Exception {
        // 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
//        factory.setUsername();
//        factory.setPassword();
//        factory.setPort();

        // 建立连接、创建频道
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            // 发送消息
            String message = "Hello World!";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

6.1.2给某条消息指定过期时间

java 复制代码
// 给消息指定过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
        .expiration("1000")
        .build();
channel.basicPublish("my-exchange", "routing-key", properties, message.getBytes(StandardCharsets.UTF_8));

实例代码

java 复制代码
public class TtlProducer {

    private final static String QUEUE_NAME = "ttl_queue";

    public static void main(String[] argv) throws Exception {
        // 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
//        factory.setUsername();
//        factory.setPassword();
//        factory.setPort();

        // 建立连接、创建频道
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            // 发送消息
            String message = "Hello World!";

            // 给消息指定过期时间
            AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                    .expiration("1000")
                    .build();
            channel.basicPublish("my-exchange", "routing-key", properties, message.getBytes(StandardCharsets.UTF_8));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

6.2消息确认机制

官方文档:Consumer Acknowledgements and Publisher Confirms | RabbitMQ

为了保证消息成功被消费(快递成功被取走),RabbitMQ提供了消息确认机制,当消费者接收到消息后,要给一个反馈:

ack:消费成功

nack:消费失败

reject:拒绝

如果告诉RabbitMQ服务器消费成功,服务器才会放心地移除消息。

支持配置autoack,会自动执行ack命令,接收到消息立刻就成功了。

java 复制代码
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {});

一般情况,建议autoack设置为false,根据实际情况,去手动进行确认。

指定确认某条消息:

java 复制代码
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), );

第二个参数multiple批量确认:是指是否要一次性确认所有的历史消息直到这条

指定拒绝某条消息:

java 复制代码
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);

第三个参数标识是否重新入队,可用于重试。

6.3死信队列

官方文档:Dead Letter Exchanges | RabbitMQ

为了保证消息的可靠性,比如每条消息都成功消费,需要提供一给容错机制,即:失败的消息怎么处理?

死信:过期的消息,拒收的消息,消息队列满了,处理失败的消息的统称。

死信队列:专门处理死信的队列(注意,仅仅是一个普通的队列,只不过是专门用来进行处理死信的,你甚至可以理解这个队列名称叫"死信队列")

死信交换机:专门给死信队列转发消息的交换机(注意,它就是一个普通交换机,只不过是专门给死信队列发消息而已,理解为这个交换机的名称就叫"死信交换机")。也存在路由绑定。

死信可以通过死信交换机绑定到死信队列。

示例场景:

6.3.1如何实现死信队列?

1)创建死信交换机和死信队列,并且绑定关系

2)给失败之后需要容错处理的队列绑定死信交换机

示例代码:

java 复制代码
// 指定死信队列参数
Map<String, Object> args = new HashMap<>();
// 要绑定到哪个交换机
args.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
// 指定死信要转发到哪个死信队列
args.put("x-dead-letter-routing-key", "waibao");

// 创建队列,随机分配一个队列名称
String queueName = "xiaodog_queue";
channel.queueDeclare(queueName, true, false, false, args);
channel.queueBind(queueName, EXCHANGE_NAME, "xiaodog");

3)可以给需要容错的队列指定死信之后的转发规则,死信应该再转发到哪个死信队列

java 复制代码
// 指定死信要转发到哪个死信队列
args.put("x-dead-letter-routing-key", "waibao");

4)可以通过程序来读取死信队列中的消息,从而进行处理

java 复制代码
// 创建队列,随机分配一个队列名称
String queueName = "laoban_dlx_queue";
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, DEAD_EXCHANGE_NAME, "laoban");

String queueName2 = "waibao_dlx_queue";
channel.queueDeclare(queueName2, true, false, false, null);
channel.queueBind(queueName2, DEAD_EXCHANGE_NAME, "waibao");

DeliverCallback laobanDeliverCallback = (consumerTag, delivery) -> {
    String message = new String(delivery.getBody(), "UTF-8");
    // 拒绝消息
    channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
    System.out.println(" [laoban] Received '" +
            delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};

DeliverCallback waibaoDeliverCallback = (consumerTag, delivery) -> {
    String message = new String(delivery.getBody(), "UTF-8");
    // 拒绝消息
    channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
    System.out.println(" [waibao] Received '" +
            delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};

channel.basicConsume(queueName, false, laobanDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName2, false, waibaoDeliverCallback, consumerTag -> {
});

6.3.2生产者代码

生产者主要是进行创建死信队列和死信交换,并将信息发布到工作交换机。

java 复制代码
public class DlxDirectProducer {

    private static final String DEAD_EXCHANGE_NAME = "dlx-direct-exchange";
    private static final String WORK_EXCHANGE_NAME = "direct2-exchange";


    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            // 声明死信交换机
            channel.exchangeDeclare(DEAD_EXCHANGE_NAME, "direct");

            // 创建队列,随机分配一个队列名称
            String queueName = "laoban_dlx_queue";
            channel.queueDeclare(queueName, true, false, false, null);
            channel.queueBind(queueName, DEAD_EXCHANGE_NAME, "laoban");

            String queueName2 = "waibao_dlx_queue";
            channel.queueDeclare(queueName2, true, false, false, null);
            channel.queueBind(queueName2, DEAD_EXCHANGE_NAME, "waibao");

            DeliverCallback laobanDeliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                // 拒绝消息
                channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
                System.out.println(" [laoban] Received '" +
                        delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
            };

            DeliverCallback waibaoDeliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                // 拒绝消息
                channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
                System.out.println(" [waibao] Received '" +
                        delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
            };

            channel.basicConsume(queueName, false, laobanDeliverCallback, consumerTag -> {
            });
            channel.basicConsume(queueName2, false, waibaoDeliverCallback, consumerTag -> {
            });


            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()) {
                String userInput = scanner.nextLine();
                String[] strings = userInput.split(" ");
                if (strings.length < 1) {
                    continue;
                }
                String message = strings[0];
                String routingKey = strings[1];

                channel.basicPublish(WORK_EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
                System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
            }

        }
    }
    //..
}

6.3.3消费者代码

消费者主要进行创建工作队列,并且给工作队列绑定死信交换机,消费消息。

java 复制代码
public class DlxDirectConsumer {

    private static final String DEAD_EXCHANGE_NAME = "dlx-direct-exchange";

    private static final String WORK_EXCHANGE_NAME = "direct2-exchange";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(WORK_EXCHANGE_NAME, "direct");

        // 指定死信队列参数
        Map<String, Object> args = new HashMap<>();
        // 要绑定到哪个交换机
        args.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        // 指定死信要转发到哪个死信队列
        args.put("x-dead-letter-routing-key", "waibao");

        // 创建队列,随机分配一个队列名称
        String queueName = "xiaodog_queue";
        channel.queueDeclare(queueName, true, false, false, args);
        channel.queueBind(queueName, WORK_EXCHANGE_NAME, "xiaodog");

        Map<String, Object> args2 = new HashMap<>();
        args2.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        args2.put("x-dead-letter-routing-key", "laoban");

        // 创建队列,随机分配一个队列名称
        String queueName2 = "xiaocat_queue";
        channel.queueDeclare(queueName2, true, false, false, args2);
        channel.queueBind(queueName2, WORK_EXCHANGE_NAME, "xiaocat");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback xiaoyuDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            // 拒绝消息
            channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
            System.out.println(" [xiaodog] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        DeliverCallback xiaopiDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            // 拒绝消息
            channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
            System.out.println(" [xiaocat] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        channel.basicConsume(queueName, false, xiaoyuDeliverCallback, consumerTag -> {
        });
        channel.basicConsume(queueName2, false, xiaopiDeliverCallback, consumerTag -> {
        });
    }
}

7.RabbitMQ重点知识

也是面试的重点考点。

1.消息队列的概念,模型,应用场景

消息队列就是有生产者,消费者,生产者将信息推送到队列里,消费者去把从队列中取出,两者其他的事情并没有任何耦合的关系

模型有,一对一,一对多,交换机

一对一使用于,生产者生产出信息后,消费者系统去取信息,其实更加适用于一个系统对接另一个系统,不涉及多系统的

一对多适用于,比如老板发送一次信息,所有员工都能收到,或者一个庞大的系统进行推送了消息,其他子系统都要收到等

fanout交换机适用于,比如每个部门都有自己和老板进行沟通的队列,老板进行发布信息,fanout交换机可以同时推送到所有部门的队列中,直接可以通知到所有的部门

direct交换机可以进行指定一个routingKey,比如老板要给特定的部门进行通知,并且每个部门都有自己的队列存储老板的任务,老板可以根据routingKey的不同,进行去指定派送某任务通知某部门。

topic交换机可以指定一个模糊匹配的routingKey,比如老板要同时将消息派发给多个部门,所以可以通过模糊匹配,匹配到多个部门,进行发送消息

2.交换机的类别,路由的绑定关系

fanout(无路由)

direct(routingKey)

topic(模糊Key)

3.消息可靠性

1.消息的确认机制(ack,nack,reject)

2.消息持久化(durable)

3.消息过期机制

4.死信队列

4.延迟队列(类似死信队列)

5.顺序消费,消费幂等特性

6.可拓展性(仅作了解)

集群

故障的恢复机制

镜像

7.运维监控告警(仅作了解)

相关推荐
浮游本尊38 分钟前
Java学习第22天 - 云原生与容器化
java
渣哥2 小时前
原来 Java 里线程安全集合有这么多种
java
间彧3 小时前
Spring Boot集成Spring Security完整指南
java
间彧3 小时前
Spring Secutiy基本原理及工作流程
java
Java水解4 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆6 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学6 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole7 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端
华仔啊7 小时前
基于 RuoYi-Vue 轻松实现单用户登录功能,亲测有效
java·vue.js·后端