rabbitmq的概念和使用

官网:https://www.rabbitmq.com/

特性

以下是从官网翻译过来的,由于自己翻译水平有限,可能有纰漏

异步消息传递:支持多种消息传递协议,消息队列,传递确认,灵活的路由到队列,多种交换类型。

分布式部署:集群部署方式可以支持高可用性和高吞吐量;还支持跨多区域的联合部署

企业和云端部署安全:可插拔的认证和授权机制,支持TLS、LDAP。能够在公有云和私有云上轻量和方便的部署

丰富的工具和插件:支持与其他系统的集成

管理和监控:可以使用http-API、命令行工具以及UI进行管理和监控。

1 安装

rabbitmq是使用elang语言开发的,因此安装rabbitmq之前首先需要安装elang。并且,elang和rabbitmq版本之前具有对应关系,我们可以去官网上查看其对应关系,https://www.rabbitmq.com/which-erlang.html

并且注意要根据Linux发行版选择特定的rpm包,我这里是centos7(对应Redhat版本为redhat7,也就是el/7)

这里我选择的安装版本为:erlang-23.3.4.11、rabbitmq-server-3.10.0。我们可以输入el/7进行搜索,然后点击进入对应的rpm版本页面,会提示我们安装命令,如下图所示

安装命令如下:

curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm.sh | sudo bash
sudo yum install erlang-23.3.4.11-1.el7.x86_64
curl -s https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.rpm.sh | sudo bash
sudo yum install rabbitmq-server-3.10.0-1.el7.noarch

安装完成后会看到如下信息

启动服务

[root@localhost software]# systemctl status rabbitmq-server
[root@localhost software]# systemctl start rabbitmq-server
[root@localhost software]# systemctl enable rabbitmq-server

管理页面插件

[root@localhost software]# rabbitmq-plugins enable rabbitmq_management
[root@localhost software]# systemctl restart rabbitmq-server

此时,只允许localhost访问管理页面,如果我们想使用ip访问,需要增加个用户并且授权

[root@localhost software]# rabbitmqctl add_user admin admin
[root@localhost software]# rabbitmqctl set_user_tags admin administrator
[root@localhost software]# systemctl stop firewalld
[root@localhost software]# systemctl disable firewalld

访问页面

至此,安装完成

2 一些概念

概念详解
Exchange 消息交换机,它指定消息按什么规则,路由到哪个队列
Queue 消息队列,每个消息都会被投入到一个或多个队列
Binding 绑定,它的作用就是把 exchange 和 queue 按照路由规则绑定起来
Routing Key 路由关键字,exchange 根据这个关键字进行消息投递
Vhost 虚拟主机,可以开设多个 vhost,用作不同用户的权限分离
Producer 消息生产者,就是投递消息的程序
Consumer 消息消费者,就是接受消息的程序
Channel 消息通道,在客户端的每个连接里,可建立多个 channel,每个 channel 代表一个会话任务

完整架构图

生产者和Vhost建立Connection

生产者和Exchange建立Channel

Exchange根据路由规则发布消息到指定的一些queue

一个queue中的消息只会被一个消费者消费一次,可以被多个消费者同时消费

RabbitMQ 投递过程:

这里的客户端包括:生产者和消费者。只有客户端存在,其对应的connection、channel才会被创建,而且是长连接,中间一般不会中断。客户端退出,其对应的connection会被销毁。

  • 1. 客户端连接到消息队列服务器,打开一个 channel。
  • 2. 客户端声明一个 exchange,并设置相关属性。
  • 3. 客户端声明一个 queue,并设置相关属性。
  • 4. 客户端使用 routing key,在 exchange 和 queue 之间建立好绑定关系。
  • 5. 客户端投递消息到 exchange。
  • 6. 客户端从指定的 queue 中消费信息。

2.1 vhost

官网介绍https://www.rabbitmq.com/vhosts.html,我们可以在amqp0-9-1协议的介绍中找到该链接

rabbitmq是一个多租户系统,一个vhost是一个虚拟机,类似于apache中的virtual host或者nginx中的server block。

connection、exchange、queue、binding、用户权限、策略以及其他一些东西都属于vhost。

AMQP 0-9-1客户端首先需要连接一个特定的vhost,并且授权成功,connection才会建立

vhost的创建

创建一个vhost有三个方式:使用cli工具、使用http api、使用管理页面。

一个新创建的vhost只有一个默认的交换机,没有其他实体,并且没有用户权限,需要使用cli命令授权

使用cli创建vhost参考 4.2 rabbitmqctl 小节

使用http api创建vhost,比如

 curl -u userename:pa$sw0rD -X PUT http://rabbitmq.local:15672/api/vhosts/vh1 

我们可以给vhost设置limit、比如最大连接数,最大队列数等

