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 相关文章推荐
COM中获取JavaScript数组大小的代码
Nov 22 Javascript
jqeury eval将字符串转换json的方法
Jan 20 Javascript
js获取窗口相对于屏幕左边和上边的位置坐标
May 15 Javascript
js实现有时间限制消失的图片方法
Feb 27 Javascript
使用AngularJS 应用访问 Android 手机的图片库
Mar 24 Javascript
javascript实现根据3原色制作颜色选择器的方法
Jul 17 Javascript
javascript实现五星评价代码(源码下载)
Aug 11 Javascript
AngularJS基础 ng-paste 指令简单示例
Aug 02 Javascript
纯JavaScript手写图片轮播代码
Oct 20 Javascript
[原创]JavaScript语法高亮插件highlight.js用法详解【附highlight.js本站下载】
Nov 01 Javascript
js 动态校验开始结束时间的实现代码
May 25 Javascript
详解JavaScript数据类型和判断方法
Sep 04 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执行速率优化技巧小结
2008/03/15 PHP
一个PHP实现的轻量级简单爬虫
2015/07/08 PHP
PHP利用超级全局变量$_GET来接收表单数据的实例
2016/11/05 PHP
thinkPHP框架实现的无限回复评论功能示例
2018/06/09 PHP
TP5(thinkPHP5)框架基于ajax与后台数据交互操作简单示例
2018/09/03 PHP
JS俄罗斯方块,包含完整的设计理念
2010/12/11 Javascript
Jquery时间验证和转换工具小例子
2013/07/01 Javascript
JS判断两个时间大小的示例代码
2014/01/28 Javascript
node.js解决获取图片真实文件类型的问题
2014/12/20 Javascript
jQuery实现将div中滚动条滚动到指定位置的方法
2016/08/10 Javascript
javascript设计模式之module(模块)模式
2016/08/19 Javascript
JavaScript toUpperCase()方法使用详解
2016/08/26 Javascript
javascript html5轻松实现拖动功能
2017/03/01 Javascript
node.js实现的装饰者模式示例
2017/09/06 Javascript
判断jQuery是否加载完成,没完成继续判断的解决方法
2017/12/06 jQuery
微信小程序实现左滑修改、删除功能
2020/10/19 Javascript
vue缓存的keepalive页面刷新数据的方法
2019/04/23 Javascript
Vue中rem与postcss-pxtorem的应用详解
2019/11/20 Javascript
小程序按钮避免多次调用接口和点击方案实现(不用showLoading)
2020/04/15 Javascript
[01:02:03]2014 DOTA2华西杯精英邀请赛 5 24 NewBee VS VG
2014/05/26 DOTA
Python实现将HTML转换成doc格式文件的方法示例
2017/11/20 Python
关于Python中的向量相加和numpy中的向量相加效率对比
2019/08/26 Python
pytorch 实现查看网络中的参数
2020/01/06 Python
如何基于Python和Flask编写Prometheus监控
2020/11/25 Python
HTML5实现的图片无限加载的瀑布流效果另带边框圆角阴影
2014/03/07 HTML / CSS
时装界的“朋克之母”:Vivienne Westwood
2017/07/06 全球购物
美国购买汽车零件网站:Buy Auto Parts
2018/04/02 全球购物
人事专员岗位职责
2013/11/20 职场文书
教师党员公开承诺书
2014/03/25 职场文书
课外活动总结范文
2014/07/09 职场文书
农村门前三包责任书
2014/07/25 职场文书
党的群众路线教育实践活动心得体会(企业)
2014/11/03 职场文书
2014年个人教学工作总结
2014/12/09 职场文书
教师年度考核个人总结
2015/02/12 职场文书
谢师宴家长答谢词
2015/09/30 职场文书
2016思想纪律作风整顿心得体会
2016/01/23 职场文书