RabbitMQ的理论与实现

RabbitMQ

一、简介

是一款开源的,基于Erlang语言开发的消息中间件

第一章节属于基础理论性的知识点梳理,如果想深入了解RabbitMQ,最好是对照知识点,结合书本和官方文档,将不清楚的深入学习。如果只是想应用,可以跳过,看第二、三部分。

1.特性

  • 平台无关性

  • 传输协议 Advanced Message Queuing Protocol (AMQP),不仅定义了传输协议,也定义了服务器端的服务和行为

  • Binding 绑定 一套规则,告诉交换器消息应该存储到哪个队列

  • Queue 队列

  • Exchange 交换器

    消息代理服务器中用于把消息路由到队列的组件存储消息的数据结构,位于硬盘或内存中

  • 轻量级

    核心功能运行插件只需要不到40MB

  • 支持多种编程语言

    java、Ruby、Python、PHP、JavaScript、C#等

  • 可持久化到硬盘

  • 多层安全

    可使用SSL协议通信

2.作用

  • 应用解耦

  • 异步执行任务

  • 流量消峰

  • 副作用

    • 降低系统的可用性

    • 系统复杂度提高

    • 一致性问题,消息消费异常业务不一致问题

3.AMQP

3.1 什么是AMQP

  • AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。

  • AMQP只是一种规范协议,Erlang的实现是即是RabbitMQ的AMQP。

  • 双向通讯协议

    客户端和服务器端可以相互发送请求

  • 请求响应

    • 服务器端通过Connection.Start命令问候语来启动命令/响应序列

      • Connection 类

      • Start 方法

    • 客户端通过Connection.StartOk响应帧来响应RPC请求

    • 客户端要连接到服务端需要3个同步RPC请求

      • 启动

      • 调整

      • 打开连接

3.2AMQP帧组件

帧数据结构,存储与RabbitMQ交互命令的参数

3.2.1 帧类型

3.2.1.1 协议头帧

用于连接到RabbitMQ,仅使用一次

3.2.1.2 方法帧

携带RabbitMQ发送接收到的RPC请求或响应 类、方法、相关参数

Basic.Publish方法帧

  • Basic类

  • Publish方法

  • 交换器名称

  • 路由键值

  • Mandatory标志

    • 告知RabbitMQ必须投递成功 否则发布消息就是失败的

    • 客户端监听返回,失败返回Basic.return

    • 如果有定义备用交换机,消息会被路由到备用交换机,而不会发送失败信息给客户端,消息会丢失

3.2.1.3 内容头帧

包含一条消息的大小和属性

3.2.1.3.1 属性

存储在Basic.properties映射表

  • content-type 消息体类型

  • content-encoding 消息编码

    • 使用gzip压缩消息体时, 可以指定以什么方式压缩,以便解码

  • message-id和correlation-id 唯一标识和跟踪

  • timestamp 创建时间

  • expiration 消息过期时间

    • 如果在这个时间消息没有被消费,RabbitMQ会抛弃这条消息,重新发布到服务器也是

  • app-id和user-id 追踪出现问题的消息发布者应用

  • type 定义消费者和发布者之间的契约

  • relay-to 实现响应消息的路由

  • headers映射表定义自由格式的属性和实现RabbitMQ路由

    • 键值对表

      • key ASCII或Unicode字符串, 最大长度为255个字符

      • value 任意有效的AMQP值类型

  • delivery-mode

    • 1 非持久化消息 2 持久化消息

  • priority 优先级

    • 0~255 越小消费者越优先接收到

  • cluster-id/reserverd 集群ID/保留字段

    • 客户端不能使用的字段

3.2.1.3.2 组成
  • 消息体大小

  • 标志值指明被设置的属性

  • 内容类型 application/json

  • app_id属性

  • timestamp时间戳二进制打包数值

  • 投递属性值,设置为1,消息将被持久化到硬盘

3.2.1.4 消息体帧

包含消息的内容

3.2.1.5 心跳帧
  • 确保客户端与服务器端的连接可用且在正常工作

  • 发送请求给客户端,如果没有响应则会断开连接

  • 默认间隔发送时间为600秒

3.3 AMQP事务

通过这种机制,消息可以批量发布

  • 启动事务

    channel.txSelect();

  • 提交事务

    channel.txCommit();

  • 回滚事务

    channel.txRollback();

3.4 HA队列

  • 高可用队列,发布时会同步副本到RabbitMQ集群的每个节点

  • 有一个主服务器节点,其他为从服务节点,当主服务节点宕机,从节点会升级为主节点

  • 从服务器只做同步操作,不消费消息

  • 当任意节点消费,集群中所有消息副本都会删除

  • 可以配置发布到一部分节点,或者所有集群节点

  • 宕机重新添加的节点不会包含原有节点的消息

3.5 消息队列容量问题

3.5.1 消息大小超长
  • 最大2G

  • 客户端与服务器端的传输帧为128K,超过这个长度会自动拆分多个,太大会影响传输效率甚至宕机,所以不建议消息太大。

3.5.2 队列溢出
  • overflow值设置为reject-publish 丢弃队列头部较老的消息

  • 开启了发布确认模式 basic.nack方法通知发送者消息被拒绝

