参考http://blog.csdn.net/lmj623565791/article/details/37620057和RabbitMQ官网,加之自己部分修改和实验,因是新手自学,难免有不对之处,欢迎大家指正,小弟先谢过了.
这篇文章主要是学习下RabbitMQ 工作队列是如何分发任务给消费者的.
关于工作队列,官网原文如下:
The main idea behind Work Queues (aka: Task Queues) is to avoid doing a resource-intensive task immediately and having to wait for it to complete. Instead we schedule the task to be done later. We encapsulate a task as a message and send it to the queue. A worker process running in the background will pop the tasks and eventually execute the job. When you run many workers the tasks will be shared between them.
大概翻译一下—-工作队列的主要任务是:避免立即执行资源密集型的任务,并等待它完成任务. 相反,我们有计划的去执行这些任务: 我们把一个任务封装成一条消息并把它发送到队列里面去,此时,一个工作在后台的消费者会把该消息拿到并去执行它.当有多个消费者同时运行时,这些消息会被这些消费者共享.
开始
我们用一些有规律的字符串代表工作任务的耗时性, 一个.代表耗时1秒,每加一个.就表示该任务耗时增加1秒,例如: …..表示该任务将消耗5秒钟.现在先将这些悬赏任务写出来:
MyTask.java
// 队列名称
private final static String queneName = "workquene1";
// 我们在发送到队列的消息的末尾添加一定数量的点
// 每个点代表在工作线程中需要耗时1秒,例如hello…将会需要等待3秒
public static void main(String[] args) throws Exception {
// 创建连接和频道
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection newConn = factory.newConnection();
Channel channel = newConn.createChannel();
// 声明队列
channel.queueDeclare(queneName, false, false, false, null);
// 发送10条消息
for (int i = 0; i < 10; i++) {
String dots = "";
for (int j = 0; j <= i ; j++) {
dots += ".";
}
String msg = "helloworld" + dots + dots.length();
// 发送消息
channel.basicPublish("", queneName, null, msg.getBytes());
System.out.println("发送了: '" + msg + "'");
}
channel.close();
newConn.close();
}
消费者:NewCustomer.java
private final static String queneName = "workquene1";
public static void main(String[] args) throws Exception {
// 取进程号
String name = ManagementFactory.getRuntimeMXBean().getName();
String pid = name.split("@")[0];
//int customerHashcode = NewCustomer.class.hashCode();
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
Connection conn = factory.newConnection();
// 创建频道
Channel channel = conn.createChannel();
// 声明队列
channel.queueDeclare(queneName, false, false, false, null);
System.out.println(pid + "**** 在等待消息");
QueueingConsumer consumer = new QueueingConsumer(channel);
// 指定消费队列, 关闭消息确认机制
// channel.basicConsume(queneName, true, consumer);
// int prefetch = 1;
// channel.basicQos(prefetch);
// 指定消费队列, 关闭消息确认机制
channel.basicConsume(queneName, true, consumer);
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println(pid + " 已经接到一条消息---" + msg);
doWork(msg);
System.out.println(pid + " 已经消费了一条消息---" + msg);
// channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
private static void doWork(String msg) throws InterruptedException {
for (char ch : msg.toCharArray()) {
if (ch == '.') {
Thread.sleep(1000);
}
}
}
Round-robin 式分发任务
使用工作队列的好处之一是可以更容易的处理并行问题,假如我们有很多挤压任务,我们可以仅仅增加消费者的数目就可以解决问题,非常的弹性.看下上面程序输出结果:
-----生产者输出-----
发送了: 'helloworld.1'
发送了: 'helloworld..2'
发送了: 'helloworld...3'
发送了: 'helloworld....4'
发送了: 'helloworld.....5'
发送了: 'helloworld......6'
发送了: 'helloworld.......7'
发送了: 'helloworld........8'
发送了: 'helloworld.........9'
发送了: 'helloworld..........10'
-----消费者输出-----
2380**** 在等待消息
2380 已经接到一条消息---helloworld..2
2380 已经消费了一条消息---helloworld..2
2380 已经接到一条消息---helloworld....4
2380 已经消费了一条消息---helloworld....4
2380 已经接到一条消息---helloworld......6
2380 已经消费了一条消息---helloworld......6
2380 已经接到一条消息---helloworld........8
2380 已经消费了一条消息---helloworld........8
2380 已经接到一条消息---helloworld..........10
2380 已经消费了一条消息---helloworld..........10
4212**** 在等待消息
4212 已经接到一条消息---helloworld.1
4212 已经消费了一条消息---helloworld.1
4212 已经接到一条消息---helloworld...3
4212 已经消费了一条消息---helloworld...3
4212 已经接到一条消息---helloworld.....5
4212 已经消费了一条消息---helloworld.....5
4212 已经接到一条消息---helloworld.......7
4212 已经消费了一条消息---helloworld.......7
4212 已经接到一条消息---helloworld.........9
默认的,RabbitMQ会把消息按顺序一条一条的发送给下一个消费者,不管任务的耗时多少,并且是一次性的分配(这点可以从输出结果判断:进程号4212在执行完1秒的任务后,并没有抢着执行2380的进程的2秒任务,二十执行自己的3秒任务,互不干涉),消费者会平均得获得消息. 这种分发任务的方式就叫做round-robin
方便是方便,但是仔细想想上面那种默认的分发任务的方式在实际情况下是不是有点问题?
问题一: 如果我的消费者程序突然挂了,那分配给我的消息怎么办?是不是跟着一起消失了?如何确保我消息的处理完整性?
问题二:如果我奇数的任务都是比较耗时的,偶数的任务都是轻松加愉快的,那是不是对消费者1和消费者2有点不公平呢?
先来看下问题的一的现象,比如我现在暴力点,在1552进程接收耗时4秒的任务时,关掉1552进程,看下会如何
-----生产者输出-----
发送了: 'helloworld.1'
发送了: 'helloworld..2'
发送了: 'helloworld...3'
发送了: 'helloworld....4'
发送了: 'helloworld.....5'
发送了: 'helloworld......6'
发送了: 'helloworld.......7'
发送了: 'helloworld........8'
发送了: 'helloworld.........9'
发送了: 'helloworld..........10'
-----消费者输出------
1552**** 在等待消息
1552 已经接到一条消息---helloworld..2
1552 已经消费了一条消息---helloworld..2
1552 已经接到一条消息---helloworld....4
4516**** 在等待消息
4516 已经接到一条消息---helloworld.1
4516 已经消费了一条消息---helloworld.1
4516 已经接到一条消息---helloworld...3
4516 已经消费了一条消息---helloworld...3
4516 已经接到一条消息---helloworld.....5
4516 已经消费了一条消息---helloworld.....5
4516 已经接到一条消息---helloworld.......7
4516 已经消费了一条消息---helloworld.......7
4516 已经接到一条消息---helloworld.........9
我们从输出可以发现, 我的4,6,8,10消息直接蒸发了…这明显不科学,还好RabbitMQ给我们提供了一种message acknowledgment的消息机制, 该机制默认是打开的,只不过我把它关了而已,修改上面NewCustomer.java程序(第24行)
// true为关闭该机制, false为打开
channel.basicConsume(queneName, false, consumer);
// 然后在处理完任务后, 手动发送一次消息确认
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
修改后,看下修改效果:
-----生产者输出-----
发送了: 'helloworld.1'
发送了: 'helloworld..2'
发送了: 'helloworld...3'
发送了: 'helloworld....4'
发送了: 'helloworld.....5'
发送了: 'helloworld......6'
发送了: 'helloworld.......7'
发送了: 'helloworld........8'
发送了: 'helloworld.........9'
发送了: 'helloworld..........10'
-----消费者输出------
5068**** 在等待消息
5068 已经接到一条消息---helloworld..2
5068 已经消费了一条消息---helloworld..2
5068 已经接到一条消息---helloworld....4
3328**** 在等待消息
3328 已经接到一条消息---helloworld.1
3328 已经消费了一条消息---helloworld.1
3328 已经接到一条消息---helloworld...3
3328 已经消费了一条消息---helloworld...3
3328 已经接到一条消息---helloworld.....5
3328 已经消费了一条消息---helloworld.....5
3328 已经接到一条消息---helloworld.......7
3328 已经消费了一条消息---helloworld.......7
3328 已经接到一条消息---helloworld.........9
3328 已经消费了一条消息---helloworld.........9
3328 已经接到一条消息---helloworld....4
3328 已经消费了一条消息---helloworld....4
3328 已经接到一条消息---helloworld......6
3328 已经消费了一条消息---helloworld......6
3328 已经接到一条消息---helloworld........8
3328 已经消费了一条消息---helloworld........8
3328 已经接到一条消息---helloworld..........10
可以看到我在进程5068在接收耗时4秒的任务时,突然关掉该进程,然后3328进程开始先把自己的任务执行完,接着执行5068未完成的任务,问题一(处理消息完整性问题)已解决.
好,现在再来看问题二:关于消息分配的公平性问题.先弄清楚为什么会出现问题二,官方解释:This happens because RabbitMQ just dispatches a message when the message enters the queue. It doesn’t look at the number of unacknowledged messages for a consumer. It just blindly dispatches every n-th message to the n-th consumer.翻译一下: 是因为消息到达队列时,RabbitMQ只是简单的将其分发下去,它并不关心有多少消费者并未给acknowledge,它只是盲目的将每一个消息给一个消费者,然后再给下一个消费者. 可以看出 RabbitMQ还是挺笨的(o(∩_∩)o ). 为了解决这个问题,可以将代码进行以下修改:
//RabbitMQ不会发消息给该消费者,除非该消费者已经处理完当前的消息
channel.basicQos(1);
这样也会导致一个问题, 就是如果消费者不够,而消息耗时比较大的话,会导致消息积压在队列里,这时可以观察队列消息情况,增加消费者.修改NewTask.java
// 把耗时任务改成6到1
for (int i = 5; i >= 0; i--) {
//.....
}
运行效果:
-----生产者输出------
发送了: 'helloworld......6'
发送了: 'helloworld.....5'
发送了: 'helloworld....4'
发送了: 'helloworld...3'
发送了: 'helloworld..2'
-----消费者输出-----
4360**** 在等待消息
4360 已经接到一条消息---helloworld......6
4360 已经消费了一条消息---helloworld......6
4360 已经接到一条消息---helloworld...3
4360 已经消费了一条消息---helloworld...3
4360 已经接到一条消息---helloworld..2
5816**** 在等待消息
5816 已经接到一条消息---helloworld.....5
5816 已经消费了一条消息---helloworld.....5
5816 已经接到一条消息---helloworld....4
5816 已经消费了一条消息---helloworld....4