RabbitMQ之Queues-5
工作队列的主要任务是:避免立刻执行资源密集型任务,然后必须等待其完成。相反地,我们进行任务调度:我们把任务封装为消息发送给队列。工作进行在后台运行并不断的从队列中取出任务然后执行。当你运行了多个工作进程时,任务队列中的任务将会被工作进程共享执行.
该模型适用于分发资源密集型的任务,多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
轮询调度Round-robin(负载均衡算法)
采用这种模式,消息在队列中保存,以轮询的方式将消息发送给监听消息队列的消费者,可以动态的增加消费者以提高消息的处理能力。
java代码示例展示:(发送方发送多条消息,多个接收方接收,这里演示两个接收方)
代码如下:
发送方代码:
package com.teaboy.rabitmq.client.publish; import java.io.IOException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; /** * 消息轮询 * 假设有两个或多个消费者,发送方发送的消息会顺序轮询的发送给消费者 * 例如:有消费者01和消费者02,发送的第一个消息由第一个消费者01接收, * 发送的第二个消息由第二个消费者02接收,发送的第三个消息又会由第一个 * 消费者01接收,以此类推 * * */ public class Send{ private final static String QUEUE_NAME ="IhotelOrderQueue"; public static void main(String[] args) throws IOException{ ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); factory.setPort(5678); Connection con = factory.newConnection(); Channel channel = con.createChannel(); channel.queueDeclare(QUEUE_NAME, true, false,false,null); String[] msgs = {"第一个消息!","第二个消息!","第三个消息!","第四个消息!","第五个消息!"}; //发送多个消息 for(String msg:msgs){ channel.basicPublish("", QUEUE_NAME, null, msg.getBytes()); System.out.println("send message["+msg+"] to "+QUEUE_NAME+" success!"); } channel.close(); con.close(); } }
Receive01接收方代码:
package com.teaboy.rabitmq.client.consumer; import java.io.IOException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.ConsumerCancelledException; import com.rabbitmq.client.QueueingConsumer; import com.rabbitmq.client.ShutdownSignalException; import com.rabbitmq.client.QueueingConsumer.Delivery; /** * * (类型功能说明描述) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年10月7日 下午7:19:46 user 1.0 初始化创建<br> * </p> * * @author Peng.Li * @version 1.0 * @since JDK1.7 */ public class Receive01 { private final static String QUEUE_NAME ="IhotelOrderQueue"; public static void main(String[] args) throws IOException, ShutdownSignalException, ConsumerCancelledException, InterruptedException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); factory.setPort(5678); Connection conn; conn = factory.newConnection(); Channel channel = conn.createChannel(); channel.queueDeclare(QUEUE_NAME, true, false, false, null); //配置好获取消息得方式 QueueingConsumer consumer = new QueueingConsumer(channel); //自动确认 boolean autoAck = true; channel.basicConsume(QUEUE_NAME, autoAck,consumer); //循环获取消息 while(true){ //获取消息,如果没有消息,这一步将会一直阻塞 Delivery delivery = consumer.nextDelivery(); String msg = new String(delivery.getBody()); System.out.println("received message["+msg+"] from "+QUEUE_NAME); } } }
Receive02接收方代码:
package com.teaboy.rabitmq.client.consumer; import java.io.IOException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.ConsumerCancelledException; import com.rabbitmq.client.QueueingConsumer; import com.rabbitmq.client.ShutdownSignalException; import com.rabbitmq.client.QueueingConsumer.Delivery; public class Receive02 { private final static String QUEUE_NAME ="IhotelOrderQueue"; public static void main(String[] args) { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); factory.setPort(5678); Connection conn; try { conn = factory.newConnection(); Channel channel = conn.createChannel(); channel.queueDeclare(QUEUE_NAME, true, false, false, null); //以上部分和sender一样 //配置好获取消息得方式 QueueingConsumer consumer = new QueueingConsumer(channel); boolean autoAck = true; channel.basicConsume(QUEUE_NAME, autoAck,consumer); //循环获取消息 while(true){ //获取消息,如果没有消息,这一步将会一直阻塞 Delivery delivery = consumer.nextDelivery(); String msg = new String(delivery.getBody()); System.out.println("received message["+msg+"] from "+QUEUE_NAME); } } catch (IOException e) { e.printStackTrace(); } catch (ShutdownSignalException e) { e.printStackTrace(); } catch (ConsumerCancelledException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }
先运行接收方代码,再发送消息
结果如下:
发送方发送的5个消息:
Receive01接收到的消息:为1,3,5:
Receive02接收到的消息为:2,4:
从结果可以看出,默认的rabbitMq一个一个发送消息给下一个消费者(consumer),而不考虑每个任务的时长等,且是一次性分配,
并非一个一个的分配,平均的每个消费者将会获得相等数量的消息。
这样分发消息的方式叫做round-robin。round-robin算法缺点是不关心每台服务器的当前连接数和响应速度,当请求服务间隔时间变化比较大时,
轮询调度算法容易导致服务器间的负载不平衡。
图1
消息确认(message acknowledgments):为了确保消息一定被消费者处理,rabbitMQ提供了消息确认功能,就是在消费者处理完任务之后,就给服务器一个回馈,服务器就会将该消息删除,如果消费者超时不回馈,那么服务器将就将该消息重新发送给其他消费者。从图1 看到,queues message中的被两个消费者消费后,自动删除消息,由于消费的速度比较快,从图中看不到消息。
执行一个任务需要花费几秒钟。你可能会担心当一个工作者在执行任务时发生中断。我们上面的代码,一旦RabbItMQ交付了一个信息给消费者,会马上从内存中(队列中)移除这个信息。在这种情况下,如果杀死正在执行任务的某个工作者,我们会丢失它正在处理的信息。我们也会丢失已经转发给这个工作者且它还未执行的消息。上面的例子,我们开启Receive01,Receive02,然后执行发送任务代码Send,这时候立即关闭Receive02,可以看到控制台结果:
Receive01接收到的消息为 1,3,5,7,9
Receive02的消息丢失。
但是,我们不希望丢失任何任务(信息)。当某个工作者(接收者)被杀死时,我们希望将任务传递给另一个工作者。
为了保证消息永远不会丢失,RabbitMQ支持消息应答(message acknowledgments)。消费者发送应答给RabbitMQ,告诉它信息已经被接收和处理,然后RabbitMQ可以自由的进行信息删除。
如果消费者被杀死而没有发送应答,RabbitMQ会认为该信息没有被完全的处理,然后将会重新转发给别的消费者。通过这种方式,你可以确认信息不会被丢失,即使消者偶尔被杀死。看下面的例子:
//自动确认 boolean autoAck = true; channel.basicConsume(QUEUE_NAME, autoAck,consumer);
如果把代码中的两个消费者Receive01和Receive02中的autoAck改为false,看下运行结果:
控制台打印结果:
Receive01:
Receive02:
从控制台看出确实消费了生产者生产的5个消息:但是从rabbitMq后台管理看图,有5个消息一直为unacked,有total为5个消息一直在队列中,也即在内存中。
将消息改为手动确认的方式需要修改接收者代码(发送方代码不变):
1、设置接收方为手动方式
2、接收到消息后回复确认标志
//自动确认关闭
boolean autoAck = false; channel.basicConsume(QUEUE_NAME, autoAck,consumer); //循环获取消息 while(true){ //获取消息,如果没有消息,这一步将会一直阻塞 Delivery delivery = consumer.nextDelivery(); String msg = new String(delivery.getBody()); System.out.println("received message["+msg+"] from "+QUEUE_NAME); //消息真正接收成功,发送应答,消息可被删除 channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); }
这样即使关闭Receive02,Receive02没有完成的任务,没有确认的消息也会被转发给Receive01,即使消费者被杀死,消息也不会被丢失。
消息持久化(Message durability):
我们已经学习了即使消费者被杀死,消息也不会被丢失。但是如果此时RabbitMQ服务被停止,我们的消息仍然会丢失。当RabbitMQ退出或者异常退出,将会丢失所有的队列和信息,除非你告诉它不要丢失。我们需要做两件事来确保信息不会被丢失:我们需要给所有的队列和消息设置持久化的标志。
第一, 我们需要确认RabbitMQ永远不会丢失我们的队列。为了这样,我们需要声明它为持久化的。
/** * 声明消息为持久化,消息持久化,当rabbitmq退出或崩溃后,会把queue中的消息持久化。但注意, * RabbitMQ并不能百分之百保证消息一定不会丢失,因为为了提 升性能,RabbitMQ会把消息暂存在内存缓存中, * 直到达到阀值才会批量持久化到磁盘,也就是说如果在持久化到磁盘之前RabbitMQ崩溃了, * 那么就 会丢失一小部分数据,这对于大多数场景来说并不是不可接受的,如果确实需要保证任务绝对不丢失,那么应该使用事务机制 */ boolean durable = true; channel.queueDeclare(queue, durable, false, false, null);
注:RabbitMQ不允许使用不同的参数重新定义一个队列,所以已经存在的队列,我们无法修改其属性。
第二, 我们需要标识我们的信息为持久化的。通过设置MessageProperties(implements BasicProperties)值为PERSISTENT_TEXT_PLAIN。
//发送持久化消息 channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
公平转发(Fair dispatch):
目前的消息转发机制(Round-robin)并非是我们想要的。因为上面提到这种算法有缺点,round-robin算法缺点是不关心每台服务器的当前连接数和响应速度,当请求服务间隔时间变化比较大时,轮询调度算法容易导致服务器间的负载不平衡。例如,这样一种情况,对于两个消费者,有一系列的任务,奇数任务特别耗时,而偶数任务却很轻松,这样造成一个消费者一直繁忙,另一个消费者却很快执行完任务后等待。
造成这样的原因是因为RabbitMQ仅仅是当消息到达队列进行转发消息。并不在乎有多少任务消费者并未传递一个应答给RabbitMQ。仅仅盲目转发所有的奇数给一个消费者,偶数给另一个消费者。
为了解决这样的问题,我们可以使用basicQos方法,传递参数为prefetchCount = 1。这样告诉RabbitMQ不要在同一时间给一个消费者超过一条消息。换句话说,只有在消费者空闲的时候会发送下一条信息。
注:如果所有的工作者都处于繁忙状态,你的队列有可能被填充满。你可能会观察队列的使用情况,然后增加工作者,或者使用别的什么策略。
关键代码:
// 设置公平调度的标志 int prefetchCount = 1; channel.basicQos(prefetchCount);
完整代码如下:
发送端:
package com.teaboy.rabitmq.client.publish; import java.io.IOException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.MessageProperties; /** * * (类型功能说明描述) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年10月6日 下午9:52:02 user 1.0 初始化创建<br> * </p> * * @author Peng.Li * @version 1.0 * @since JDK1.7 */ public class SendQos { public static final String queue = "IhotelOrderQueue"; private static final String EXCHANGE_NAME = "IhotelOrderExchange"; private static final String ROUTING_KEY = "hello.P1"; public static void main(String[] args) throws IOException { ConnectionFactory factory = new ConnectionFactory(); //user@localhost:/usr/local/etc/rabbitmq> cat rabbitmq-env.conf -->NODE_IP_ADDRESS 127.0.0.1 factory.setHost("127.0.0.1"); //RABBITMQ_NODE_PORT=5678 factory.setPort(5678); //创建一个连接 Connection conn = factory.newConnection(); //创建一个channel Channel channel = conn.createChannel(); /** * 声明消息为持久化,消息持久化,当rabbitmq退出或崩溃后,会把queue中的消息持久化。但注意, * RabbitMQ并不能百分之百保证消息一定不会丢失,因为为了提 升性能,RabbitMQ会把消息暂存在内存缓存中, * 直到达到阀值才会批量持久化到磁盘,也就是说如果在持久化到磁盘之前RabbitMQ崩溃了, * 那么就 会丢失一小部分数据,这对于大多数场景来说并不是不可接受的,如果确实需要保证任务绝对不丢失,那么应该使用事务机制 */ boolean durable = true; channel.queueDeclare(queue, durable, false, false, null); for(int i=0;i<10;i++){ String msg = "公平调度:message-"+i; //发送持久化消息 channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes()); System.out.println("send message["+msg+"] to "+queue+"success!"); } //关闭通道 channel.close(); //关闭连接 conn.close(); } }
接收端RecieveQos1
package com.teaboy.rabitmq.client.consumer; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.QueueingConsumer; import com.rabbitmq.client.QueueingConsumer.Delivery; /** * * (公平调度) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年10月7日 上午10:48:09 Peng.Li 1.0 初始化创建<br> * </p> * * @author Peng.Li * @version 1.0 * @since JDK1.7 */ public class RecvQos1 { public static void main(String[] args) throws Exception{ ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); factory.setPort(5678); Connection conn = factory.newConnection(); Channel channel = conn.createChannel(); //设置公平调度的标志 int prefetchCount = 1; channel.basicQos(prefetchCount); String queueName = "IhotelOrderQueue"; channel.queueDeclare(queueName, true, false, false, null); //以上部分和sender一样 //配置好获取消息得方式 QueueingConsumer consumer = new QueueingConsumer(channel); channel.basicConsume(queueName, false,consumer); //循环获取消息 while(true){ //获取消息,如果没有消息,这一步将会一直阻塞 Delivery delivery = consumer.nextDelivery(); String msg = new String(delivery.getBody()); Thread.sleep(1000); //确认消息,已经收到 channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false); System.out.println("received message["+msg+"] from "+queueName); } } }
接收端RecieveQos2:
package com.teaboy.rabitmq.client.consumer; import java.io.IOException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.ConsumerCancelledException; import com.rabbitmq.client.QueueingConsumer; import com.rabbitmq.client.QueueingConsumer.Delivery; import com.rabbitmq.client.ShutdownSignalException; /** * * (类型功能说明描述) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年10月6日 下午9:51:56 user 1.0 初始化创建<br> * </p> * * @author Peng.Li * @version 1.0 * @since JDK1.7 */ public class RecieveQos2 { public static final String queueName = "IhotelOrderQueue"; public static void main(String[] args) throws IOException, ShutdownSignalException, ConsumerCancelledException, InterruptedException { // 创建conn的制造工厂 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); factory.setPort(5678); // 创建一个连接,RabbitMQ基于socket的链接,它封装了socket协议相关部分逻辑。 Connection conn = factory.newConnection(); // 创建channel Channel channel = conn.createChannel(); // 设置公平调度的标志 int prefetchCount = 1; channel.basicQos(prefetchCount); /** * 声明消息为持久化,消息持久化,当rabbitmq退出或崩溃后,会把queue中的消息持久化。但注意, * RabbitMQ并不能百分之百保证消息一定不会丢失,因为为了提 升性能,RabbitMQ会把消息暂存在内存缓存中, * 直到达到阀值才会批量持久化到磁盘,也就是说如果在持久化到磁盘之前RabbitMQ崩溃了, * 那么就 会丢失一小部分数据,这对于大多数场景来说并不是不可接受的,如果确实需要保证任务绝对不丢失,那么应该使用事务机制 */ boolean durable = true; channel.queueDeclare(queueName, durable, false, false, null); // 自动确认为false boolean autoAck = false; QueueingConsumer queueingConsumer = new QueueingConsumer(channel); channel.basicConsume(queueName, autoAck, queueingConsumer); while (true) { // 获取消息,如果没有消息,这一步将会一直阻塞 Delivery delivery = queueingConsumer.nextDelivery(); String msg = new String(delivery.getBody()); // 为了更好的观察公平调度原则,把接收者设定不同的沉睡时间 Thread.sleep(1000); // 发送应答 channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); System.out.println("received message[" + msg + "] from " + queueName); } } }
控制台打印结果:
消费者1
消费者2
RabbitMq后台如图:
总结:可以看出此时并没有按照之前的Round-robin机制进行转发消息,而是当消费者不忙时进行转发。且这种模式下支持动态增加消费者,因为消息并没有 发送出去,动态增加了消费者马上投入工作。而默认的转发机制会造成,即使动态增加了消费者,此时的消息已经分配完毕,无法立即加入工作,即使有很多未完成 的任务。