rabbitmqctl set_vhost_limits -p vhost_name '{"max-connections": 256}'
rabbitmqctl set_vhost_limits -p vhost_name '{"max-queues": 1024}'

2.2 Connection

https://www.rabbitmq.com/connections.html

本质上,rabbitmq是一个运行在5672的进程,客户端和host:5672建立tcp连接。而且是长连接。

AMQP 0-9-1协议中,客户端和vhost的连接本质上是上面讲的tcp长连接。在AMQP协议中一个Connection是一个客户端和vhost的连接,如果这个客户端已经和vhost建立了连接,不会再出现第二个连接。

因为连接是长连接,客户端通常通过订阅消息来进行消息的消费,这样消息会被push到消费者而不是客户端主动pulling消息

AMQP 0-9-1提供了在一个长连接上复用的方式,这就是说一个客户端可以在一个connection上打开多个“轻量级connection”,叫做Channel(在AMQP 1.0中叫Session)

连接过程:

  • 客户端和host:5672建立tcp连接。
  • AMQP协议开始处理工作(建立和vhost的connection、建立channel、建立queue等以及鉴权)。因此,本质上AMQP 0-9-1下层还是使用tcp连接,且连接到5672端口,AMQP 0-9-1协议层的connection是虚拟连接。
  • AMQP 0-9-1协议层的connection连接建立后就一直工作,来和broker通信

2.3 Channel

https://www.rabbitmq.com/channels.html

有时我们希望在一个应用中有多个逻辑上的connection连接到broker,这是可以的,这些逻辑上的connection就是Channels

channel依赖于connection,当connection关闭后,依赖它的channels都会关闭

和connection一样,channel也是长连接。当我们不需要时,我们可以主动关闭channel。(不过在springboot中没见过主动关闭的场景)

2.4 Exchange

https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges

Exchange的分类

  • Direct Exchange
  • Fanout Exchange
  • Topic Exchange
  • Headers Exchange

当我们创建一个vhost后,rabbit会为我们创建几个默认exchange

2.4.1 Default Exchange

当我们创建一个queue,但是不绑定任何exchange时,rabbitmq后台其实默认帮我们绑定了一个交换机,这个交换机是Direct类型的交换机,其名称是(AMQP default)。

且rabbitmq自动为我们创建一个routingKey并且自动将这个队列绑定到默认交换机。且这个routeKey的名称就是queue的名称。

从效果上看,好像是客户端直接将消息传递到队列。

2.4.2 Direct Exchange

  • 使用routing key,把queue和exchange实现绑定关系
  • 当Message到达exchange时,根据消息的routing key,消息被exchange路由到对应的queue
  • 如果一个消息到达exchange后,根据routing key可以路由到多个queue,则就会路由到多个queue。

2.4.3 Fanout Exchange

该交换机会将消息路由到所有与其绑定的queues,而不考虑routing key

2.4.4 Topic Exchange

routing key是根据如下规则定义的:key1.key2.key3。也就是点分单词。路由匹配时,使用#匹配0个或多个单词,*匹配1个单词。

2.4.5 Headers Exchange

 AMQP 0-9-1协议指定了Message可包含Message Header。Header Exchange就是通过匹配消息头中的key=value对来进行消息的路由。

我们创建Exchange时,如果所创建的Exchange类型为Headers Exchange,我们可以设置几个key=value对作为路由匹配规则

匹配过程:

  • 创建Header Exchange
  • queue绑定到exchange时指定一组匹配规则:submitType=alicloud,analysisType=RNA
  • 客户端创建消息实体时,设置Header:x-match=all/any,submitType=alicloud,analysisType=RNA
  • 消息到达Exchange后,当x-match=all时,进行全匹配,匹配成功则将消息路由到指定queue;同理当x-match=any时,只要有一个匹配成功则就将消息路由到指定queue

3 官方文档

https://www.rabbitmq.com/documentation.html

官方文档博大精深,包罗万象,由于时间有限,这里可能只介绍其中少部分,待要深入研究时还是要看官方文档

3.1 configuration

3.1.1 日志文件

我们可以通过如下命令查看日志文件和配置文件位置,默认日志路径为/var/log/rabbitmq/

[root@localhost rabbitmq]# systemctl status rabbitmq-server

 

