Node.js + Redis Sorted Set实现任务队列


Posted in Javascript onSeptember 19, 2016

需求:功能 A 需要调用第三方 API 获取数据,而第三方 API 自身是异步处理方式,在调用后会返回数据与状态 { data: "查询结果", "status": "正在异步处理中" } ,这样就需要间隔一段时间后再去调用第三方 API 获取数据。为了用户在使用功能 A 时不会因为第三方 API 正在异步处理中而必须等待,将用户请求加入任务队列中,返回部分数据并关闭请求。然后定时从任务队列里中取出任务调用第三方 API,若返回状态为”异步处理中“,将该任务再次加入任务队列,若返回状态为”已处理完毕“,将返回数据入库。

根据以上问题,想到使用 Node.js + Redis sorted set 来实现任务队列。Node.js 实现自身应用 API 用来接受用户请求,合并数据库已存数据与 API 返回的部分数据返回给用户,并将任务加入到任务队列中。利用 Node.js child process 与 cron 定时从任务队列中取出任务执行。

在设计任务队列的过程中需要考虑到的几个问题

  • 并行执行多个任务
  • 任务唯一性
  • 任务成功或失败后的处理

针对以上问题的解决方案

  • 并行执行多个任务利用 Promise.all 来实现
  • 任务唯一性利用 Redis sorted set 来实现。使用时间戳作为分值可以实现将 sorted set 作为 list 来使用,在加入任务时判断任务是否已经存在,在取出任务执行时将该任务分值设置为 0,每次取出分值大于 0 的任务来执行,可以避免重复执行任务。
  • 执行任务成功后删除任务,执行任务失败后将任务分值更新为当前时间时间戳,这样就可以将失败的任务重新加入任务队列尾部

示例代码

// remote_api.js 模拟第三方 API
'use strict';

const app = require('express')();

app.get('/', (req, res) => {
  setTimeout(() => {
    let arr = [200, 300]; // 200 代表成功,300 代表失败需要重新请求
    res.status(200).send({ 'status': arr[parseInt(Math.random() * 2)] });
  }, 3000);
});

app.listen('9001', () => {
  console.log('API 服务监听端口:9001');
});
// producer.js 自身应用 API,用来接受用户请求并将任务加入任务队列
'use strict';

const app = require('express')();
const redisClient = require('redis').createClient();

const QUEUE_NAME = 'queue:example';

function addTaskToQueue(taskName, callback) {
  // 先判断任务是否已经存在,存在:跳过,不存在:加入任务队列
  redisClient.zscore(QUEUE_NAME, taskName, (error, task) => {
    if (error) {
      console.log(error);
    } else {
      if (task) {
        console.log('任务已存在,不新增相同任务');
        callback(null, task);
      } else {
        redisClient.zadd(QUEUE_NAME, new Date().getTime(), taskName, (error, result) => {
          if (error) {
            callback(error);
          } else {
            callback(null, result);
          }
        });
      }
    }
  });
}

app.get('/', (req, res) => {
  let taskName = req.query['task-name'];
  addTaskToQueue(taskName, (error, result) => {
    if (error) {
      console.log(error);
    } else {
      res.status(200).send('正在查询中......');
    }
  });
});

app.listen(9002, () => {
  console.log('生产者服务监听端口:9002');
});
// consumer.js 定时获取任务并执行
'use strict';

const redisClient = require('redis').createClient();
const request = require('request');
const schedule = require('node-schedule');

const QUEUE_NAME = 'queue:expmple';
const PARALLEL_TASK_NUMBER = 2; // 并行执行任务数量

function getTasksFromQueue(callback) {
  // 获取多个任务
  redisClient.zrangebyscore([QUEUE_NAME, 1, new Date().getTime(), 'LIMIT', 0, PARALLEL_TASK_NUMBER], (error, tasks) => {
    if (error) {
      callback(error);
    } else {
      // 将任务分值设置为 0,表示正在处理
      if (tasks.length > 0) {
        let tmp = [];
        tasks.forEach((task) => {
          tmp.push(0);
          tmp.push(task);
        });
        redisClient.zadd([QUEUE_NAME].concat(tmp), (error, result) => {
          if (error) {
            callback(error);
          } else {
            callback(null, tasks)
          }
        });
      }
    }
  });
}

function addFailedTaskToQueue(taskName, callback) {
  redisClient.zadd(QUEUE_NAME, new Date().getTime(), taskName, (error, result) => {
    if (error) {
      callback(error);
    } else {
      callback(null, result);
    }
  });
}

function removeSucceedTaskFromQueue(taskName, callback) {
  redisClient.zrem(QUEUE_NAME, taskName, (error, result) => {
    if (error) {
      callback(error);
    } else {
      callback(null, result);
    }
  })
}

function execTask(taskName) {
  return new Promise((resolve, reject) => {
    let requestOptions = {
      'url': 'http://127.0.0.1:9001',
      'method': 'GET',
      'timeout': 5000
    };
    request(requestOptions, (error, response, body) => {
      if (error) {
        resolve('failed');
        console.log(error);
        addFailedTaskToQueue(taskName, (error) => {
          if (error) {
            console.log(error);
          } else {

          }
        });
      } else {
        try {
          body = typeof body !== 'object' ? JSON.parse(body) : body;
        } catch (error) {
          resolve('failed');
          console.log(error);
          addFailedTaskToQueue(taskName, (error, result) => {
            if (error) {
              console.log(error);
            } else {

            }
          });
          return;
        }
        if (body.status !== 200) {
          resolve('failed');
          addFailedTaskToQueue(taskName, (error, result) => {
            if (error) {
              console.log(error);
            } else {

            }
          });
        } else {
          resolve('succeed');
          removeSucceedTaskFromQueue(taskName, (error, result) => {
            if (error) {
              console.log(error);
            } else {

            }
          });
        }
      }
    });
  });
}

