(一)、什么是AMQP,AMQP与rabbitmq的关系
说简单点就是在异步通讯中,消息不会立刻到达接收方,而是被存放到一个容器中,当满足一定的条件之后,消息会被容器发送给接收方,这个容器即消息队列(MQ),而完成这个功能需要双方和容器以及其中的各个组件遵守统一的约定和规则,AMQP就是这样的一种协议,消息发送与接受的双方遵守这个协议可以实现异步通讯。
rabbitmq是AMQP协议的实现者,所以amqp中的概念和准则也适用于rabbitmq。
(二)、Rabbitmq基本组成及基本概念
1.组成和运行流程
Rabbitmq的基本组成如下图所示,该模型同样适用于AMQP。
- 生产者
- 消息
- 消费者
- Broker
- 队列
- 交换器
- 绑定
- 路由键
消费者,生产者统称为客户端,mq(broker)为服务端。队列,交换器,路由键看似在代码中是channel.queueDeclare(),channel.exchangeDeclare() 以为是channel创建的,其实不是。它们都是broker 服务器创建的
此图展示了生产者将消息存入Broker,以及消费者Broker中消费数据的整个流程
生产者发送消息:
1.生产者与Broker建立连接(Connection基于TCP的),开启信道(Channel)
2.生产者声明交换器(交换器类型、是否持久化、是否自动删除等)
3.生产者声明队列(是否持久化、是否排他、是否自动删除)
4.生产者通过路由键将交换器和队列绑定
5.生产者发送消息至Broker(携带路由键等)
6.交换器根据接收到的路由键,以及交换器类型查找匹配的队列
7.找到,队列将消息存入相应队列中
8.找不到,则根据生产者的配置,选择丢弃还是回退给生产者
9.关闭信道
10.关闭连接
消费者接收消息:
1.消费者与Broker建立连接(Connection基于TCP的),开启信道(Channel)
2.消费者向Broker请求消费相应队列的消息,可能设置回调函数
3.等待Broker回应并投递相应队列中的消息,接收消息
4.消费者确认(ack)接收到的消息
5.RabbitMQ从队列中删除相应已经被确认的消息
6.关闭信道
7.关闭连接
channel里面的消息里面包括有效载荷与标签。有效载荷:具体的信息。标签:描述有效载荷的属性(例如 bindingkey ,rountingkey等)。
2.基本概念
a.生产者(Publisher/Producer):生产者顾名思义就是生产消息的角色。生产者会将消息发送给交换机。为了使交换机正确的将消息路由给队列,发布消息时需指定消息的路由键(routing key)。
b.消息代理(Broker):代理的职能就是接收发布者发布的消息并将消息推送给订阅了消息的消费者。rabbitmq就相当于一个代理。Broker可以简单地看作一个RabbitMQ服务节点,或者RabbitMQ服务实例。
c.虚拟主机(Virtual Host):出于多租户和安全因素设计的,把AMQP的基本组(交换机,队列,绑定称为AMQP组件)件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个borker(RabbitMQ server)提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。
d.交换机(Exchange):交换机负责接收消息并按照一定的规则将消息路由给队列。路由的规则存储在交换机与队列绑定时指定的routing key。
e.绑定(Binding):交换机和队列之间通过路由键(routing key)相互绑定起来,并根据路由键将消息路由到对应队列。
f.队列(Queue):消息的载体,exchange中的消息将被路由到队列中,并推送给消费者或者被消费者取走。
g.连接(Connection):消费者和生产者与消息代理之间的连接
h.通道(Channel):如果消费者每一次从代理(Broker)中取消息都建立一次连接的话,在消息量大的情况下建立多个连接将会有巨大的开销。Channel是在Connection内部建立的逻辑连接与Broker直接连接(生产者和消费者各连接一端),同时channel可以是双向的,既是发送消息通道也是接收消息通道。如果应用程序支持多线程,可以为每个线程建立单独的通道进行通讯。AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。
i.消费者(Consumer):接收消息的角色就是消费者
(三)、交换机
以一下讲的是交换机与队里绑定,但是实际上交换机和交换机也可以绑定
1.交换机的分类
交换机主要分为四种,还有一种比较特殊的默认交换机:
RabbitMQ常用的交换器类型有fanout、direct、topic、headers四种:
fanout:它会把所有发送到该交换器的消息,路由到所有与该交换器绑定的队列中;
direct:把消息路由到那些BindingKey和RoutingKey完全匹配的队列中;
topic:类似于direct,但可以使用通配符匹配规则;
header:消息不根据路由键的匹配规则路由,而是根据发送的消息内容中的headers属性进行匹配。
a.直连交换机(direct exchange):把消息路由到那些BindingKey和RoutingKey完全匹配的队列中;。该种交换机为直连交换 机。直连交换机经常用来循环分发任务给多个消费者,此时消息的负载均衡是发生在消费者之间的。
交换机与队列绑定,绑定建是Bindingkey在 queueBind()中的设置。
发送消息的时候需要携带路由键:RoutingKey在basicPublish()中的设置
对于下图而言:当携带有orange路由键的消息被发送到交换机时,消息会被路由到队列Q1。
b.主题交换机(topic exchange):类似于direct,但可以使用通配符匹配规则。该种交换机为主题交换机。主题交换机经常用来实现各种分发、订阅模式及其变种。主题交换机通常用来实现消息的多播路由(multicast routing)。
模式路由键routing key一般使用 . 来分隔单词,而且有两个通配符可以使用。* 代表任意一个单词,#代表0个或多个单词。例如:apple.orange.penaunt,.orange.,orange.#。
(#)代表匹配所有 (*) 代表匹配所有中某一个或者只有一个
主题交换机拥有非常广泛的用户案例。无论何时,当一个问题涉及到那些想要有针对性的选择需要接收消息的 多消费者/多应用(multiple consumers/applications) 的时候,主题交换机都可以被列入考虑范围。
使用案例:
1:分发有关于特定地理位置的数据,例如销售点
2:由多个工作者(workers)完成的后台任务
3:每个工作者负责处理某些特定的任务
4:股票价格更新(以及其他类型的金融数据更新) 5:涉及到分类或者标签的新闻更新(例如,针对特定的运动项目或者队伍)
6:云端的不同种类服务的协调
7:分布式架构/基于系统的软件封装,其中每个构建者仅能处理一个特定的架构或者系统。
对于下图而言:当携带routing key为apple.orange.penanut的消息被发送给交换机时,将会被路由到队列Q1中。当lazy.apple.pen被发送到交换机时,将会被路由到队列Q2中。
c.扇形交换机/广播交换机(fanout exchange):它会把所有发送到该交换器的消息,路由到所有与该交换器绑定的队列中;
对下图而言:携带任何routing key的消息被发送到交换机上时,消息都将被拷贝并分发到Q1,Q2,Q3中。
扇形交换机的使用的案例:
1:大规模多用户在线(MMO)游戏可以使用它来处理排行榜更新等全局事件
2:体育新闻网站可以用它来近乎实时地将比分更新分发给移动客户端
3:分发系统使用它来广播各种状态和配置更新
4:在群聊的时候,它被用来分发消息给参与群聊的用户。(AMQP没有内置presence的概念,因此XMPP可能会是个更好的选择)。
d.头交换机:消息不根据路由键的匹配规则路由,而是根据发送的消息内容中的headers属性进行匹配。
e.默认交换机(default exchange):默认交换机是一种特殊的直连交换机(direct exchange)。它是由消息代理默认声明的,,如果不手动创建交换机则使用默认的。该交换机有一个特性,所有新建的队列都会默认绑定到默认交换机上,并且绑定的routing key就是队列的名字。
2.交换机的属性
除交换机类型外,在声明交换机时还可以附带许多其他的属性,其中最重要的几个分别是:
Name:交换机名称。
Durable:交换机有两个状态,持久(durable)、暂存(transient)。交换器的创建持久化到磁盘重启后依然存在,而暂存的交换机重启后就没有了。
Auto-delete :自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与
这个交换器绑定的队列或者交换器都与此解绑。
Arguments:依赖代理本身。
internal:声明为内部交换机。客户端程序无法直接发送消息到交换器,只能通过交换器路由到内置交换器。数据直接发送到交换机是会报错的
(四)、队列
Name:队列名,队列名称可以自己声明,也可以由代理来生成。但是自己声明时,不能声明以amp.开头的队列名,因为这是代理内部使用的队列名称格式,如果这样声明,将会抛出异常。
Durable:和交换机一样,消息代理重启后,队列是否还存在
Exclusive:只被一个连接使用,连接关闭后,将立即删除队列。
Auto-delete:当所有的消费者都退订队列后将自动删除该队列
注意:
队列上的某一条消息只能由一个消费者订阅,不能多个同时订阅。但是一个队列可以连接多个消费者。一个消费者订阅一部分,另一个消费者订阅另一部分这是可以的。
声明队列:
声明一个默认队列(这个队列时自动删除的 AD)
Queue.DeclareOk queueDeclare() throws IOException;
声明一个普通队列
Queue.DeclareOk queueDeclare(String queue,boolean durable,boolean exclusive,boolean
autoDelete,Map<String,Object> arguments) throws IOException;
备注:排他队列,如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这种队列适用于一个客户端同时
发送和读取消息的应用场景。
1、排他队列是基于连接(Connection)可见的,同一个连接的不同信道(Channel)是可以同时访问同一连接创建的排他队列;
2、"首次"是指如果一个连接己经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同;
3、即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除。
参数 arguments(map类型的):
具体操作如下:
1:可以设置消息的过期时间(默认是永不过期的)
有两种方式设置过期时间:
第一种通过设置队列属性值,队列中所有消息过期时间是一致的。
第二种设置消息自己的过期时间,队列中每条消息过期时间可以不同。
如果两种方式一起设置,则消息的TTL以两者之间更小的那个数值为准。
注意:
1:第一种设置队列TTL属性的方法,一旦消息过期,就会从队列中抹去;
2:第二种方法中,即使消息过期,也不会马上从队列中抹去。因为每条消息是否过期是在即将投递到消费者之前判定的
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
//2 为持久化 1为非持久化
builder.deliveryMode(2);
//TTL
builder.expiration("1000");
对于第一种方式过期了会从队列中删除,对于第二种消息过期也不会从队列中删除。
两种不同的原因是:
1:第一种队列中过期的消息将在队列的头部,mq只要定期从队列头部开始扫描过期信息即可。
2::第二种由于每条消息的TTL不同如果要删除所有过期消息必须扫描对列的所有消息,所有不如等到消息被消费时在判定是否过期,过期则删除
注意:
1:对列中间的消息过期mq不会主动删除
2:会定期扫描对列头部的消息,删除过期消息
3:未删除的过期消息不会进入死性队列
2:可以设置队列中消息条数(如果多了,会把多出几条放入队列底部,把头部几条剔除,先进先出,剔除的消息可以放到死性交换机,死性队列中)
3:可以在arguments中设置死信交换机(死性队列不能设置),死性的rountingkey(用普通交换机的key也可以)。自己在设置一个连接的队列(死性队列),最后在绑定死性队列和死性交换机。把废弃消息放到死性队列中。
成为死性队列的原因:
消息变成死信一般是由于以下几种情况:
(1)消息被拒绝(Basic.Reject/Basic.Nack),井且设置requeue参数为false;
(2)消息过期;
(3)队列达到最大长度。
DLX也是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定。
死性队列,死性交换器其实和普通队列,普通交换器没有区别。
4:消息持久化硬盘与否:
有一个前提首先得保证队列持久化,队列删了消息在持计划也用!
//MessageProperties.PERSISTENT_TEXT_PLAIN 通过这个设置持久化硬盘
channel.basicPublish(EXCHANGE_NAME, ExCHANEROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
//MessageProperties.TEXT_PLAIN 通过这个设置直接去内存中取
4:消息路由失败解决办法:
在发送消息channel.basicPublish方法中有两个参数分别是mandatory和immediate,它们都有当消息在传递过程中
不能到达目的地时,将消息返回给生产者的功能。
RabbitMQ提供的备份交换器(Alternate Exchange)可以将未能被交换器路由的消息(没有绑定队列或者没有匹配的绑定)存储起来,而不用返回给客户端。
关于备份交换器:
(1) 如果设置的备份交换器不存在,客户端和RabbitMQ服务端都不会出现异常,此时消息会丢失。
(2) 如果备份交换器没有绑定任何队列,客户端和RabbitMQ服务端都不会出现异常,此时消息会丢失。
(3) 如果备份交换器没有任何匹配的队列,客户端和RabbitMQ服务端都不会出现异常,此时消息会丢失。
(4) 如果备份交换器和mandatory参数一起使用,如果备份交换器有效,那么mandatory参数无效
备份交换器代码:
先进normalExchange这个交换器如果路由失败,则会进入myAe这个备份交换机中
Map<String, Object> params = new HashMap<>();
params.put("alternate-exchange", "myAe");
// normalExchange, 设置了备份交换器为 myAe
channel.exchangeDeclare("normalExchange", "direct", true, false, params);
channel.exchangeDeclare("myAe", "fanout", true, false, null);
channel.queueDeclare("normalQueue", true, false, false, null);
channel.queueDeclare("unRoutedQueue", true, false, false, null);
channel.queueBind("normalQueue", "normalExchange", "normalKey");
channel.queueBind("unRoutedQueue", "myAe", "");
channel.basicPublish("normalExchange", "errorKey",
MessageProperties.PERSISTENT_TEXT_PLAIN, "normalKey is running ok".getBytes());
(五)、消费者
1.消息确认
消费者在处理消息的时候偶尔会失败或者有时会直接崩溃掉。而且网络原因也有可能引起各种稳日。所以AMQP在什么时候删除消息才是正确的呢?AMQP规范给了两种建议:
当消息代理将消息发送给应用后立即删除,无论消费者获取到消息后是否成功消费,都会认为消息成功消费,即自动确认机制。
//4.定义队列的消费者
//QueueingConsumer queueingConsumer1 = new QueueingConsumer(channel);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//用于计数
anInt+=1;
String msg = new String(body, "utf-8");
System.out.println("["+String.valueOf(anInt)+"]:receve msg:" + msg);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
System.out.println("[1] done");
}
}
};
//true为自动确认
boolean autoAck = true;
channel.basicConsume(QUEUE_NAME, autoAck, defaultConsumer);
}
待消费完后发送一个ack确认后再删除消息,即显示确认模式。
//4.定义队列的消费者
QueueingConsumer queueingConsumer1 = new QueueingConsumer(channel);
/*
true:表示自动确认,只要消息从队列中获取,无论消费者获取到消息后是否成功消费,都会认为消息成功消费.
false:表示手动确认,消费者获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,
如果消费者一直没有反馈,那么该消息将一直处于不可用状态,并且服务器会认为该消费者已经挂掉,不会再给其发送消息,
直到该消费者反馈.例如:channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
*/
//b:ack 为false 为收到ack后在删除
channel.basicConsume(QUEUE_NAME, false,queueingConsumer1);
//6.获取消息
while (true) {
anInt += 1;
QueueingConsumer.Delivery delivery1 = queueingConsumer1.nextDelivery();
// QueueingConsumer.Delivery delivery2 = queueingConsumer2.nextDelivery();
String message1 = new String(delivery1.getBody());
// String message2 = String.valueOf(delivery2.getBody());
System.out.println("[" + String.valueOf(anInt) + "]:receve msg:" + message1);
//是否批量处理.true:将一次性ack所有deliveryTag之前的所有消息
//false:只能ack当前这一条消息。这种就不会while了
channel.basicAck(delivery1.getEnvelope().getDeliveryTag(), false);
// System.out.println("[x] Received '" + message2 + "'");
Thread.sleep(500);
}
}
true 和 false 代表消费多少
//是否批量处理.true:将在该信道channel上一次性ack所有消息
//false:只能ack当前这一条消息。这种就不会在while循环里面了
//delivery1.getEnvelope().getDeliveryTag() 消息ID
channel.basicAck(delivery1.getEnvelope().getDeliveryTag(), false);
如果服务器一直没有收到消费者的确认消费信号(Unacked),并且该消费者已经断开连接,则该消息会重新进入队列(Ready)在队列中等待下一个消费者或者又重新连接的原消费者就行消费。
所有无论是否自动确认,消息都不会丢失的。
2.拒绝消息
当一个消费者在收到消息后,由于某种原因导致消息处理失败,消费者可以告诉mq拒绝消息,并指明如何处理这条消息——主动丢失或者重新放入队列中(重新放入队列位置,顺序和原来一样没有变动)或者把拒绝消息放入死性队列中。
所有消息是可能丢失的。
//拒绝单条信息
channel.basicReject(long deliveryTag, boolean requeue) throws IOException
//拒绝多条信息
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
其中一些参数的意义:
multiple参数设置为false,则表示拒绝编号为deliveryTag这条消息;
multiple参数设置为true,则表示拒绝deliveryTag编号之前所有未被当前消费者确认的消息;
requeue参数用于设置是否再次将该消息放入队列中重新发送给消费者。
注意:channel.basicReject()或channel.basicNack()中的requeue设置为false,可以启用“死信队列”功能。
3.预取消息
在多个消费者共享一个队列的时候,可能会出现有的消费者处理很多消息,而有的消费者没有消息可处理。此时可以指定在收到下一个确认回执前,消费者一次可以接受的最大消息数。
(六)、消息属性和有效载荷
channel里面的消息里面包括有效载荷与标签。有效载荷:具体的信息。标签:描述有效载荷的属性(例如 bindingkey ,rountingkey等)。
消息能够以持久化的方式发布,AMQP代理会将此消息存储在磁盘上。如果服务器重启,系统会确认收到的持久化消息未丢失。简单地将消息发送给一个持久化的交换机或者路由给一个持久化的队列,并不会使得此消息具有持久化性质:它完全取决与消息本身的持久模式(persistence mode)。将消息以持久化方式发布时,会对性能造成一定的影响(就像数据库操作一样,健壮性的存在必定造成一些性能牺牲)。
消息推模式(basicConsumer())用的最多:
服务器主动把消息推送到客户端,可以多条,只是推送条数受限制
同一时刻服务器只会发送N条消息给消费者,只有在确认消费ack后才能下一次接收,可以用于限流秒杀
channel.basicQos(N)
消息拉模式(basicGet()):
客户端主动去服务器去取消息,但是只能一次只能拉一条。不能在for循环中利用basicGet() 拉取多条,性能是非常差的。
(七)、消息属性和有效载荷
参考资料:
https://blog.csdn.net/mx472756841/article/details/50815895
https://blog.csdn.net/whoamiyang/article/details/54954780
https://www.cnblogs.com/frankyou/p/5283539.html