2023-08-15 01:00:07.999768-07:00 [info] <0.221.0>
2023-08-15 01:00:07.999768-07:00 [info] <0.221.0>  node           : rabbit@localhost
2023-08-15 01:00:07.999768-07:00 [info] <0.221.0>  home dir       : /var/lib/rabbitmq
2023-08-15 01:00:07.999768-07:00 [info] <0.221.0>  config file(s) : (none)
2023-08-15 01:00:07.999768-07:00 [info] <0.221.0>  cookie hash    : 7V++Z9ztbZi80GHvaVK41Q==
2023-08-15 01:00:07.999768-07:00 [info] <0.221.0>  log(s)         : /var/log/rabbitmq/rabbit@localhost.log
2023-08-15 01:00:07.999768-07:00 [info] <0.221.0>                 : /var/log/rabbitmq/rabbit@localhost_upgrade.log
2023-08-15 01:00:07.999768-07:00 [info] <0.221.0>                 : <stdout>
2023-08-15 01:00:07.999768-07:00 [info] <0.221.0>  database dir   : /var/lib/rabbitmq/mnesia/rabbit@localhost

此外,我们可以设置日志输出位置,可输出到文件或者控制台。默认输出到文件。默认的日志级别是error和higher severity messages

3.1.2 配置文件rabbitmq.conf

同样我们可以通过如下命令查看配置文件位置

 [root@localhost rabbitmq]# systemctl status rabbitmq-server 

rabbitmq启动时默认在/etc/rabbitmq/rabbitmq.conf加载配置文件,如果有这个文件,将加载这个配置文件

如果我们想修改这个配置文件的位置,可以使用RABBITMQ_CONFIG_FILE 环境变量指定(经过实验,设置这个环境变量不起作用,原因还未知)

也可在环境变量中配置RABBITMQ_CONFIG_FILES=/path/to/a/custom/location/rabbitmq/conf.d,其中conf.d是一个路径,下面放置配置文件,rabbitmq将会按照字母顺序加载配置文件(经过实验,设置这个环境变量不起作用,原因还未知)

可配置的参数列表在官网上有列表

3.2 消息确认Acknowledgements and Confirms

acknowledgement 既表示收到了消息,也表示所有权的转移,接收方承担了对消息的全部责任。消费者收到消息后,何时进行acknowledgement完全取决于消费者,消费者可以处理完业务逻辑再进行acknowledgement,或者立即acknowledgement

消费者一旦进行了消息确认,broker就可以标记这个消息为已删除

消息acknowledgement是对消费者来说的,confirm是对生产者来说的。

从consumers到rabbitmq的传输过程应答,叫做acknowledgements
rabbitmq对生产者的应答,叫做publisher confirms

从publisher到rabbitmq和rabbitmq到consumers的消息可靠传输是必须的

acknowledgements可分为手动和自动,自动acknowledgements是指当rabbitmq发送了消息到consumer,就认为该消息发送成功。手动acknowledgements的应答信息分以下几种情况
// basic.ack is used for positive acknowledgements消息成功发送到消费者,并且可以删除该消息了
// basic.nack is used for negative acknowledgements (note: this is a RabbitMQ extension to AMQP 0-9-1)
// basic.reject is used for negative acknowledgements but has one limitation compared to basic.nack消息未能成功到达消费者,该消息丢弃
批量确认,可以传递参数进行批量确认
重新入队,basic.reject 和 basic.nack 可以传递参数,是否重新入队

客户端消费消息的两种方式:push方式和pull方式。当生产者把消息放到broker中后,这时候消息处于ready状态,然后这些消息可push到消费者,也可由消费者pull。

confirms消息,何时broker认为该消息被confirmed?对于不可路由消息,一旦exchange认为该消息不可路由,broker就会发起一次confirm,会向生产者发送basic.return。对于可路由消息,当消息被所有queues接收后,会发送basic.ack,对于持久化queue,消息被写道磁盘时,发送basic.ack,对于集群,消息被elected leader接收时会发送basic.ack

4 cli工具

官网https://www.rabbitmq.com/cli.html

4.1 rabbitmqadmin

rabbitmqadmin --help查询命令帮助文档

 

 

 举例:

创建队列

rabbitmqadmin declare queue name=test durable=true

 上面创建的queue,没有创建exchange。Rabbit会使用默认的Exchange和默认的Binding。

发布消息

rabbitmqadmin publish routing_key=test payload="hello" 

 获取消息

rabbitmqadmin get queue=test,只是获取而不消费

 

 查看默认bindings

 

 binding定义了Exchange和queue之前的对应关系。我们可以看到,默认创建的queue,使用默认的Exchange。

binding中source指的是Exchange,destinatuon指的是queue,routing_key指的是路由匹配规则

创建Exchange

rabbitmqadmin declare exchange name=my.topic type=topic

 创建binding

rabbitmqadmin declare binding source=my.topic destination=test routing_key=my.#

 使用交换机发布消息

rabbitmqadmin publish routing_key=my.test exchange=my.topic  payload="hello world by my.test"

删除queue、exchange

rabbitmqadmin delete queue name=test

rabbitmqadmin delete exchange name=my.topic

创建user、vhost、授权

rabbitmqadmin declare vhost name=szj