3.5.3 TCP背压
  • 建立连接时,设置一个可信阀值,每发布一个消息会减1,消息消费之后会加回1,当超过这个值时,RabbitMQ会调用Connection.Blocked();阻塞客户端,不再接收消息,直到可用时,调用Connection.UnBlocked();

3.6 查询连接状态

  • 使用基于Web的管理API来查询

    • 使用频率过高,可能会造成RabbitMQ服务器不必要的负载

  • 提供Restful URL的管理API

    • name、node、connection_details、consumer_count、client_flow_blocked(是否应用TCP背压)

    • 其他信息

3.7 获取消息的两种RPC请求方式

3.7.1 Basic.Get 轮询模型
  • 客户端想要接收消息的时候就要发送一个Basic.Get请求,需要不停的轮询发送

    • 如果有消息,返回Basic.GetOk 没有消息,返回Basic.GetEmpty

  • 每条消息都会产生和RabbotMQ同步通信的开销 性能比Consume模型差

3.7.2 Basic.Consume 推送模型
3.7.2.1 客户端想要接收消息
  • 发送一个Basic.Consume

  • RabbitMQ返回Basic.ConsumeOk

  • 当有消息的时候,服务端会主动给客户端发送推送消息请求Basic.Deliver 客户端返回Basic.Ack确认消息

  • 直到客户端给服务端发送Basic.Cancel 服务端返回Basic.CancelOk

3.7.2.2 Consumer Tag 消费者标签
  • 当客户端发送Basic.Consume创建一个唯一的字符串,服务端发送消息时,会将这个标签一起发送给消费者

  • 消费者通过标签来实现不同类型的操作

3.7.2.3 no_ack接收的方式
  • 服务端不断给消费者发送消息,直到套接字缓冲区填满为止,无需客户端确认

  • 套接字是通信的基石,是支持TCP/IP协议的路通信的基本操作单元

  • 套接字缓冲区

    • 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区

    • Linux操作系统的套接字缓存冲区默认大小为128KB net.core.rmem_default 默认值 net.core.rmem_max 最大值

3.7.2.4 Prefetch 预取
  • 确认消息接收之前,消费者可以预先要求接收一定数量的消息

  • 需要no_ack=false,设置才会生效

3.7.2.5 multiple 批量确认
  • 消费者一次确认多个消息

  • 较少网络通信量,提高消息吞吐量

  • 如果消费者在确认之前出现问题,所有消息都会重新进入队列

3.7.2.6 消息踢回机制
  • Basic.Reject 拒绝

    • 只可以单条

    • 拒绝后根据requeue标志 决定丢弃消息或重新入队

    • 重新入队的消息会被标记redelivered

  • Basic.Nack 否定确认 negative acknowledgment

    • 可以单条或批量

  • 死信交换器 Dead-Letter exchange,DLX

    • 用来保存被拒绝不重新入队的消息, 方便分析问题

    • 队列绑定死信交换机,创建队列时声明

      • x-dead-letter-exchange

      • X-dead-letter-routing-key

    • 消息成为死信的三种情况

      • 队列消息长度达到限制

        • x-max-length

      • 消费者拒绝消息,basicNack/basicReject,并且不把消息重新放入队列,request=false

      • 队列存在消息过期设置,消息过期未消费

        • x=message-ttl

    • 死信交换机有多个队列,都会被路由到吗?

3.8 队列控制

定义队列时,可以设置队列的多种行为参数 一旦创建了就不能修改,只能删除重建

3.8.1 autoDelete 自动删除自己
  • 当消费者消费完消息,连接断开后, 队列自动删除

  • 需要发送Basic.Consume请求

3.8.2 exclusive 独占/排它
  • 只允许一个消费者进行消费

  • 当消费者消费完消息,连接断开后, 队列自动删除

3.8.3 TTL 自动过期的消息
  • 使用x-expiration定义队列

  • 限制

    • 只有在没有消费者的情况下才会过期

    • 队列只有在TTL周期之类没有收到Basic.Get请求时才会过期

    • 不能重新声明和更改过期时间

    • RabbitMQ不能保证删除过期队列的实效性

3.8.4 保持有限数量的消息
3.8.5 将旧消息推出堆栈
3.8.6 永久队列
3.8.7 最大长度队列
  • x-max-length

3.9 延迟队列

消息进入队列后,不会立即被消费,只有到达指定时间后,才会被消费。RabbitMQ原生不支持延迟消费功能,需要通过 TTL + 死信队列 来实现相同效果

3.9.1 应用场景
  • 下单后,30分钟未支付,取消订单,会滚库存

  • 新用户注册7天后,发送短信问候

3.10 日志与监控
  • 日志存放路径

    • 默认 /var/log/rabbitmq/rabbit@主机名.log

  • Web管控台

    • 默认 主机IP:15672

  • 终端命令 rabbitmqctl


     

3.11 消息追踪

记录消息的投递过程,用于排查消息丢失情况

  • 生产者或消费者断开,与RabbitMQ采用了不同的确认机制

  • 队列与交换机不同的转发策略

  • 交换机没有和队列绑定,生产者不感知

  • 集群策略问题

Firehouse

  • 将用户发送给RabbbitMQ的消息,以一定的格式投递给默认的交换机( amq.rabbitmq.trace)

  • amq.rabbitmq.trace

    • Topic类型交换机

    • routing key

      • publish.exchangename

      • deliver.queuename

  • 开启/关闭命令

    • rabbitmqctl trace_on rabbitmqctl trace_off

