Queue 具有自己的 erlang 进程;exchange 内部实现为保存 binding 关系的查找表;channel 是实际进行路由工作的实体,即负责按照 routing_key 将 message 投递给 queue 。由 AMQP 协议描述可知,channel 是真实 TCP 连接之上的虚拟连接,所有 AMQP 命令都是通过 channel 发送的,且每一个 channel 有唯一的 ID。一个 channel 只能被单独一个操作系统线程使用,故投递到特定 channel 上的 message 是有顺序的。但一个操作系统线程上允许使用多个 channel 。
若该队列至少有一个消费者订阅,消息将以循环(round-robin)的方式发送给消费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)
从概念上来说,消息路由必须有三部分:交换器、路由、绑定。生产者把消息发布到交换器上;绑定决定了消息如何从路由器路由到特定的队列;消息最终到达队列,并被消费者接收。
消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。通过队列路由键,可以把队列绑定到交换器上。消息到达交换器后,RabbitMQ会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)。如果能够匹配到队列,则消息会投递到相应队列中;如果不能匹配到任何队列,消息将进入 “黑洞”。
fanout:广播模式 队列直接绑定该交换机(不需要指定routing key)
例:
BindingBuilder.bind(Message_1).to(fanoutExchange);
BindingBuilder.bind(Message_2).to(fanoutExchange);
队列1和2都可以收到来自交换机fanoutExchange消息
direct:消息路由到那些binding key与routing key完全匹配的Queue中
例:
生产者发送消息时需要指定bing-key
rabbitTemplate.convertAndSend(directExchange,"bind-key", goJson.toJSONString());
BindingBuilder.bind(AMessage).to(directExchange).with("bind-key");
队列AMessage使用bind-key绑定到directExchange交换机上
为了保证这一步的可靠性,AMQP 协议在建立之初就提供了事务机制。RabbitMQ 客户端中与事务机制相关的方法有三个:channel.txSelect、channel.txCommit 以及 channel.txRollback。channel.txSelect 用于将当前的信道设置成事务模式,channel.txCommit 用于提交事务,而 channel.txRollback 用于事务回滚。在通过channel.txSelect 方法开启事务之后,我们便可以发布消息给 RabbitMQ 了,如果事务提交成功,则消息一定到达了 RabbitMQ 中,如果在事务提交执行之前由于 RabbitMQ 异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行 channel.txRollback 方法来实现事务回滚。
try {
// 开启事务
channel.txSelect();
// 发送消息
channel.basicPublish(exchange, routingKey, props, body);
// 事务提交
channel.txCommit();
} catch(Exception e) {
// 事务回滚
channel.txRollback();
e.printStackTrace();
}
RabbitMQ使用发送方确认模式,确保消息正确地发送到RabbitMQ。
发送方确认模式:将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(not acknowledged,未确认)消息。发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
如果 Exchange 没有匹配到队列的话,那么消息依然会丢失。
mandatory
当 mandatory 参数设为 true 时,如果 Exchange 无法根据自身的类型和路由键找到一个符合条件的队列的话,那么RabbitMQ 会调用 Basic.Return 命令将消息返回给生产者。而 mandatory 参数设置为 false 时,出现上述情形的话,消息直接被丢弃。这时候可以通过调用 channel.addReturnListener 来添加 ReturnListener 监听器获取到没有被正确路由到合适队列的消息。
备份交换器
使用备份交换器,这样可以将未被路由的消息存储在 RabbitMQ 中,再在需要的时候去处理这些消息。可以通过在声明交换器(调用 channel.exchangeDeclare 方法)的时候添加 alternate-exchange 参数来实现。
可为所有的交换器设置同一个备份交换器,所有无法正确路由的消息都将发往这个备份交换器中,不过需确保备份交换器已经正确的绑定了队列。如果备份交换器和 mandatory 参数一起使用,那么 mandatory 参数无效。
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","备份交换机Name");
channel.exchangeDeclare(EXCHANGE_NAME,"fanout",true,false,arguments);
接收方消息确认机制:消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。
可以指定 autoAck 参数,当 autoAck 等于 fals e时,RabbitMQ 会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上是先打上删除标记,之后再删除)。当 autoAck 等于 true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正的消费到了这些消息
下面罗列几种特殊情况:
如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要根据bizId去重)
如果消费者接收到消息却没有确认消息,连接也未断开,则RabbitMQ认为该消费者繁忙,将不会给该消费者分发更多的消息。
如果 RabbitMQ 在等待回调的过程中,消费者服务挂掉
对于 RabbitMQ 而言,队列中的消息分成了两个部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者确认信号的消息。如果 RabbitMQ 一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则 RabbitMQ 会安排该消息重新进入队列,等待投递给下一个消费者。RabbitMQ 判断此消息是否需要重新投递的唯一依据是消费该消息的消费者连接是否已经断开,这种设计允许消费者消费一条消息很久很久。
如果消息消费失败,也可以调用 Basic.Reject 或者 Basic.Nack 来拒绝当前消息,但需要注意的是,如果只是简单的拒绝那么消息将会丢失,需要将相应的 requeue 参数设置为 true,RabbitMQ 才会将这条消息重新存入队列。而如果 requeue 参数设置为 false 的话,RabbitMQ 立即会把消息从队列中移除,而不会把它发送给新的消费者。