rabbitmqadmin declare user name=szj password=admin tags=administrator

 rabbitmqadmin declare permission vhost=szj user=szj configure=.* write=.* read=.*

4.2 rabbitmqctl

 官网:https://www.rabbitmq.com/rabbitmqctl.8.html,内容后续补充

5 官方api文档

官网:https://www.rabbitmq.com/api-guide.html

  • Channel: represents an AMQP 0-9-1 channel, and provides most of the operations (protocol methods).表示一个AMQP协议中的channel,并且提供了大部分操作(AMQP协议中定义的方法)
  • Connection: represents an AMQP 0-9-1 connection 表示一个AMQP协议中的connection
  • ConnectionFactory: constructs Connection instances 构建Connection实例的工厂
  • Consumer: represents a message consumer 表示一个消费者
  • DefaultConsumer: commonly used base class for consumers 消费者常用的基类
  • BasicProperties: message properties (metadata) 消息属性
  • BasicProperties.Builder: builder for BasicProperties 构建器

AMQP 0-9-1协议中定义的操作可以通过Channel接口进行。Connection用于打开Channel、注册connection生命周期事件处理程序和关闭不再需要的connection。连接通过ConnectionFactory实例化

3.1 连接rabbitmq

ConnectionFactory factory = new ConnectionFactory();
// "guest"/"guest" by default, limited to localhost connections. 如果不设置下面的属性,默认是guest/guest,且只能连接localhost。 
factory.setUsername(userName);
factory.setPassword(password);
factory.setVirtualHost(virtualHost);
factory.setHost(hostName);
factory.setPort(portNumber);

Connection conn = factory.newConnection();

或者我们使用uri方式连接

ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://userName:password@hostName:portNumber/virtualHost");
Connection conn = factory.newConnection();

我们可以提供一个节点列表来创建connection。建立collection时会首先尝试第一个host:port,然后第二个,直到找到能够连接的host:port对。

注意,该方式和rabbitmq的集群部署不是一回事。

这种方式也有一定的缺点。比如生产者collection初始化时用的时host1:port1.而消费者connection初始化时用的是host2:port2.这样,消费者就消费不到消息了,有消息丢失的隐患。

Address[] addrArr = new Address[]{ new Address(hostname1, portnumber1), new Address(hostname2, portnumber2)};
Connection conn = factory.newConnection(addrArr);

 我们可以在创建Collection时指定线程池

ExecutorService es = Executors.newFixedThreadPool(20);
Connection conn = factory.newConnection(es);

如果不指定连接池,会有一个默认的线程池,消费者线程会在这个默认的线程池中被分配。如果我们使用手动创建的线程池,则需要注意的是,当Collection关闭的时候,es不会自动关闭,需要我们手动关闭线程池es。

除非默认线程池确实不够用(比如存在大量Consumer callbacks),否则我们不应该自定义线程池。

 

如果属性在创建collection时没有指定,则使用属性的默认值

Property Default Value
Username "guest"
Password "guest"
Virtual host "/"
Hostname "localhost"
port 5672 for regular connections, 5671 for connections that use TLS

 

3.2 开启一个Channel

 Channel channel = conn.createChannel(); 

通过Collection,我们开启一个channel。然后我们就可以通过这个channel通道发送和接收消息了。

3.3 关闭rabbitmq连接

channel.close();
conn.close();

3.4 connection和channel生命周期

客户端连接应该是长连接。AMQP协议被设计和优化成支持长期连接。我们应该尽量只连接一次,重复使用这个连接,而不是发送一条消息连接一次。

channel也应该是长连接。我们应尽量只初始化一次channel。但是有些时候channel会自动断开(比如我们消费一个根本不存在的queue)

3.5 创建Exchange和Queue

客户端应用使用Exchange和Queue。Exchange和Queue在使用之前必须先declare。declare后我们会在rabbitmq中保留他们的名字,到真正需要创建他们的时候才创建他们。

创建Exchange有很多重载方法,见https://rabbitmq.github.io/rabbitmq-java-client/api/current/

比如:

exchangeDeclare​(String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map<String,​Object> arguments)

其中,type:[direct,fanout,topics,headers]。

durable:true表示持久,rabbit服务重启后会自动恢复

autoDelete:true表示自动删除,当不再被使用时,将删除这个exchange(rabbitmq服务启动后消失)

internel:true if the exchange is internal, i.e. can't be directly published to by a client.

arguments:其他一些参数,

channel.exchangeDeclare(exchangeName, "direct", true, false, false, null);

这里我们创建了一个exchange,其属性为持久化、非自动删除、direct

创建Queue也有很多重载方法

queueDeclare​(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String,​Object> arguments)

durable:true表示持久化,rabbitmq服务重启后不会消失

exclusive:true表示只能够在当前collection中使用