4.四大核心

  • 生产者

  • 消费者

    • 消息应答

      • 自动应答

      • 手动应答

        • 批量

        • 单条

  • 交换机

  • 队列

5.消息发布模式

  • 简单模式

  • Work queues 工作模式

    • 轮询获取消息

  • Publish/Subsrcibe 发布订阅模式

  • Routing 路由模式

  • Topics 主题模式

  • Publisher Confirms 发布确认模式

6.消息路由模式

  • Direct交换器

  • Fanout交换器

  • Topic交换器

  • Headers交换器

7.Broker接收和分发消息的应用

Message Broker就是RabbitMQ服务器

8.Virtual host

多租户和安全因素设计

9.Connection

Publish/Consumer和Broker之间的TCP连接

10.Channel

  • 访问只建立一次Connection,在内部建立逻辑连接Channel

  • Channel连接之间相互隔离

  • 减少和系统建立TCP连接的开销

11.Binding

  • Exchange和Queue之间的虚拟连接

  • 包含Routing key,保存在Exchange中的查询表中

  • 用于消息分发的依据

二、安装

1.RabbitMQ依赖Elang,需要先安装Elang

这里介绍最简单的homebrew方式,其他方式可以网上自行搜索。

如果你的操作系统安装了homebrew,可以直接终端使用命令 brew install erlang 进行安装。如果是报错了,可能是HomeBrew版本问题,可以使用 brew update,先更新homebrew。

关于homebrew如何安装,可以观看我的另一篇文章Mac安装Homebrew-CSDN博客

2.下载RabbitMQ

RabbitMQ的下载,这里我也是用最简单的homebrew方式安装。

brew install rabbitmq

  • 配置环境变量

修改用户目录下的 .zshrc 环境变量,该文件可能不存在,如果没有自行创建

打开配置文件  open .zshrc

添加以下配置

export RABBITMQ_HOME=/opt/homebrew/opt/rabbitmq rabbitmq的安装目录 export PATH=$PATH:$RABBITMQ_HOME/sbin

使配置文件生效  source .zshrc

  • 后台启动服务

sudo rabbitmq-server -detached

  • 查看rabbitmq状态

sudo rabbitmqctl status

默认账号guest/guest

  • 关闭后台服务

rabbitmqctl stop

3.账号相关

登录 rabbitmqctl login -u guest -p guest

创建虚拟主机(virtual host)
rabbitmqctl add_vhost <vhost_name>

3.1 创建用户

  • rabbitmqctl add_user admin 123

3.2 设置用户角色

  • rabbitmqctl set user_tags admin administrator

3.3 设置用户权限

  • rabbitmqctl set_permissions -p vhostpath username “.” “.” “.*”

    参数的代表的意思

  • vhostpath:虚拟主机路径

  • username:用户名

  • 正则表达式1:配置访问权限

  • 正则表达式2:读队列权限

  • 正则表达式3:写队列权限

  • 比如授予用户Jerry_LAN,在 / 路径下的所有权限

  • rabbitmqctl set_permissions -p / Jerry_LAN ".*" ".*" ".*"

  • 给用户设置标签

    rabbitmqctl set_user_tags Jerry_LAN administrator

3.4 查询用户和角色

  • rabbitmqctl list_users

  • 查看权限

    rabbitmqctl list_user_permissions Jerry_LAN

三、使用

1.声明交换器

  • Exchange.Declare

  • 创建成功 RabbitMQ返回Exchange.DeclareOK

  • 创建失败 Channel.close关闭命令通道

    返回编码和文本值

2.创建队列

  • Queue.Declare

    可以重复发送,后面的命令返回队列处理消息的数量、订阅数量等信息

  • Queue.close

    监听处理RabbitMQ返回的错误信息

3.绑定队列到交换器

  • Queue.bind

    成功返回Queue.bindOK

4.发布消息到队列

  • 至少需要消息头帧、Basic.publish方法帧、消息体帧

  • delivery-mode 是否持久化到磁盘

5.消费消息

  • Basic.Consume订阅队列

    • no_ack参数 接收的方式

      • 设置为true,无需确认消息

      • 设置为false,客户端需要发送Basic.Ack确认每条消息收到

      • delivery tag 投递标签和信道作为唯一标识符来实现消息的确认、拒绝

  • 成功,服务器返回Basic.consumeOK

  • 客户端以Basic.deliver接收消息

  • 如果客户端想停止接收消息,异步发送Basic.Cancel

6. RabbitMQ Java开发部分

6.1 添加Pom依赖

Maven Central: com.rabbitmq:amqp-client 官网各个版本的下载路径

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

6.2 Simple 简单模式

