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 相关文章推荐
Js 订制自己的AlertBox(信息提示框)
Jan 09 Javascript
js 静态动态成员 and 信息的封装和隐藏
May 29 Javascript
js 获取元素下面所有li的两种方法
Apr 14 Javascript
jQuery模拟原生态App上拉刷新下拉加载更多页面及原理
Aug 10 Javascript
jQuery事件绑定on()与弹窗实现代码
Apr 28 Javascript
node.js插件nodeclipse安装图文教程
Oct 19 Javascript
使用jQuery实现WordPress中的Ctrl+Enter和@评论回复
May 21 Javascript
Vue2.0系列之过滤器的使用
Mar 01 Javascript
说说node中的可读流和可写流的区别
Jun 01 Javascript
详解vuex数据传输的两种方式及this.$store undefined的解决办法
Aug 26 Javascript
JavaScript Array对象基本方法详解
Sep 03 Javascript
如何使用JavaScript实现无缝滚动自动播放轮播图效果
Aug 20 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
什么是短波收听SWL
2021/03/01 无线电
PHP中将数组转成XML格式的实现代码
2011/08/08 PHP
yii框架使用分页的方法分析
2019/07/25 PHP
javascript:以前写的xmlhttp池,代码
2008/05/18 Javascript
jquery ajax abort()的使用方法
2010/10/28 Javascript
JS实现让访问者自助选择网页文字颜色的方法
2015/02/24 Javascript
浅析Bootstrap组件之面板组件
2016/05/04 Javascript
jQuery Easyui 验证两次密码输入是否相等
2016/05/13 Javascript
Bootstrap学习笔记之js组件(4)
2016/06/12 Javascript
大白话讲解JavaScript的Promise
2017/04/06 Javascript
JS自定义滚动条效果简单实现代码
2020/10/27 Javascript
JQuery 选择器、DOM节点操作练习实例
2017/09/28 jQuery
vue下拉列表功能实例代码
2018/04/08 Javascript
微信小程序有旋转动画效果的音乐组件实例代码
2018/08/22 Javascript
在JavaScript中使用严格模式(Strict Mode)
2019/06/13 Javascript
微信小程序下拉框搜索功能的实现方法
2019/07/31 Javascript
深度剖析使用python抓取网页正文的源码
2014/06/11 Python
详解Python中find()方法的使用
2015/05/18 Python
Window10+Python3.5安装opencv的教程推荐
2018/04/02 Python
python遍历一个目录,输出所有的文件名的实例
2018/04/23 Python
python爬虫之线程池和进程池功能与用法详解
2018/08/02 Python
Python用字典构建多级菜单功能
2019/07/11 Python
关于Numpy中的行向量和列向量详解
2019/11/30 Python
pandas dataframe 中的explode函数用法详解
2020/05/18 Python
纯CSS改变webkit内核浏览器的滚动条样式
2014/04/17 HTML / CSS
英国在线滑雪板和冲浪商店:The Board Basement
2020/01/11 全球购物
声明struct x1 { . . . }; 和typedef struct { . . . }x2;有什么不同
2012/06/02 面试题
2013年大学生的自我鉴定
2013/10/24 职场文书
十八大闭幕感言
2014/01/22 职场文书
告诉你怎样写创业计划书
2014/01/27 职场文书
2014年端午节演讲稿范文
2014/05/23 职场文书
房屋分割离婚协议书范本
2014/12/01 职场文书
部队个人年终总结
2015/03/02 职场文书
修改并编译golang源码的操作步骤
2021/07/25 Golang
电脑只能进入安全模式无法正常启动的解决办法
2022/04/08 数码科技
python高温预警数据获取实例
2022/07/23 Python