autoDelete:true表示,当该queue不再被使用时将会被删除

arguments:其他一些参数

注意:创建Exchange 和 Queue只是做了声明处理,当真正使用时才会真正创建。

exchangeDeclarePassive和queueDeclarePassive用于声明被动exchange和被动queue。被动exchange和普通exchange的区别是:当exchange不存在时将报错,当exchange存在时将不做任何操作。被动queue会返回消费者个数和就绪态的消息总数,如果不存在则会报错

Queue.DeclareOk response = channel.queueDeclarePassive("queue-name");
// returns the number of messages in Ready state in the queue
response.getMessageCount();
// returns the number of consumers the queue has
response.getConsumerCount();

3.6 删除队列

 channel.queueDelete("queue-name") 

其有多个重载方法,可以控制队列空时删除、队列没有消费者时删除

3.7 清空队列

 channel.queuePurge("queue-name") 

3.7 publishing messages

有3个重载方法

void basicPublish​(String exchange, String routingKey, boolean mandatory, boolean immediate, AMQP.BasicProperties props, byte[] body) Publish a message.
void basicPublish​(String exchange, String routingKey, boolean mandatory, AMQP.BasicProperties props, byte[] body) Publish a message.
void basicPublish​(String exchange, String routingKey, AMQP.BasicProperties props, byte[] body)

详细的消息发布介绍https://www.rabbitmq.com/publishers.html

举例,最简单的形式,发布消息时指定exchange、routingKey、消息实体

byte[] messageBodyBytes = "Hello, world!".getBytes();
channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);

使用Properties指定消息属性(持久化消息)

 channel.basicPublish(exchangeName, routingKey, mandatory, MessageProperties.PERSISTENT_TEXT_PLAIN, messageBodyBytes); 

指定消息属性(text/plain、持久化、优先级1)

channel.basicPublish(exchangeName, routingKey,
             new AMQP.BasicProperties.Builder()
               .contentType("text/plain")
               .deliveryMode(2)
               .priority(1)
               .userId("bob")
               .build(),
               messageBodyBytes);

使用客制化header

Map<String, Object> headers = new HashMap<String, Object>();
headers.put("latitude",  51.5252949);
headers.put("longitude", -0.0905493);

channel.basicPublish(exchangeName, routingKey,
             new AMQP.BasicProperties.Builder()
               .headers(headers)
               .build(),
               messageBodyBytes);

header中设置超时时间

channel.basicPublish(exchangeName, routingKey,
             new AMQP.BasicProperties.Builder()
               .expiration("60000")
               .build(),
               messageBodyBytes);

 

3.8 Channel和并发

 应尽量避免多线程之间共享一个Channel。应首选一个线程中只有一个Channel。

线程之间共享Channel会有如下问题:

双重确认(double acknowledgements)

3.9 消费消息-订阅方式

订阅方式消费消息是最高效的方式。

String basicConsume​(String queue, boolean autoAck, Consumer callback)    
String basicConsume​(String queue, boolean autoAck, DeliverCallback deliverCallback, CancelCallback cancelCallback)    
String basicConsume​(String queue, boolean autoAck, DeliverCallback deliverCallback, CancelCallback cancelCallback, ConsumerShutdownSignalCallback shutdownSignalCallback)
String basicConsume​(String queue, boolean autoAck, DeliverCallback deliverCallback, ConsumerShutdownSignalCallback shutdownSignalCallback)
String basicConsume​(String queue, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive, Map<String,​Object> arguments, Consumer callback)
String basicConsume​(String queue, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive, Map<String,​Object> arguments, DeliverCallback deliverCallback, CancelCallback cancelCallback)
String basicConsume​(String queue, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive, Map<String,​Object> arguments, DeliverCallback deliverCallback, CancelCallback cancelCallback, ConsumerShutdownSignalCallback shutdownSignalCallback)
String basicConsume​(String queue, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive, Map<String,​Object> arguments, DeliverCallback deliverCallback, ConsumerShutdownSignalCallback shutdownSignalCallback)
String basicConsume​(String queue, boolean autoAck, String consumerTag, Consumer callback)
String basicConsume​(String queue, boolean autoAck, String consumerTag, DeliverCallback deliverCallback, CancelCallback cancelCallback)
String basicConsume​(String queue, boolean autoAck, String consumerTag, DeliverCallback deliverCallback, CancelCallback cancelCallback, ConsumerShutdownSignalCallback shutdownSignalCallback)
String basicConsume​(String queue, boolean autoAck, String consumerTag, DeliverCallback deliverCallback, ConsumerShutdownSignalCallback shutdownSignalCallback)
String basicConsume​(String queue, boolean autoAck, Map<String,​Object> arguments, Consumer callback)
String basicConsume​(String queue, boolean autoAck, Map<String,​Object> arguments, DeliverCallback deliverCallback, CancelCallback cancelCallback)
String basicConsume​(String queue, boolean autoAck, Map<String,​Object> arguments, DeliverCallback deliverCallback, CancelCallback cancelCallback, ConsumerShutdownSignalCallback shutdownSignalCallback)
String basicConsume​(String queue, boolean autoAck, Map<String,​Object> arguments, DeliverCallback deliverCallback, ConsumerShutdownSignalCallback shutdownSignalCallback)
String basicConsume​(String queue, Consumer callback)    
String basicConsume​(String queue, DeliverCallback deliverCallback, CancelCallback cancelCallback)
String basicConsume​(String queue, DeliverCallback deliverCallback, CancelCallback cancelCallback, ConsumerShutdownSignalCallback shutdownSignalCallback)
String basicConsume​(String queue, DeliverCallback deliverCallback, ConsumerShutdownSignalCallback shutdownSignalCallback)