6.2.1 创建生产者

  • 创建连接工厂

    • ConnectionFactory factory = new ConnectionFactory();

  • 设置IP

    • factory.setHost(“127.0.0.1”);

  • 设置虚拟主机(租户)

    • setVirtualHost(virtualHost);

  • 设置用户名密码

    • factory.setUserName(“admin”); factory.setPassWord(“123”);

  • 创建连接

    • Connection conn = factory.newConnection();

  • 获取信道

    • Channel channel = cconn.createChannel();

  • 创建一个队列

    • channel.queueDeclare(queueName, durable, exclusive, autoDelete, arguments);

      • 队列名

      • 是否持久化

      • 队列是否只供一个消费者消费,是否进行消息共享

      • 是否自动删除,最后一个消费者断开连接后,队列是否自动删除

      • 其他参数

  • 发送消息

  • channel.basicPublish(exchange, queueName, props, body);

    • 交换机

      • 不添路由到默认交换机

    • 队列名

    • 其他参数

    • 消息体

  • 代码demo

    package org.example.simple;
    
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * ClassName:
     * Description: 简单模式下的生产者
     * @Date: 2023/2/25 18:50
     * @author: Mr.LAN
     */
    public class ServerApplicationTest {
        public static void main(String[] args) throws IOException, TimeoutException {
            try {
                String rabbitmq_host = "localhost";
                int rabbitmq_port = 5672;//注意这里端口不是管理界面的15672
                String userName = "Jerry_LAN";
                String passWord = "Jerry_LAN";
                String virtualHost = "/dh_host";
                ConnectionFactory factory = new ConnectionFactory();
                factory.setHost(rabbitmq_host);
                factory.setPort(rabbitmq_port);
                factory.setUsername(userName);
                factory.setPassword(passWord);
                factory.setVirtualHost(virtualHost);
                Connection connection = factory.newConnection();
                Channel channel = connection.createChannel();
    
                channel.queueDeclare("myTestQueue", true, false, false, null);
                byte[] body = "hello rabbitMQ".getBytes();
                channel.basicPublish("", "myTestQueue", null, body);
                System.out.println("创建成功");
            } catch (IOException | TimeoutException e) {
                throw new RuntimeException(e);
            }
        }
    }

    运行如下,如果出现报错,请检查

  • virtualHost是否存在

  • 账号是否存在

  • 账号是否有virtualHost的权限

  • 地址端口是否正确

        

6.2.2 创建消费者

  • 创建连接工厂

    • ConnectionFactory factory = new ConnectionFactory();

  • 设置IP

    • factory.setHost(“127.0.0.1”);

  • 设置虚拟主机(租户)

    • setVirtualHost(virtualHost);

  • 设置用户名密码

    • factory.setUserName(“admin”); factory.setPassWord(“123”);

  • 创建连接

    • Connection conn = factory.newConnection();

  • 获取信道

    • Channel channel = cconn.createChannel();

  • 接收消息

    • channel.basicConsume(queueName, autoAck, DeliverCallBack, CancelCallBack);

      • 队列名

      • 消费成功后是否自动应答

      • 消费失败回调

      • 消费成功回调

  • 消费失败回调

    • CancelCallback cancelCallback = (consumerTag) ->{ System.out.println("消费失败回调"); };

  • 消费成功回调

    • DeliverCallback deliverCallback = (String consumerTag, Delivery delivery)-> { System.out.println(consumerName + "消费成功回调"); channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); };

  • 消息确认

    • boolean multiple = false; channel.basicAck(delivery.getEnvelope().getDeliveryTag(), multiple)

    • multiple 批量确认消息

  • 代码demo

package org.example.simple;

import com.rabbitmq.client.*;

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

/**
 * ClassName:
 * Description: 简单模式下的消费者
 *
 * @Date: 2023/2/26 13:43
 * @author: Mr.LAN
 */
public class ClientTest {
    public static void main(String[] args) {
        try {
            String rabbitmq_host = "localhost";
            int rabbitmq_port = 5672;
            String userName = "Jerry_LAN";
            String passWord = "Jerry_LAN";
            String virtualHost = "/dh_host";
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost(rabbitmq_host);
            factory.setPort(rabbitmq_port);
            factory.setUsername(userName);
            factory.setPassword(passWord);
            factory.setVirtualHost(virtualHost);
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();

            //lambda表达式,等价于上面的匿名内部类写法
            DeliverCallback deliverCallback = (String consumerTag, Delivery message)-> {
                System.out.println("消费成功回调");
                System.out.println("consumerTag: " + consumerTag);
                System.out.println("message: " + message);
                System.out.println("message.getEnvelope(): " + message.getEnvelope());
                System.out.println("message.getEnvelope().getDeliveryTag(): " + message.getEnvelope().getDeliveryTag());
                System.out.println("message.getEnvelope().getExchange(): " + message.getEnvelope().getExchange());
                System.out.println("message.getEnvelope().getRoutingKey(): " + message.getEnvelope().getRoutingKey());
                System.out.println("message.getProperties(): " + message.getProperties());
                System.out.println("message.getBody(): " + new String( message.getBody()));
            };

            CancelCallback cancelCallback = (consumerTag) ->{
                System.out.println("消费失败回调");
                System.out.println("consumerTag:" + consumerTag);
            };

            channel.basicConsume("myTestQueue", true, deliverCallback, cancelCallback);
            System.out.println("消费成功");
        } catch (IOException | TimeoutException e) {
            throw new RuntimeException(e);
        }

    }
}

  • 运行结果如下

6.2.3 WokerQueue 工作队列

  • 一个队列对应多个消费者

  • RabbitMQ默认采用轮询分发消息

  • 不公平分发模式

    • 消费者设置 channel.basicQos(1);

    • 如果其他消费者在忙,优先分配给空闲的消费者

  • 预取值

