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 相关文章推荐
JavaScript OOP面向对象介绍
Dec 02 Javascript
基于jquery完美拖拽,可返回拖动轨迹
Mar 29 Javascript
jQuery中$.ajax()和$.getJson()同步处理详解
Aug 12 Javascript
针对初学者的jQuery入门指南
Aug 15 Javascript
JQuery标签页效果的两个实例讲解(4)
Sep 17 Javascript
JavaScript中的this机制
Jan 30 Javascript
基于daterangepicker日历插件使用参数注意的问题
Aug 10 Javascript
vue引入ueditor及node后台配置详解
Jan 03 Javascript
vue实现div拖拽互换位置
Jul 29 Javascript
bootstrap Table实现合并相同行
Jul 19 Javascript
基于javascript实现移动端轮播图效果
Dec 21 Javascript
JS函数式编程实现XDM一
Jun 16 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静态文件返回304技巧分享
2015/01/06 PHP
PHP实现按之字形顺序打印二叉树的方法
2018/01/16 PHP
实例说明js脚本语言和php脚本语言的区别
2019/04/04 PHP
PHP For循环字母A-Z当超过26个字母时输出AA,AB,AC
2020/02/16 PHP
给事件响应函数传参数的四种方式小结
2013/12/05 Javascript
js获取 type=radio 值的方法
2014/05/09 Javascript
DOM基础教程之事件类型
2015/01/20 Javascript
动态的9*9乘法表效果的实现代码
2016/05/16 Javascript
基于JavaScript实现鼠标箭头移动图片跟着移动
2016/08/30 Javascript
完美的js div拖拽实例代码
2016/09/24 Javascript
基于vue2的table分页组件实现方法
2017/03/20 Javascript
vue-router中scrollBehavior的巧妙用法
2018/07/09 Javascript
vue-cli脚手架的安装教程图解
2018/09/02 Javascript
解决Vue2.0 watch对象属性变化监听不到的问题
2018/09/11 Javascript
JS监听滚动和id自动定位滚动
2018/12/18 Javascript
js打开word文档预览操作示例【不是下载】
2019/05/23 Javascript
更优雅的微信小程序骨架屏实现详解
2019/08/07 Javascript
利用JS如何获取form表单数据
2019/12/19 Javascript
jQuery模仿ToDoList实现简单的待办事项列表
2019/12/30 jQuery
使用React-Router实现前端路由鉴权的示例代码
2020/07/26 Javascript
python复制文件代码实现
2013/12/23 Python
python实现在目录中查找指定文件的方法
2014/11/11 Python
对Python3使运行暂停的方法详解
2019/02/18 Python
Python中按键来获取指定的值
2019/03/02 Python
python plotly画柱状图代码实例
2019/12/13 Python
PyCharm无法识别PyQt5的2种解决方法,ModuleNotFoundError: No module named 'pyqt5'
2020/02/17 Python
python实现IOU计算案例
2020/04/12 Python
为中国消费者甄选天下优品:网易严选
2016/08/11 全球购物
荷兰多品牌网上鞋店:Stoute Schoenen
2017/08/24 全球购物
eBay加拿大站:eBay.ca
2019/06/20 全球购物
机械系大学毕业生推荐信
2013/11/27 职场文书
学生违纪检讨书200字
2014/10/21 职场文书
店铺转让协议书
2015/01/29 职场文书
无房证明样本
2015/06/17 职场文书
关于五一放假的通知
2015/08/18 职场文书
SQL实现LeetCode(177.第N高薪水)
2021/08/04 MySQL