举例

这个例子中,channel的basicConsume方法,我们订阅队列queueName中的消息,并且启动手动确认,并设置了consumerTag。定义消费者:继承默认消费者DefaultConsumer并重写handleDelivery方法,由于autoAck我们设置为false,所以在其中我们需要手动确认消息 channel.basicAck(deliveryTag, false) 。

boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "myConsumerTag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException
         {
             String routingKey = envelope.getRoutingKey();
             String contentType = properties.getContentType();
             long deliveryTag = envelope.getDeliveryTag();
             // (process the message components here ...)
             channel.basicAck(deliveryTag, false);
         }
     });

 

3.10 消费消息-拉取方式

  GetResponse basicGet​(String queue, boolean autoAck) 

 该方式使用pull方式拉取某一个队列里面的消息,是十分低效的一种方式。

boolean autoAck = false;
GetResponse response = channel.basicGet(queueName, autoAck);
if (response == null) {
    // No message retrieved.
} else {
    AMQP.BasicProperties props = response.getProps();
    byte[] body = response.getBody();
    long deliveryTag = response.getEnvelope().getDeliveryTag();
    // ...
    channel.basicAck(method.deliveryTag, false); // acknowledge receipt of the message
}

3.11 mandatory和immediate

 mandatory标志告诉服务器至少将该消息route到一个队列中,否则将消息返还给生产者;immediate标志告诉服务器如果该消息关联的queue上有消费者,则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。

void basicPublish​(String exchange, String routingKey, boolean mandatory, boolean immediate, AMQP.BasicProperties props, byte[] body)    
void basicPublish​(String exchange, String routingKey, boolean mandatory, AMQP.BasicProperties props, byte[] body)

但是channel中必须有ReturnListener对返回给生产者的消息进行监听,否则该返回消息将会被丢弃。

channel.addReturnListener(new ReturnListener() {
    public void handleReturn(int replyCode,String replyText,String exchange,String routingKey,AMQP.BasicProperties properties,byte[] body)
    throws IOException {
        ...
    }
});

 3.12 basicQos

 Qos(Quality Of Service),在消费者确认Ack之前,限制发往消费者的消息数量。比如basicQos(1)表示每次向消费者推送1条消息。

void basicQos​(int prefetchCount)
void basicQos​(int prefetchCount, boolean global)
void basicQos​(int prefetchSize, int prefetchCount, boolean global)

例子

public void validMethod(Channel channel)
{
    try {
        ...
        channel.basicQos(1);
    } catch (ShutdownSignalException sse) {
        // possibly check if channel was closed
        // by the time we started action and reasons for
        // closing it
        ...
    } catch (IOException ioe) {
        // check why connection was closed
        ...
    }
}

 3.13 使用NIO

 默认情况下,一个线程对应一个Collection。但是如果Connection数量非常多,推荐使用NIO模型,这样可以减少线程个数,且不会造成性能的明显下降。

ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.useNio();

我们可以设置NIO参数

 connectionFactory.setNioParams(new NioParams().setNbIoThreads(4)); 

3.14 心跳机制

rabbitmq默认开启heartbeat,默认是60秒。如果客户端丢失超过2个心跳,责认为是连接中断。

3.14 网络故障恢复

 当网络不稳定或出现故障时,rabbitmq client具有自动恢复机制。包括恢复Collection、Channel、CollectionListener、ChannelListener、Exchange、Queue、bindings和consumers等。

4.0.0版本默认是开启自动恢复功能的。我么也可以显式的设置

 factory.setAutomaticRecoveryEnabled(true); 

我们还可以设置重试时间间隔,默认是5秒

 factory.setNetworkRecoveryInterval(10000); 

如下情况会触发网络故障恢复:

connection中网络IO异常被抛出

socket timeout

检测到心跳停止

