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玩一玩WSH吧
Feb 23 Javascript
ASP.NET jQuery 实例4(复制TextBox的文本到本地剪贴板上)
Jan 13 Javascript
js操作iframe兼容各种主流浏览器示例代码
Jul 22 Javascript
JS常见问题整理(持续更新)
Aug 06 Javascript
谈谈JavaScript自定义回调函数
Oct 18 Javascript
Perl Substr()函数及函数的应用
Dec 16 Javascript
vue中简单弹框dialog的实现方法
Feb 26 Javascript
vue中各选项及钩子函数执行顺序详解
Aug 25 Javascript
创建Vue项目以及引入Iview的方法示例
Dec 03 Javascript
微信小程序Flex布局用法深入浅出分析
Apr 25 Javascript
解决LayUI数据表格复选框不居中显示的问题
Sep 25 Javascript
vue修饰符.capture和.self的区别
Apr 22 Vue.js
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类
2006/11/25 PHP
PHP 判断常量,变量和函数是否存在
2009/04/26 PHP
完美实现GIF动画缩略图的php代码
2011/01/02 PHP
PHP统计数值数组中出现频率最多的10个数字的方法
2015/04/20 PHP
php正则匹配文章中的远程图片地址并下载图片至本地
2015/09/29 PHP
如何实现动态删除javascript函数
2007/05/27 Javascript
网页打开自动最大化的js代码
2012/08/22 Javascript
jQuery图片播放8款精美插件分享
2013/02/17 Javascript
jQuery之尺寸调整组件的深入解析
2013/06/19 Javascript
js编码、解码函数介绍及其使用示例
2013/09/05 Javascript
一个简单的JS时间控件示例代码(JS时分秒时间控件)
2013/11/22 Javascript
动态加载jquery库的方法
2014/02/12 Javascript
js格式化时间小结
2014/11/03 Javascript
javascript实现带节日和农历的日历特效
2015/02/01 Javascript
基于jQuery实现自动轮播旋转木马特效
2015/11/02 Javascript
js判断主流浏览器类型和版本号的简单实现代码
2016/05/26 Javascript
jQuery实现二维码扫描功能
2017/01/09 Javascript
React Native实现地址挑选器功能
2017/10/24 Javascript
基于cropper.js封装vue实现在线图片裁剪组件功能
2018/03/01 Javascript
详解webpack 打包文件体积过大解决方案(code splitting)
2018/04/10 Javascript
基于ionic实现下拉刷新功能
2018/05/10 Javascript
seajs和requirejs模块化简单案例分析
2019/08/26 Javascript
vue.js的状态管理vuex中store的使用详解
2019/11/08 Javascript
Python编写检测数据库SA用户的方法
2014/07/11 Python
Python实现动态图解析、合成与倒放
2018/01/18 Python
Redis使用watch完成秒杀抢购功能的代码
2018/05/07 Python
Python中修改字符串的四种方法
2018/11/02 Python
Django模板标签中url使用详解(url跳转到指定页面)
2020/03/19 Python
Django Admin后台添加数据库视图过程解析
2020/04/01 Python
手机配件第一品牌:ZAGG
2017/05/28 全球购物
国际奢侈品品牌童装购物网站:Designer Childrenswear
2019/05/08 全球购物
学生档案自我鉴定
2013/10/07 职场文书
超市营业员岗位职责
2013/12/20 职场文书
监察建议书
2015/02/04 职场文书
《天净沙·秋思》教学反思三篇
2019/11/02 职场文书
Android开发之WECHAT微信小程序路由跳转的两种形式
2022/04/12 Java/Android