在RabbitMQ中,Worker Queue模式通常用于并行处理大量任务。这种模式涉及到一个任务队列和多个工作进程。为了确保工作进程能够正确地并行处理任务,需要确保以下几个因素:

  • 队列的预期值(x-max-length)应该足够大,以便能够存储待处理的任务。
  • 工作进程的数量应该根据可用资源(CPU、内存等)进行适当设置。

  • 代码demo

前面我们已经知道如果获取连接了,这里我们先封装一个工具类RabbitMQUtil,用于获取连接,后面的案例也会直接用这个工具类。

package org.example.utils;

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

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

/**
 * ClassName:
 * Description:
 *
 * @Date: 2023/2/26 14:38
 * @author: Mr.LAN
 */
public class RabbitMQUtil {
    private static final String rabbitmq_host = "localhost";
    private static final int rabbitmq_port = 5672;
    private static final String userName = "Jerry_LAN";
    private static final String passWord = "Jerry_LAN";
    private static final String virtualHost = "/dh_host";

    public static Channel getChannel() {
        try {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost(rabbitmq_host);
            factory.setPort(rabbitmq_port);
            factory.setUsername(userName);
            factory.setPassword(passWord);
            factory.setVirtualHost(virtualHost);
            Connection connection = factory.newConnection();
            return connection.createChannel();
        } catch (IOException e) {
            System.out.println("获取RabbitMQ连接异常:" + e);
            throw new RuntimeException(e);
        } catch (TimeoutException e) {
            System.out.println("获取RabbitMQ连接超时:" + e);
            throw new RuntimeException(e);
        }
    }

    public static void close() {
        try {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost(rabbitmq_host);
            factory.setPort(rabbitmq_port);
            factory.setUsername(userName);
            factory.setPassword(passWord);
            factory.setVirtualHost(virtualHost);
            Connection connection = factory.newConnection();
            connection.close();
        } catch (IOException e) {
            System.out.println("获取RabbitMQ连接异常:" + e);
            throw new RuntimeException(e);
        } catch (TimeoutException e) {
            System.out.println("获取RabbitMQ连接超时:" + e);
            throw new RuntimeException(e);
        }
    }
}

定义一个生产者、消费者公用的参数类,可以根据自己需要,不一定要创建

package org.example.workqueue.pojo;

/**
 * ClassName:
 * Description:
 *
 * @Date: 2023/2/26 16:21
 * @author: Mr.LAN
 */
public class QueueInfo {
    public static final String QUEUE_NAME = "workQueueTest";
    public static final String EXCHANGE = "";
}

  • 创建生产者
package org.example.workqueue;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import org.example.utils.RabbitMQUtil;
import org.example.workqueue.pojo.QueueInfo;

import java.io.IOException;
import java.util.Scanner;

/**
 * ClassName:
 * Description: 工作队列生产者
 *
 * @Date: 2023/2/26 16:19
 * @author: Mr.LAN
 */
public class ProducerWorkQueueTest {
    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //durable 持久化队列
        channel.queueDeclare(QueueInfo.QUEUE_NAME, true, false, false, null);
        //这里使用控制台输入的方式发送消息
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String message = scanner.next();
            byte[] body = message.getBytes();
            //MessageProperties.PERSISTENT_TEXT_PLAIN 消息持久化磁盘
            channel.basicPublish(QueueInfo.EXCHANGE, QueueInfo.QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, body);
            System.out.println("消息发布成功");
        }

    }
}

  • 创建两个消费者

因为这里工作队列默认采用轮询分发消息,为了验证会轮询分发,我们创建两个消费者。

package org.example.workqueue;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
import org.example.utils.RabbitMQUtil;
import org.example.workqueue.pojo.QueueInfo;

import java.io.IOException;

/**
 * ClassName:
 * Description: 工作队列消费者A
 *
 * @Date: 2023/2/26 16:30
 * @author: Mr.LAN
 */
public class ConsumerWorkQueueTestA {
    private static final String consumerName = "consumer-A";
    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        channel.queueDeclare(QueueInfo.QUEUE_NAME, true, false, false, null);

        DeliverCallback deliverCallback = (String consumerTag, Delivery message)-> {
            System.out.println(consumerName + "消费成功回调");
            System.out.println("consumerTag:" + consumerTag);
            System.out.println("message:" + message);
            System.out.println("message.getBody():" + new String( message.getBody()));
        };

        CancelCallback cancelCallback = (consumerTag) ->{
            System.out.println("消费失败回调");
            System.out.println("consumerTag:" + consumerTag);
        };

        channel.basicConsume(QueueInfo.QUEUE_NAME, true, deliverCallback, cancelCallback);
        System.out.println(consumerName + "消费者启动成功");
    }
}
/**
 * ClassName:
 * Description: 工作队列消费者B
 *
 * @Date: 2023/2/26 16:30
 * @author: Mr.LAN
 */
class ConsumerWorkQueueTestB{
    private static final String consumerName = "consumer-B";
    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        channel.queueDeclare(QueueInfo.QUEUE_NAME, true, false, false, null);

        DeliverCallback deliverCallback = (String consumerTag, Delivery message)-> {
            System.out.println(consumerName + "消费成功回调");
            System.out.println("consumerTag:" + consumerTag);
            System.out.println("message:" + message);
            System.out.println("message.getBody():" + new String( message.getBody()));
        };