3.15 confirm和acknowledge机制

 参考:https://www.rabbitmq.com/confirms.html

生产者的confirm机制保证消息能够成功到达Server端。消费者的acknowledge机制保证消息被成功消费。

 详情待扩充

 3.17 实例

上面我们创建了一个全新的用户szj,创建了一个全新的vhost:szj。并且设置szj用户可以操作vhost:szj的权限。下面我们以此为基础举一些列子来说明rabbitmq的原理和使用。

生产者

第一步,获取到vhost的连接

第二步,创建到Exchange的channel

第三步,发送消息到exchange

第四步,关闭channel和collection

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.szj.rabbitmq.producer.config.RabbitConfig;

public class RabbitProducer {
    public static void send(String exchange, String routingKey, String message) throws Exception{
        //1、获取connection
        Connection connection = RabbitConfig.getConnection();
        //2、创建channel
        Channel channel = connection.createChannel();
        //3、发送消息到exchange
        /**
         * 参数1:指定exchange,使用“”。默认的exchange
         * 参数2:指定路由的规则,使用具体的队列名称。exchange为""时,消息直接发送到队列中
         * 参数3:制动传递的消息携带的properties
         * 参数4:指定传递的消息,byte[]类型
         */
        channel.basicPublish("", routingKey, null,message.getBytes());
        //PS:exchange是不会将消息持久化的,Queue可以持久化,得配置

        System.out.println("生产者发布消息成功!");
        //4、关闭管道和连接
        channel.close();
        connection.close();
    }

    public static void main(String[] args) {
        try {
            send("","temp_queue", "hello");
        }
        catch (Exception e) {

        }
    }
}

 

消费者

第一步,获取到vhost的连接

第二步,创建到Exchange的channel

第三步,声明一个队列,指定名称、是否持久化、是否排外、是否自动删除等属性

第四步,创建默认消费者DefaultConsumer

第五步,开启监听,指定队列名、消费者、ACK模式

import com.rabbitmq.client.*;
import com.szj.rabbitmq.producer.config.RabbitConfig;
import java.io.IOException;

public class RabbitConsumer {
    //消费者
    public static void consumer() throws Exception{
        //1、获取连对象、
        Connection connection = RabbitConfig.getConnection();
        //2、创建channel
        Channel channel = connection.createChannel();
        //3、创建队列-temp_queue
        /**
         * 参数1:queue 指定队列名称
         * 参数2:durable 是否开启持久化(true)
         * 参数3:exclusive 是否排外(conn.close()-》当前对列自动删除,当前队列只能被一个 消费者消费)
         * 参数4:autoDelete 如果这个队列没有其他消费者在消费,队列自动删除
         * 参数5:arguments 指定队列携带的信息
         *
         */
        channel.queueDeclare("temp_queue",true,false,false,null);


        //4.开启监听Queue
        DefaultConsumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("接收到消息:"+new String(body,"UTF-8"));
            }
        };
        /**
         * 参数1:queue 指定消费哪个队列
         * 参数1:deliverCallback 指定是否ACK(true:收到消息会立即告诉RabbiMQ,false:手动告诉)
         * 参数1:cancelCallback 指定消费回调
         *
         */
        channel.basicConsume("temp_queue",true,consumer);
        System.out.println("消费者开始监听队列");

        //5、键盘录入,让程序不结束!
        System.in.read();

        //6、释放资源
        channel.close();
        connection.close();

    }

    public static void main(String[] args) {
        try
        {
            consumer();
        }
        catch (Exception e)
        {
            System.out.println(e);
        }
    }

}

 

4 常用的几种模型

4.1 basic

参考https://blog.csdn.net/weixin_45967375/article/details/115536526

细节后续补充

 

 

 

 

 

 

 

 

5 springboot中的使用

 5.1 Direct模式

创建一个队列

rabbitmqadmin declare queue name=szj_queue1 durable=true

查看bindings

 

 从binding信息我们可知生产者发送消息时指定发送到routing_key:szj_quque1对应的那个队列szj_quque1,且只能发送到1个队列

生产者

application.yml

server:
  port: 8080
spring:
  rabbitmq:
    host: 192.168.3.57
    port: 5672
    username: admin
    password: admin

 

@RestController
@RequestMapping("/api/producer")
public class ProducerController {
    @Autowired
    private ProducerService producerService;

    //params格式:{"routingKey":"szj_queue1","message":"hello"}
    @RequestMapping("/send")
    @ResponseBody
    public Object send(@RequestBody Map params)
    {
        producerService.sendQueue(params.get("routingKey").toString(), params.get("message").toString());
        return "success";
    }
}
@Service
public class ProducerService {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendQueue(String routingKey, String message){
        System.out.println("开始向队列中发送一条消息!");
        rabbitTemplate.convertAndSend(routingKey,message);
        System.out.println("消息发送完毕!");
    }
}

