》如果没有交换机,那么routekey就必须是队列名,否则通道不知道要把消息传到哪个队列中去
》RabbitMQ 默认将消息顺序发送给下一个消费者,这样,每个消费者会得到相同数量的消息。即轮询(round-robin)分发消息。
假如信息a已经被发送给了消费者1,那么一定会等到信息a的ack返回后,才会把信息b发送给消费者2(即下一个消费者),不管消费者1空闲与否或者其他消费者空闲与否,这里应该叫“轮流,顺上来”的概念即默认情况下,当一个信息被发送给一个消费者,那么下一个信息一定会被发送给下一个消费者,而不是其他消费者。当然,只有1个消费者时,由于它自己就是下一个消费者,那就一直是它获得消息怎样才能做到按照每个消费者的能力分配消息呢?联合使用 Qos 和 Acknowledge 就可以做到。basicQos 方法设置了当前信道最大预获取(prefetch)消息数量为1。消息从队列异步推送给消费者,消费者的 ack 也是异步发送给队列,从队列的视角去看,总是会有一批消息已推送但尚未获得 ack 确认,Qos 的 prefetchCount 参数就是用来限制这批未确认消息数量的。设为1时,队列只有在收到消费者发回的上一条消息 ack 确认后,才会向该消费者发送下一条消息。prefetchCount 的默认值为0,即没有限制,队列会将所有消息尽快发给消费者。轮询分发 :使用任务队列的优点之一就是可以轻易的并行工作。如果我们积压了好多工作,我们可以通过增加工作者(消费者)来解决这一问题,使得系统的伸缩性更加容易。在默认情况下,RabbitMQ将逐个发送消息到在序列中的下一个消费者(而不考虑每个任务的时长等等,且是提前一次性分配,并非一个一个分配)。平均每个消费者获得相同数量的消息。这种方式分发消息机制称为Round-Robin(轮询)。公平分发 :虽然上面的分配法方式也还行,但是有个问题就是:比如:现在有2个消费者,所有的奇数的消息都是繁忙的,而偶数则是轻松的。按照轮询的方式,奇数的任务交给了第一个消费者,所以一直在忙个不停。偶数的任务交给另一个消费者,则立即完成任务,然后闲得不行。而RabbitMQ则是不了解这些的。这是因为当消息进入队列,RabbitMQ就会分派消息。它不看消费者为应答的数目,只是盲目的将消息发给轮询指定的消费者。为了解决这个问题,我们使用basicQos( prefetchCount = 1)方法,来限制RabbitMQ只发不超过1条的消息给同一个消费者。当消息处理完毕后,有了反馈,才会进行第二次发送。还有一点需要注意,使用非公平分发,必须关闭自动应答,改为手动应答。// 同一时刻服务器只会发一条消息给消费者channel.basicQos(1);//开启这行 表示使用手动确认模式channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);同时改为手动确认:// 监听队列,false表示手动返回完成状态,true表示自动channel.basicConsume(QUEUE_NAME, false, consumer);这样的话,消费快的消费者能得到更多的消息
》如果交换机设置为fanout类型,那么routekey是不会起作用的,交换机一定会把它收到的所有消息都发给所有与它绑定的队列,只有当类型为topic或direct时才会起作用
》消息无法由exchange路由到合适的队列的处理方法(这种情况一定是direct或topic,fanout不存在这种问题)
》》1.设置mandatory参数为true
上一篇文章中我们知道,生产者将消息发送到RabbitMQ的交换器中通过RoutingKey与BindingKey的匹配将之路由到具体的队列中以供消费者消费。那么当我们通过匹配规则找不到队列的时候,消息将何去何从呢?Rabbit给我们提供了两种方式。mandatory与备份交换器。mandatory参数是channel.BasicPublish方法中的参数。其主要功能是消息传递过程中不可达目的地时将消息返回给生产者。当mandatory 参数设为true 时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么RabbitMQ 会调用BasicReturn 命令将消息返回给生产者。当mandatory 参数设置为false 时。则消息直接被丢弃。其运转流程与实现代码如下(以C# RabbitMQ.Client 3.6.9为例):注意,这和死信队列没关系,死信是已经在队列中的内容过期后的处理措施,这个mandatory是交换器把这个消息路由不到任何消息队列的处理方法。
![](https://img-blog.csdnimg.cn/20200407170931356.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzc1Mjg1NA==,size_16,color_FFFFFF,t_70)
String message = "hello world";channel.basicPublish("eee", "", true, properties, message.getBytes());System.out.println("[x] Sent '" + message + "'");channel.addReturnListener(new ReturnListener() {public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {System.out.println("replyCode="+replyCode + " replyText="+replyText+" exchange="+exchange + " routeKey="+routingKey+" body="+new String(body)); //#1}});注意,由于是异步的,如果主线程先结束了,#1处的代码可能还没执行就整体结束了。
》》设置备份exchange
当消息不能路由到队列时,通过mandatory设置参数,我们可以将消息返回给生产者处理。但这样会有一个问题,就是生产者需要开一个回调的函数来处理不能路由到的消息,这无疑会增加生产者的处理逻辑。备份交换器(Altemate Exchange)则提供了另一种方式来处理不能路由的消息。备份交换器可以将未被路由的消息存储在RabbitMQ中,在需要的时候去处理这些消息。其主要实现代码如下:Map<String, Object> arguments = new HashMap<String, Object>(16);arguments.put("alternate-exchange", "backup");//普通交换器和普通队列channel.exchangeDeclare("normal", "direct", true, false, arguments);channel.queueDeclare("q1", false, false, false, null);channel.queueBind("q1", "normal", "r1");//备份交换器和备份队列channel.exchangeDeclare("backup", "fanout", true, false, null);channel.queueDeclare("qb", false, false, false, null);channel.queueBind("qb", "backup", "");channel.basicPublish("normal", "", null, "abcdef".getBytes());
![](https://img-blog.csdnimg.cn/20200407170931436.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzc1Mjg1NA==,size_16,color_FFFFFF,t_70)
备份交换器其实和普通的交换器没有太大的区别,为了方便使用,建议设置为fanout类型,若设置为direct 或者topic的类型。需要注意的是,消息被重新发送到备份交换器时的路由键和从生产者发出的路由键是一样的。考虑这样一种情况,如果备份交换器的类型是direct,并且有一个与其绑定的队列,假设绑定的路由键是key1,当某条携带路由键为key2 的消息被转发到这个备份交换器的时候,备份交换器没有匹配到合适的队列,则消息丢失。如果消息携带的路由键为key1,则可以存储到队列中。
对于备份交换器,有以下几种特殊情况:
-
如果设置的备份交换器不存在,客户端和RabbitMQ 服务端都不会有异常出现,此时消息会丢失。
-
如果备份交换器没有绑定任何队列,客户端和RabbitMQ 服务端都不会有异常出现,此时消息会丢失。
-
如果备份交换器没有任何匹配的队列,客户端和RabbitMQ 服务端都不会有异常出现,此时消息会丢失。
-
如果备份交换器和mandatory参数一起使用,那么mandatory参数无效。
》设置消息的TTL:
目前有两种方法可以设置消息的TTL。第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。第二种方法是对消息本身进行单独设置,每条消息的TTL可以不同。如果两种方法一起使用,则消息的TTL 以两者之间较小的那个数值为准。消息在队列中的生存时间一旦超过设置的TTL值时,就会变成"死信" (Dead Message) ,消费者将无法再收到该消息。(有关死信队列请往下看)方法一:给队列参数上加上x-message-ttl属性,这样所有进入该队列的消息都会有统一的过期时间Map<String, Object> arguments = new HashMap<String, Object>(16);arguments.put("x-message-ttl", 10000);arguments.put("x-max-priority", 10);channel.exchangeDeclare("eee", "fanout", true, false, null);channel.queueDeclare(QUEUE_NAME, false, false, false, arguments);方法二:给消息上加上属性:AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().expiration("20000") //表示20000ms.build();String message = "hello world";channel.basicPublish("eee", "", properties, message.getBytes());注意:对于第一种设置队列TTL属性的方法,一旦消息过期,就会从队列中抹去,而在第二种方法中,即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费者之前判定的。Why?在第一种方法里,队列中己过期的消息肯定在队列头部, RabbitMQ 只要定期从队头开始扫描是否有过期的消息即可。而第二种方法里,每条消息的过期时间不同,如果要删除所有过期消息势必要扫描整个队列,所以不如等到此消息即将被消费时再判定是否过期,如果过期再进行删除即可。
》设置队列的TTL
注意,这里和上述通过队列设置消息的TTL不同。上面删除的是消息,而这里删除的是队列。通过channel.queueDeclare 方法中的x-expires参数可以控制队列被自动删除前处于未使用状态的时间。这个未使用的意思是队列上没有任何的消费者,队列也没有被重新声明,并且在过期时间段内也未调用过channel.basicGet命令。设置队列里的TTL可以应用于类似RPC方式的回复队列,在RPC中,许多队列会被创建出来,但是却是未被使用的(有关RabbitMQ实现RPC请往下看)。RabbitMQ会确保在过期时间到达后将队列删除,但是不保障删除的动作有多及时。在RabbitMQ 重启后, 持久化的队列的过期时间会被重新计算。用于表示过期时间的x-expires参数以毫秒为单位, 井且服从和x-message-ttl一样的约束条件,不同的是它不能设置为0(会报错)。Map<String, Object> arguments = new HashMap<String, Object>(16);arguments.put("x-expires", 2000);channel.exchangeDeclare("eee", "fanout", true, false, null);channel.queueDeclare(QUEUE_NAME, false, false, false, arguments);
》对于队列参数的设置,map的内容必须是在声明队列之前就有了,不能设置了arguments后再往map中放东西
Map<String, Object> arguments = new HashMap<String, Object>(16);//死信队列配置 ----------------String dlxExchangeName = "dlx.exchange";String dlxQueueName = "dlx.queue";String dlxRoutingKey = "#";// 为队列设置队列交换器arguments.put("x-dead-letter-exchange", dlxExchangeName); //只能设置死信交换器,不能直接把死信队列与其他队列绑定// 设置队列中的消息 3s 钟后过期arguments.put("x-message-ttl", 3000);channel.exchangeDeclare("eee", "fanout", true, false, null);channel.queueDeclare(QUEUE_NAME, true, false, false, arguments); //给arguments中设置内容不能放在这句后面不然参数不起作用channel.queueBind(QUEUE_NAME, "eee", "”);// 创建死信交换器和队列channel.exchangeDeclare(dlxExchangeName, "topic", true, false, null);channel.queueDeclare(dlxQueueName, true, false, false, null);channel.queueBind(dlxQueueName, dlxExchangeName, "#");
》死信队列必须有一个交换器与之绑定。死信队列一定要持久化,但设置死信队列的队列不一定要持久化
死信队列和死信交换器的声明和普通队列,交换器没什么区别(除了一定要持久化),表明它是死信交换器的位置是把它的名字设置为一个其他队列的“x-dead-letter-exchange” 参数
》可以用过期队列+死信队列 来模拟延迟队列
![](https://img-blog.csdnimg.cn/20200407170931438.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzc1Mjg1NA==,size_16,color_FFFFFF,t_70)
生产者将消息发送到过期时间为n的队列中,这个队列并未有消费者来消费消息,当过期时间到达时,消息会通过死信交换器被转发到死信队列中。而消费者从死信队列中消费消息。这个时候就达到了生产者发布了消息在讲过了n时间后消费者消费了消息,起到了延迟消费的作用。
延迟队列在我们的项目中可以应用于很多场景,如:下单后两个消息取消订单,七天自动收货,七天自动好评,密码冻结后24小时解冻,以及在分布式系统中消息补偿机制(1s后补偿,10s后补偿,5m后补偿......)。
》优先级队列:
就像我们生活中的“特殊”人士一样,我们的业务上也存在一些“特殊”消息,可能需要优先进行处理,在生活上我们可能会对这部分特殊人士开辟一套VIP通道,而Rabbit同样也有这样的VIP通道(前提是在3.5的版本以后),即优先级队列,队列中的消息会有优先级。优先级高的消息具备优先被消费的特权。针对这些VIP消息,我们只需做两件事:我们只需做两件事情:1.将队列声明为优先级队列,即在创建队列的时候添加参数 x-max-priority 以指定最大的优先级,值为0-255(整数)。2.为优先级消息添加优先级。其示例代码如下:channel.exchangeDeclare("exchange.priority", "direct", true);//定义交换器Map<String, Object> arguments = new HashMap<String, Object>(16);// 为队列设置队列交换器arguments.put("x-dead-letter-exchange", dlxExchangeName);// 设置队列中的消息 ms 钟后过期arguments.put("x-message-ttl", 10000);arguments.put("x-max-priority", 10);args.Add("x-max-priority", 10);//定义优先级队列的最大优先级为10channel.queueDeclare("queue.priority", true, false, false, args);//定义优先级队列channel.queueBind("queue.priority", "exchange.priority", "priorityKey");//队列交换器绑定AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().priority(8).build();var message = “testMsg8";//发布消息channel.BasicPublish("exchange.priority", "priorityKey", properties, message);注意:没有指定优先级的消息会将优先级以0对待。 对于超过优先级队列所定最大优先级的消息,优先级以最大优先级对待。对于相同优先级的消息,后进的排在前面。如果在消费者的消费速度大于生产者的速度且Broker 中没有消息堆积的情况下, 对发送的消息设置优先级也就没有什么实际意义。因为生产者刚发送完一条消息就被消费者消费了,那么就相当于Broker 中至多只有一条消息,对于单条消息来说优先级是没有什么意义的。关于优先级队列,好像违背了队列这种数据结构先进先出的原则,其具体是怎么实现的在这里就不过多讨论。