// 定时,每隔 5 秒获取新的任务来执行
let job = schedule.scheduleJob('*/5 * * * * *', () => {
  console.log('获取新任务');
  getTasksFromQueue((error, tasks) => {
    if (error) {
      console.log(error);
    } else {
      if (tasks.length > 0) {
        console.log(tasks);

        Promise.all(tasks.map(execTask))
        .then((results) => {
          console.log(results);
        })
        .catch((error) => {
          console.log(error);
        });
        
      }
    }
  });
});
Javascript 相关文章推荐
jQuery代码优化 选择符篇
Nov 01 Javascript
利用JS实现浏览器的title闪烁
Jul 08 Javascript
jQuery打字效果实现方法(附demo源码下载)
Dec 18 Javascript
JavaScript控制浏览器全屏及各种浏览器全屏模式的方法、属性和事件
Dec 20 Javascript
基于javascript编写简单日历
May 02 Javascript
JS中with的替代方法与String中的正则方法详解
Dec 23 Javascript
在一个页面重复使用一个js函数的方法详解
Dec 26 Javascript
JavaScript实现元素滚动条到达一定位置循环追加内容
Dec 28 Javascript
Node.js Koa2使用JWT进行鉴权的方法示例
Aug 17 Javascript
基于vue实现web端超大数据量表格的卡顿解决
Apr 02 Javascript
微信小程序传值以及获取值方法的详解
Apr 29 Javascript
vue-router 2.0 跳转之router.push()用法说明
Aug 12 Javascript
JavaScript学习笔记整理_用于模式匹配的String方法
Sep 19 #Javascript
JavaScript学习笔记整理_简单实现枚举类型,扑克牌应用
Sep 19 #Javascript
JavaScript学习笔记整理_关于表达式和语句
Sep 19 #Javascript
javascript学习笔记_浅谈基础语法,类型,变量
Sep 19 #Javascript
js中用cssText设置css样式的简单方法
Sep 19 #Javascript
Query常用DIV操作获取和设置长度宽度的实现方法
Sep 19 #Javascript
基于jQuery实现中英文切换导航条效果
Sep 18 #Javascript
You might like
php curl 登录163邮箱并抓取邮箱好友列表的代码(经测试)
2011/04/07 PHP
php判断上传的Excel文件中是否有图片及PHPExcel库认识
2013/01/11 PHP
JavaScript 脚本将当地时间转换成其它时区
2009/03/19 Javascript
javascript克隆对象深度介绍
2012/11/20 Javascript
Extjs4 类的定义和扩展实例
2013/06/28 Javascript
Jquery中CSS选择器用法分析
2015/02/10 Javascript
JS控制按钮10秒钟后可用的方法
2015/12/22 Javascript
JS HTML5实现拖拽移动列表效果
2020/08/27 Javascript
JS实现列表的响应式排版(推荐)
2016/09/01 Javascript
浅谈redux以及react-redux简单实现
2018/08/28 Javascript
为jquery的ajax请求添加超时timeout时间的操作方法
2018/09/04 jQuery
js中的this的指向问题详解
2019/08/29 Javascript
ES6学习笔记之let与const用法实例分析
2020/01/22 Javascript
Python编程中的for循环语句学习教程
2015/10/14 Python
Win10下Python环境搭建与配置教程
2016/11/18 Python
python的scikit-learn将特征转成one-hot特征的方法
2018/07/10 Python
python用BeautifulSoup库简单爬虫实例分析
2018/07/30 Python
python3模拟实现xshell远程执行liunx命令的方法
2019/07/12 Python
Python绘制三角函数图(sin\cos\tan)并标注特定范围的例子
2019/12/04 Python
关于python pycharm中输出的内容不全的解决办法
2020/01/10 Python
Pytorch中的VGG实现修改最后一层FC
2020/01/15 Python
wxPython修改文本框颜色过程解析
2020/02/14 Python
python实现简单井字棋小游戏
2020/03/05 Python
记一次Django响应超慢的解决过程
2020/09/17 Python
HTML5 Canvas玩转酷炫大波浪进度图效果实例(附demo)
2016/12/14 HTML / CSS
AmazeUI在模态框中嵌入表单形成模态输入框
2020/08/20 HTML / CSS
大学校园生活自我鉴定
2014/01/13 职场文书
学生实习介绍信
2014/01/15 职场文书
物价局领导班子四风问题整改措施
2014/10/26 职场文书
法务专员岗位职责
2015/02/14 职场文书
食品仓管员岗位职责
2015/04/01 职场文书
2015年员工试用期工作总结
2015/05/28 职场文书
退伍军人感言
2015/08/01 职场文书
班主任远程培训研修日志
2015/11/13 职场文书
css position fixed 左右双定位的实现代码
2021/04/29 HTML / CSS
Golang Gob编码(gob包的使用详解)
2021/05/07 Golang