        CancelCallback cancelCallback = (consumerTag) ->{
            System.out.println("消费失败回调");
            System.out.println("consumerTag:" + consumerTag);
        };

        channel.basicConsume(QueueInfo.QUEUE_NAME, true, deliverCallback, cancelCallback);
        System.out.println(consumerName + "消费者启动成功");
    }
}

  • 运行结果如下

生产者这里使用控制台输入的方式发送消息,我们在控制台输入两次消息回车

“发送第1条消息”,“发送第2条消息”

这里可以看出,消息是分别轮询分发给两个消费者。

另外两种分发方式在这里就不演示了,大家按照简介去设置参数即可。

6.2.4 Publish/subscribe 发布订阅

  • 生产者将消息发布到交换机,交换机将消息发布到绑定了该交换机的每个队列 发布订阅模式使用 fanout 交换机

  • 创建交换机

    • channel.exchangeDeclare("exchange_fanout", BuiltinExchangeType.FANOUT,true);

      • 交换机名称

      • 交换机类型

      • 交换机是否持久化

  • 创建队列

    • channel.queueDeclare("exchange_queueA", true, false, false, null);

    • channel.queueDeclare("exchange_queueB", true, false, false, null);

  • 交换机绑定队列

    • channel.queueBind("exchange_queueA", "exchange_fanout", "");

      • 队列名

      • 交换机名

      • 路由关键字 发布订阅模式写””即可

  • 发布消息

    • channel.basicPublish("exchange_fanout", "", null, message.getBytes());

  • 消费消息

    • channel.basicConsume("exchange_queueA", true, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body,"UTF-8"); System.out.println("接收到消息:"+message); } });

  • 代码demo

        这里创建一个消息发布者、两个消息订阅者,发布订阅模式下,一个消息发布,所有的订阅者都可以看到消息。有点类似于微信公众号订阅,只要订阅了公众号,公众号发布消息所有订阅的人都可以看到消息。

  • 创建消息发布者
package org.example.publishSubscribe;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import org.example.utils.RabbitMQUtil;

import java.io.IOException;

/**
 * ClassName:
 * Description: 发布订阅模式-消息发布者
 *
 * @Date: 2023/3/12 1:57
 * @author: Mr.LAN
 */
public class Publisher {

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //创建交换机
        channel.exchangeDeclare("exchange_fanout", BuiltinExchangeType.FANOUT, true);

        //创建A、B队列
        channel.queueDeclare("exchange_queueA", true, false, false, null);
        channel.queueDeclare("exchange_queueB", true, false, false, null);

        //队列绑定交换机
        channel.queueBind("exchange_queueA", "exchange_fanout", "");
        channel.queueBind("exchange_queueB", "exchange_fanout", "");

        //发布消息
        byte[] message = "这是发布订阅的消息".getBytes();
        channel.basicPublish("exchange_fanout", "", null, message);
        System.out.println("消息发送成功");
    }

}

  • 创建消息订阅者两个
package org.example.publishSubscribe;

import com.rabbitmq.client.*;
import org.example.utils.RabbitMQUtil;

import java.io.IOException;

/**
 * ClassName:
 * Description: 发布订阅模式-消息订阅者
 *
 * @Date: 2023/3/12 1:57
 * @author: Mr.LAN
 */
public class Subscriber {

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //创建交换机
        channel.exchangeDeclare("exchange_fanout", BuiltinExchangeType.FANOUT, true);

        channel.basicConsume("exchange_queueA", true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("exchange_queueA接收到消息: " + message);
            }
        });

    }


}

class SubscriberB{
    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //创建交换机
        channel.exchangeDeclare("exchange_fanout", BuiltinExchangeType.FANOUT, true);

        channel.basicConsume("exchange_queueB", true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("exchange_queueB接收到消息: " + message);
            }
        });

    }
}

  • 运行结果如下

从结果可以看到,当一个消息发布后,两个订阅者都能收到相同的消息。

6.2.5 Rounding 路由模式

  • 简介

    • 路由模式是发布订阅模式的升级版,为了不将消息无差别的发送给交换机上的所有队列

    • 生产者先将消息发布到交换机,再由交换机根据不同的RoutingKey将消息分发给对应RoutingKey的队列

    • 默认使用Direct交换机

    • 一个队列可以绑定多个RoutingKey

  • 创建交换机

    • channel.exchangeDeclare("exchange_routing", BuiltinExchangeType.DIRECT, true);

  • 创建队列

    • channel.queueDeclare("queueA", true, false, false, null);

  • 交换机通过RoutingKey绑定队列

    • channel.queueBind("queueA", "exchange_routing", "routing_key");

  • 发送消息

    • channel.basicPublish("exchange_routing", "routing_key", null, "routing_key".getBytes());

  • 代码demo
  • 创建路由模式生产者
package org.example.routing;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import org.example.utils.RabbitMQUtil;

import java.io.IOException;

/**
 * ClassName:
 * Description: 路由模式-生产者
 *
 * @Date: 2023/3/16 0:15
 * @author: Mr.LAN
 */
public class RoutingProducer {

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //创建交换机
        channel.exchangeDeclare("exchange_routing", BuiltinExchangeType.DIRECT, true);

        //创建A、B队列
        channel.queueDeclare("exchange_queueA", true, false, false, null);
        channel.queueDeclare("exchange_queueB", true, false, false, null);

