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