在上一篇文章在Node.js中使用RabbitMQ系列一 Hello world我有使用一个任务队列,不过当时的场景是将消息发送给一个消费者,本篇文章我将讨论有多个消费者的场景。

其实,任务队列最核心解决的问题是避免立即处理那些耗时的任务,也就是避免请求-响应的这种同步模式。取而代之的是我们通过调度算法,让这些耗时的任务之后再执行,也就是采用异步的模式。我们需要将一条消息封装成一个任务,并且将它添加到任务队列里面。后台会运行多个工作进程(worker process),通过调度算法,将队列里的任务依次弹出来,并交给其中的一个工作进程进行处理执行。这个概念尤其适合那些HTTP短连接的web应用,它们无法在短时间内处理这种复杂的任务。

准备工作

我们将字符串类型的消息看作是耗时的任务,并且每个字符串消息最后带上一些点。每个点代表该任务需要消耗的秒数。在worker进程处理的时候可以采用setTimeout函数来进行模拟。举个例子:一个伪造的耗时任务是Hello.,则这个任务会消耗1秒,一个伪造的耗时任务是Hello..,则这个任务会消耗2秒,一个伪造的耗时任务是Hello... ,并且这个任务的处理时间会耗费3秒。

这里稍微对上篇文章的send.js文件修改,让它可以发送用户自定义的任意的消息,这个程序会将任务交给任务队列,我们用new_task.js来进行命名:

var q = 'task_queue';
var msg = process.argv.slice(2).join(' ') || "Hello World!"; ch.assertQueue(q, {durable: true});
ch.sendToQueue(q, new Buffer(msg), {persistent: true});
console.log(" [x] Sent '%s'", msg);

我们的receive.js文件也要进行修改,它需要伪造任务的处理时间,让字符串类型的任务看起来需要几秒钟时间(具体取决于 . 的个数)。

ch.consume(q, function(msg) {
var secs = msg.content.toString().split('.').length - 1; console.log(" [x] Received %s", msg.content.toString());
setTimeout(function() {
console.log(" [x] Done");
}, secs * 1000);
}, {noAck: true});

这里以一个worker工作进程为例子,我们先在左侧的shell窗口开启一个worker进程,之后在右侧shell窗口发送自定义的消息,执行看看效果:

轮询调度算法(Round-robin)

使用任务队列的一个优势是能够将任务并行执行,如果需要处理大量的积压任务,我们只需要像上面运行worker进程的方式,增加更多的worker,这个让可伸缩变得更加的容易。

首先,我们使用item2开启2个shell窗口,并在里面运行两个worker进程,但是到底是哪个worker会对消息进行处理呢?这里我们可以做个简单的实验来看看,如下图所示,左侧是两个worker进程,右侧是消息发送端:

默认情况下,RabbitMQ会采用Round-robin算法来分发任务队列中的任务,每次分发的时候都会将任务派发给下一个消费者,这样每个消费者(worker进程)处理的任务数量其实是一样多的。

消息确认

处理一个复杂的任务需要耗费很长时间,这个时间段里面,可能我们的worker进程由于某种原因挂掉了,这种异常情况是需要考虑的。但是我们现有的代码里面并没有做这种异常的处理,当RabbitMQ将任务派发给worker进程之后,我们立即将这个任务从内存中剔除掉了,设想下,假设worker收到消息之后,我们马上将进程杀死掉,这个时候任务并没有被成功执行的。同时,我们也会丢失所有派发到这个worker进程但是还没有被处理的任务信息。

但是,我们并不想丢掉任何一个任务,如果一个worker进程挂掉,我们更希望能够将这个任务派发给其它的worker来处理。

为了避免任务信息丢失的情况,RabbitMQ支持消息确认。在一个任务发送到了worker进程并且被成功处理完毕之后,一个ack (消息确认)的标识会从消费者发回来告诉RabbitMQ这个任务已经被处理完了,可以将它删除了。

如果一个消费者挂掉了(常见的原因如消息通道关闭了,连接丢失,TCP连接丢失),没有向RabbitMQ发送消息确认这个ack的标识,这个时候RabbitMQ会将它从新加入到队列中,如果有其它消费者存在,那么RabbitMQ会马上将这个任务重新派发下去。之前的例子里面我们并没有开启消息确认这个选项,现在我们可以通过{noAck: false}来开启:

ch.consume(q, function(msg) {
var secs = msg.content.toString().split('.').length - 1; console.log(" [x] Received %s", msg.content.toString());
setTimeout(function() {
console.log(" [x] Done");
ch.ack(msg);
}, secs * 1000);
}, {noAck: false}); // 开启消息确认标识

可以用CTRL + C来做个实验看看效果。

消息持久化

刚刚谈到,如果一个worker进程挂掉了,不让消息丢失的做法。但是,如果整个RabbitMQ的服务器挂掉了呢?当一个RabbitMQ服务退出或者中断的情况下,它会忘记任务队列里面的消息除非你告诉它不要丢掉,即我们通知RabbitMQ任务队列和这些任务都是需要持久化的。

首先,我们需要确保RabbitMQ永远不会丢失掉我们的任务队列。

ch.assertQueue('hello', {durable: true});

但是,你会发现这样并没有效果,那是因为hello这个队列我们已经定义过,并且指定了它不需要持久化。RabbitMQ不允许我们通过改变参数配置的方式对已经存在的任务队列进行重新定义,因此我们需要定义一个新的任务队列。

ch.assertQueue('task_queue', {durable: true});

这行代码需要同时在生产者和消费者里面的相关代码的地方进行修改。

接下来,我们需要通过配置persistent 选项让我们发送的消息也是持久化的。

ch.sendToQueue(q, new Buffer(msg), {persistent: true});

公平调度

前面的例子,我们讨论了RabbitMQ的调度方式,即采用Round-robin轮询调度算法,因此它会将消息均匀的分配给每个worker进程。RabbitMQ并不会关注每个worker进程有多少个消息没有确认,它只会不断的给你派发任务,不管你能不能处理的过来。这个时候,问题就出现了,设想下,假设有2个worker,其中1个worker刚好很不幸被分配了一个非常复杂的任务,可能需要耗费好几个小时的时间,另外一个worker被分配的任务都比较简单,只需要几分钟就能处理完,由于RabbitMQ的任务分配问题,有很多新的任务依然会分配到那个正在处理很耗时任务的worker上面,这个worker后面的任务都会处于等待状态。幸好,RabbitMQ可以通过prefetch(1)来指定某个worker同时最多只会派发到1个任务,一旦任务处理完成发送了确认通知,才会有新的任务派发过来。

ch.prefetch(1);

最终的代码

new_task.js 代码:

var amqp = require('amqplib/callback_api');

amqp.connect('amqp://localhost', function(err, conn) {
conn.createChannel(function(err, ch) {
var q = 'task_queue';
var msg = process.argv.slice(2).join(' ') || "Hello World!"; ch.assertQueue(q, {durable: true});
ch.sendToQueue(q, new Buffer(msg), {persistent: true});
console.log(" [x] Sent '%s'", msg);
});
setTimeout(function() { conn.close(); process.exit(0) }, 500);
});

worker.js:

var amqp = require('amqplib/callback_api');

amqp.connect('amqp://localhost', function(err, conn) {
conn.createChannel(function(err, ch) {
var q = 'task_queue'; ch.assertQueue(q, {durable: true});
ch.prefetch(1);
console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", q);
ch.consume(q, function(msg) {
var secs = msg.content.toString().split('.').length - 1; console.log(" [x] Received %s", msg.content.toString());
setTimeout(function() {
console.log(" [x] Done");
ch.ack(msg);
}, secs * 1000);
}, {noAck: false});
});
});