        //队列绑定交换机
        channel.queueBind("exchange_queueA", "exchange_routing", "routingKeyA");
        channel.queueBind("exchange_queueB", "exchange_routing", "routingKeyB");

        //发布消息
        byte[] messageA = "这是发送给routingKeyA的消息".getBytes();
        byte[] messageB = "这是发送给routingKeyB的消息".getBytes();
        channel.basicPublish("exchange_routing", "routingKeyA", null, messageA);
        channel.basicPublish("exchange_routing", "routingKeyB", null, messageB);
        System.out.println("消息发送成功");
    }

}

  • 创建路由模式消费者
package org.example.routing;

import com.rabbitmq.client.*;
import org.example.utils.RabbitMQUtil;

import java.io.IOException;

/**
 * ClassName:
 * Description: 路由模式-消费者
 *
 * @Date: 2023/3/16 0:20
 * @author: Mr.LAN
 */
public class RoutingConsumer {
    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //创建交换机
        channel.exchangeDeclare("exchange_routing", BuiltinExchangeType.DIRECT, true);

        channel.basicConsume("exchange_queueA", true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("exchange_queueA接收到消息: " + message);
            }
        });
    }

}

class RoutingConsumerB{
    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //创建交换机
        channel.exchangeDeclare("exchange_routing", BuiltinExchangeType.DIRECT, true);

        channel.basicConsume("exchange_queueB", true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("exchange_queueB接收到消息: " + message);
            }
        });

    }
}

  • 运行结果如下

从结果可以看到发布的消息,都在同一个交换机,但是会根据不同的路由的key转发到具体的消费者。

6.2.6 Topic 通配符模式

  • 简介

    • 通配符模式是路由模式的升级

    • 消息设置RoutingKey时,可以指定由多断拼接,中间以 . 分割

    • 队列设置RoutingKey时,#可以匹配任意多个单词,*可以匹配任意一个单词

  • 创建交换机

    • channel.exchangeDeclare("exchange_topic", BuiltinExchangeType.TOPIC, true);

  • 创建队列

    • channel.queueDeclare("queueA", true, false, false, null);

  • 交换机绑定队列

    • channel.queueBind("queueA", "exchange_topic","#.wildcard.#");

  • 代码demo

  • 创建通配符模式生产者

package org.example.topic;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import org.example.utils.RabbitMQUtil;

import java.io.IOException;

/**
 * ClassName:
 * Description: 通配符模式-生产者
 *
 * @Date: 2023/3/16 0:25
 * @author: Mr.LAN
 */
public class TopicProducer {
    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //创建交换机
        channel.exchangeDeclare("exchange_topic", BuiltinExchangeType.TOPIC, true);

        //创建A、B队列
        channel.queueDeclare("exchange_queueA", true, false, false, null);
        channel.queueDeclare("exchange_queueB", true, false, false, null);

        //发布消息
        byte[] messageA = "这是发送给routingKeyA+B的消息".getBytes();
        byte[] messageB = "这是发送给routingKeyB的消息".getBytes();
        byte[] messageC = "这是发送给*.other.*的消息".getBytes();

        channel.basicPublish("exchange_topic", "routingKeyA", null, messageA);
        channel.basicPublish("exchange_topic", "routingKeyB", null, messageB);
        channel.basicPublish("exchange_topic", "G.other.H", null, messageC);
        System.out.println("消息发送成功");
    }
}

  • 创建通配符模式消费者

package org.example.topic;

import com.rabbitmq.client.*;
import org.example.utils.RabbitMQUtil;

import java.io.IOException;

/**
 * ClassName:
 * Description:
 *
 * @Date: 2023/3/16 0:26
 * @author: Mr.LAN
 */
public class TopicConsumer {
    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //创建交换机
        channel.exchangeDeclare("exchange_topic", BuiltinExchangeType.TOPIC, true);

        channel.queueBind("exchange_queueA", "exchange_topic", "routingKeyA.#", null);

        channel.basicConsume("exchange_queueA", true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("exchange_queueA接收到消息: " + message);
            }
        });
    }
}

class TopicConsumerB{
    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //创建交换机
        channel.exchangeDeclare("exchange_topic", BuiltinExchangeType.TOPIC, true);

        channel.queueBind("exchange_queueB", "exchange_topic", "routingKeyA.#", null);//routingKeyA开头,后面带任意个数词
        channel.queueBind("exchange_queueB", "exchange_topic", "routingKeyB.#", null);//routingKeyB开头,后面带任意个数词
        channel.queueBind("exchange_queueB", "exchange_topic", "*.other.*", null); //中间带other的三词

        channel.basicConsume("exchange_queueB", true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("exchange_queueB接收到消息: " + message);
            }
        });
    }
}

  • 运行结果如下

从结果可以看出,消息路由到哪里,是由消费者绑定的队列决定的。生产发布的消息都是明确发布给一个具体的队列,消费者绑定时,使用多个通配符达到可以接收多种生产消息的目的。

6.2.7 发布确认模式

