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.运维监控告警(仅作了解)

相关推荐
tianyuanwo4 分钟前
技术总结:AArch64架构下Jenkins Agent(RPM容器编译节点)掉线问题分析与排查
java·linux·jenkins
weixin_4565881510 分钟前
【java面试day19】mysql-优化
java·mysql·面试
eqwaak026 分钟前
科技信息差(8.26)
大数据·开发语言·人工智能·编辑器
华仔啊1 小时前
别再问了!Java里这几种场景,用抽象类就对了
java·后端
明天过后01221 小时前
PDF文件中的相邻页面合并成一页,例如将第1页和第2页合并,第3页和第4页合并
java·python·pdf
黑客影儿1 小时前
在Godot中为您的游戏添加并控制游戏角色的完整技术指南
开发语言·游戏·游戏引擎·godot·gdscript·游戏开发·3d游戏
tingting01191 小时前
Spring Boot 外部配置指定不生效的原因与解决
java·spring boot·后端
用户0332126663672 小时前
Java 设置 Excel 行高列宽:告别手动调整,拥抱自动化高效!
java·excel
2501_909686702 小时前
基于SpringBoot的网上点餐系统
java·spring boot·后端
neoooo2 小时前
Spring Boot 3 + Kafka 实战指南
java·spring boot·kafka