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);

如果把代码中的两个消费者Receive01Receive02中的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); }

这样即使关闭Receive02Receive02没有完成的任务,没有确认的消息也会被转发给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机制进行转发消息,而是当消费者不忙时进行转发。且这种模式下支持动态增加消费者,因为消息并没有 发送出去,动态增加了消费者马上投入工作。而默认的转发机制会造成,即使动态增加了消费者,此时的消息已经分配完毕,无法立即加入工作,即使有很多未完成 的任务。

 

 
posted @ 2015-10-07 23:27  积淀  阅读(573)  评论(0编辑  收藏  举报