发布确认模式,是为了保证服务端业务处理成功才将消息消费掉的一种模式,只有在服务端成功处理完业务,调用channel.basicAck,队列中的消息才会被消费掉。

  • 单个发布确认

    • channel.confirmSelect(); for (int i = 0; i < 5000; i++) { String message = "message" + i; channel.basicPublish("", queue_name, null, message.getBytes()); boolean flag = channel.waitForConfirms(); }

  • 批量发布确认

    • channel.confirmSelect(); for (int i = 0; i < 5000; i++) { String message = "message" + i; channel.basicPublish("", queue_name, null, message.getBytes()); } boolean flag = channel.waitForConfirms();

  • 异步发布确认

    • 准备消息的监听器

      • ConfirmCallback ackConfirmCallback = new ConfirmCallback() { @Override public void handle(long deliveryTag, boolean multiple) throws IOException { channel.basicAck(deliveryTag, false); } };

        ConfirmCallback unAckConfirmCallback = new ConfirmCallback() { @Override public void handle(long deliveryTag, boolean multiple) throws IOException { } };

    • 添加监听

    • channel.addConfirmListener(ackConfirmCallback, unAckConfirmCallback);

  • 代码demo

  • 创建确认单个发布消息和批量发布消息的生产者

        这里顺便提一下rabbitMQ支持单个发布和批量发布,批量发布效率更高,可以按需使用。

package org.example.ackmode;

import com.rabbitmq.client.Channel;
import org.example.utils.RabbitMQUtil;

import java.io.IOException;

public class MessageAckProducer {
    public static final String ack_model_queue_name = "ackModelQueueTest";

    public static void main(String[] args) throws IOException, InterruptedException {
        singlePublish();
        multiPublish();
    }

    /**
     * 单个发布确认
     */
    private static void singlePublish() throws IOException, InterruptedException {
        Channel channel = RabbitMQUtil.getChannel();
        channel.queueDeclare(ack_model_queue_name, false, false, false, null);
        channel.confirmSelect();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 5000; i++) {
            String message = "message" + i;
            channel.basicPublish("", ack_model_queue_name, null, message.getBytes());
            boolean flag = channel.waitForConfirms();
            if (flag) {
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("单个消息发布耗时:" + (end - start));
    }

    /**
     * 批量发布确认
     */
    private static void multiPublish() throws IOException, InterruptedException {
        Channel channel = RabbitMQUtil.getChannel();
        //durable 持久化队列
        channel.queueDeclare(ack_model_queue_name, false, false, false, null);
        channel.confirmSelect();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 5000; i++) {
            String message = "message" + i;
            channel.basicPublish("", ack_model_queue_name, null, message.getBytes());
        }
        boolean flag = channel.waitForConfirms();
        if (flag) {
        }
        long end = System.currentTimeMillis();
        System.out.println("批量消息发布耗时:" + (end - start));
    }

}

  • 创建ack确认消费者

package org.example.ackmode;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import org.example.utils.RabbitMQUtil;

import java.io.IOException;

public class MessageAckConsumer {
    public static final String ack_model_queue_name = "ackModelQueueTest";

    public static void main(String[] args) throws IOException {
        ackMessage();
    }

    /**
     * 单个消费消息
     */
    private static void ackMessage() throws IOException {
        Channel channel = RabbitMQUtil.getChannel();
        //durable 持久化队列
        channel.queueDeclare(ack_model_queue_name, false, false, false, null);
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            System.out.println("消费消息成功" + message.getEnvelope().getDeliveryTag());
        };
        CancelCallback cancelCallback = consumerTag -> {
        };
        channel.basicConsume(ack_model_queue_name, false, deliverCallback, cancelCallback);
    }

}

  • 运行结果如下 

7.消费端限流

  • 确认模式改为手动确认 acknowledge=“manual”

  • 设置prefetch,一次拉取的数量
    直到手动确认完成该数量以后,才会继续拉取消息

8.应用问题

  • 消息可靠性的保障

在RabbitMQ中,可以通过以下方式保证消息的可靠传递:

  • 确认模式(Confirmations):

对于发送方,可以开启确认模式,这样每个被publish的消息都会返回一个confirmation。如果RabbitMQ接收了消息,它会发送一个确认。如果没有,它会发送一个nack。

  • 持久化(Durable Messages):

队列创建的一个属性。可以将队列和消息都标记为持久化,这样即使RabbitMQ服务重启,消息也不会丢失。

  • 消费者确认(Acknowledgements):

也就是前面的6.2.7 发布确认模式。对于消费者,可以开启消费者确认模式,这样每个消息都会被确认之后才会从RabbitMQ中删除。

  • 预fetch参数(Prefetch Count):

可以设置预fetch参数来限制RabbitMQ发送给单个消费者的未确认消息的数量。

  • 幂等性问题
    幂等性问题也叫解决重复消费的问题,是在高并发时,消息可能重复发送请求,在服务器多次消费同一个消息的问题。

    可以通过服务器保存消息ID,和即将消费的消息ID对比是否一致来解决
     

9.RabbitMQ集群

  • 集群方案的原理

    • 创建多个一个主节点,多个从节点

    • 使用Haproxy作为反向代理转发消息

      • C语言编写

      • 提供高可用、负载均衡

      • 基于TCP/HTTP代理

    • 使用镜像集群配置将主节点交换机/消息同步到从节点

集群这里不做深入的讲解,有需要的话留言,我考虑更新另外一篇文章专门讲解。

=========================================================================
创作不易,请勿直接盗用,使用请标明转载出处。

喜欢的话,一键三连,您的支持是我一直坚持高质量创作的原动力。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值