在Node.js中使用RabbitMQ系列二 任务队列的更多相关文章

  1. 在Node.js中使用RabbitMQ系列一 Hello world

    在前一篇文章中可伸缩架构简短系列中提到过关于异步的问题.当时推荐使用RabbitMQ来做任务队列的实现方案.本篇文章以Node.js为例子,来实际操作如何和RabbitMQ进行交互. 介绍 Rabbi ...

  2. [转]在Node.js中使用RabbitMQ系列一 Hello world

    本文转自:https://www.cnblogs.com/cpselvis/p/6288330.html 在前一篇文章中可伸缩架构简短系列中提到过关于异步的问题.当时推荐使用RabbitMQ来做任务队 ...

  3. Cookie和Session在Node.JS中的实践(二)

    Cookie和Session在Node.JS中的实践(二) cookie篇在作者的上一篇文章Cookie和Session在Node.JS中的实践(一)已经是写得算是比较详细了,有兴趣可以翻看,这篇是s ...

  4. 学废了系列 - WebGL与Node.js中的Buffer

    WebGL 和 Node.js 中都有 Buffer 的使用,简单对比记录一下两个完全不相干的领域中 Buffer 异同,加强记忆. Buffer 是用来存储二进制数据的「缓冲区」,其本身的定义和用途 ...

  5. 在node.js中使用COOKIE

    node.js中如何向客户端发送COOKIE呢?有如下两个方案: 一.使用response.writeHead,代码示例: //设置过期时间为一分钟 var today = new Date(); v ...

  6. Node.js之操作文件系统(二)

    Node.js之操作文件系统(二) 1.创建与读取目录 1.1 创建目录 在fs模块中,可以使用mkdir方法创建目录,该方法的使用方法如下: fs.mkdir(path,[mode],callbca ...

  7. 关于Node.js中的路径问题

    在前端学习过程中,涉及到路径的问题非常多,相对路径,绝对路径等.有时候明明觉得没问题,但是还是会出错.或者说线下没问题,但是到了线上就出现问题,因此弄懂路径问题,非常关键.我们需要知道为什么这个地方既 ...

  8. node.js中对 redis 的安装和基本操作

    一.win下安装redis https://github.com/MicrosoftArchive/redis/releases 下载Redis-x64-3.2.100.zip,然后解压,放到自定义目 ...

  9. node.js中process进程的概念和child_process子进程模块的使用

    进程,你可以把它理解成一个正在运行的程序.node.js中每个应用程序都是进程类的实例对象. node.js中有一个 process 全局对象,通过它我们可以获取,运行该程序的用户,环境变量等信息. ...

随机推荐

  1. openjdk

    apt-get install mercurial hg clone http://hg.openjdk.java.net/jdk8u/jdk8u20/ jdk8u20 ./get_source.sh

  2. 考分鄙视(exam)

    考分鄙视(exam) 题目描述 Whence这个学期考了n次试,每一次都有一个0-20000之间的整数分数.Whence本来的状态应该是每一次考试都比前一次多一分(除第一次),但由于他很不稳定,偏差可 ...

  3. 转发:iOS开发系列--触摸事件、手势识别、摇晃事件、耳机线控

    -- iOS事件全面解析 转载来自崔江涛(KenshinCui) 链接:http://www.cnblogs.com/kenshincui/p/3950646.html 概览 iPhone的成功很大一 ...

  4. sersync 实时同步文件

    sersync 主要用于服务器同步,web镜像等功能.sersync是使用c++编写,在结合rsync同步的时候,节省了运行时耗和网络资源.因此更快.sersync配置起来很简单.另外本项目相比较其他 ...

  5. (简单) POJ 2387 Til the Cows Come Home,Dijkstra。

    Description Bessie is out in the field and wants to get back to the barn to get as much sleep as pos ...

  6. [Unity Shader]Shader分类

    Shader的分类: Shader按管线分类一般分为固定渲染管线与可编程渲染管线 (1)固定渲染管线 ——这是标准的几何&光照(Transforming&Lighting)管线,功能是 ...

  7. wex5 实战 苹果左滑删除与长按编辑

    用了多年苹果,习惯了苹果的左滑删除与长按编辑,特别是短信什么的,很多安卓界面也采用了类似方式. 我的想法突如其来,用wex5也设计一个这样的功能,可以吗? 那句广告词,没有什么不可能. 呵呵. 一   ...

  8. <转>SQL的执行顺序

    SQL 不同于与其他编程语言的最明显特征是处理代码的顺序.在大数编程语言中,代码按编码顺序被处理,但是在SQL语言中,第一个被处理的子句是FROM子句,尽管SELECT语句第一个出现,但是几乎总是最后 ...

  9. 64脚和小于64脚的STM32进行AD时注意,参照电源处理方法(转)

    源:64脚和小于64脚的STM32进行AD时注意,参照电源处理方法 请注意,ADC_IN17上没有内部基准,将其说成基准电压概念不对. 所以横线以下的理解不对,如果将其做为参考,则其电压假定按1.2V ...

  10. Nodejs之使用session

    nodejs中使用session的说明. session介绍 为什么使用session: session运行在服务器端,当客户端第一次访问服务器时,可以将客户的登陆信息保存. 当客户访问其他界面时,可 ...