查看消息:我们通过命令行可以浏览消息内容

消费者

application.yml

server:
  port: 8081
spring:
  rabbitmq:
    host: 192.168.3.57
    port: 5672
    username: admin
    password: admin

 

@Service
@RabbitListener(queues = "szj_queue1")
public class RabbitmqService {
    @RabbitHandler
    public void consumer(String message)
    {
        System.out.println("收到的消息:"+message);
    }
}

5.2 fanout广播模式

 

创建Exchange,类型是fanout

rabbitmqadmin declare exchange name=fanout-exchange type=fanout durable=true

创建3个queue

rabbitmqadmin declare queue name=test-fanout-q1 durable=true

rabbitmqadmin declare queue name=test-fanout-q2 durable=true

rabbitmqadmin declare queue name=test-fanout-q3 durable=true

创建binding

rabbitmqadmin declare binding source=fanout-exchange destination=test-fanout-q1

rabbitmqadmin declare binding source=fanout-exchange destination=test-fanout-q2

rabbitmqadmin declare binding source=fanout-exchange destination=test-fanout-q3

 生产者

//params格式:{"exchange":"fanout-exchange","message":"hello"}
    @RequestMapping("/send_fanout")
    @ResponseBody
    public Object send_fanout(@RequestBody Map params)
    {
        producerService.fanout_send(params.get("exchange").toString(), params.get("message").toString());
        return "success";
    }
public class ProducerService {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    public void fanout_send(String exchange, String message){
        System.out.println("开始向exchange中发送一条消息!");
        rabbitTemplate.convertAndSend(exchange,"",message);
        System.out.println("消息发送完毕!");
    }
}

消费者

@Service
@RabbitListener(queues = "test-fanout-q1")
class FanoutListener1 {
    @RabbitHandler
    public void consumer(String message)
    {
        System.out.println("收到的消息:"+message);
    }
}

@Service
@RabbitListener(queues = "test-fanout-q2")
class FanoutListener2 {
    @RabbitHandler
    public void consumer(String message)
    {
        System.out.println("收到的消息:"+message);
    }
}
@Service
@RabbitListener(queues = "test-fanout-q3")
class FanoutListener3 {
    @RabbitHandler
    public void consumer(String message)
    {
        System.out.println("收到的消息:"+message);
    }
}

 5.3 topic模式

 

 

 创建Exchange

rabbitmqadmin declare exchange name=topic-exchange type=topic durable=true

创建queue

rabbitmqadmin declare queue name=topic-queue1 durable=true

rabbitmqadmin declare queue name=topic-queue2 durable=true

rabbitmqadmin declare queue name=topic-queue3 durable=true

 banding

rabbitmqadmin declare binding source=topic-exchange destination=topic-queue1 routing_key=good.log

rabbitmqadmin declare binding source=topic-exchange destination=topic-queue2 routing_key=#.log

rabbitmqadmin declare binding source=topic-exchange destination=topic-queue3 routing_key=good.#

 生产者

指定Exchange和routingKey

//params格式:{"exchange":"topic-exchange","routingKey":"good.log","message":"这是一条good.log消息"}
    @RequestMapping("/send_topic")
    @ResponseBody
    public Object send_topic(@RequestBody Map params)
    {
        producerService.topic_send(params.get("exchange").toString(), params.get("routingKey").toString(), params.get("message").toString());
        return "success";
    }

 

    public void topic_send(String exchange, String routingKey, String message){
        System.out.println("开始发送topic发送一条消息!");
        rabbitTemplate.convertAndSend(exchange,routingKey,message);
        System.out.println("消息发送完毕!");
    }

消费者

@Service
@RabbitListener(queues = "topic-queue1")
class topicListener1 {
    @RabbitHandler
    public void consumer(String message)
    {
        System.out.println("topic-queue1收到的消息:"+message);
    }
}
@Service
@RabbitListener(queues = "topic-queue2")
class topicListener2 {
    @RabbitHandler
    public void consumer(String message)
    {
        System.out.println("topic-queue2收到的消息:"+message);
    }
}
@Service
@RabbitListener(queues = "topic-queue3")
class topicListener3 {
    @RabbitHandler
    public void consumer(String message)
    {
        System.out.println("topic-queue3收到的消息:"+message);
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

//params格式:{"exchange":"fanout-exchange","message":"hello"}
@RequestMapping("/send_fanout")
@ResponseBody
public Object send_fanout(@RequestBody Map params)
{
producerService.fanout_send(params.get("exchange").toString(), params.get("message").toString());
return "success";
}
channel.basicConsume

basicQos

posted @ 2022-05-30 00:26  zhenjingcool  阅读(336)  评论(0编辑  收藏  举报