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中的函数
Jan 22 Javascript
jquery trim() 功能源代码
Feb 14 Javascript
js 第二代身份证号码的验证机制代码
May 12 Javascript
jquery实现网站超链接和图片提示效果
Mar 21 Javascript
解决extjs grid 不随窗口大小自适应的改变问题
Jan 26 Javascript
JS图片自动轮换效果实现思路附截图
Apr 30 Javascript
浅谈Node.js中的定时器
Jun 18 Javascript
微信小程序 教程之wxapp视图容器 scroll-view
Oct 19 Javascript
JavaScript实现图片切换效果
Aug 12 Javascript
layer父页获取弹出层输入框里面的值方法
Sep 02 Javascript
JavaScript实时更新当前的时间的示例代码
Jul 15 Javascript
解决VUE项目localhost端口服务器拒绝连接,只能用127.0.0.1的问题
Aug 14 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读取txt文件组成SQL并插入数据库的代码(原创自Zjmainstay)
2012/07/31 PHP
深入解析PHP内存管理之谁动了我的内存
2013/06/20 PHP
Codeigniter出现错误提示Error with CACHE directory的解决方案
2014/06/12 PHP
PHP用continue跳过本次循环中剩余代码的注意点
2017/06/27 PHP
PHP生成腾讯云COS接口需要的请求签名
2018/05/20 PHP
关于js new Date() 出现NaN 的分析
2012/10/23 Javascript
jquery实现页面图片等比例放大缩小功能
2014/02/12 Javascript
nodejs接入阿里大鱼短信验证码的方法
2017/07/10 NodeJs
JavaScript实现数值自动增加动画
2017/12/28 Javascript
jQuery实现表单动态添加数据并提交的方法
2018/07/19 jQuery
vue+element实现表单校验功能
2019/05/20 Javascript
最简单的vue消息提示全局组件的方法
2019/06/16 Javascript
JavaScript生成随机验证码代码实例
2019/09/28 Javascript
angular异步验证防抖踩坑实录
2019/12/01 Javascript
微信小程序 wx:for 与 wx:for-items 与 wx:key的正确用法
2020/05/19 Javascript
Vue 如何使用props、emit实现自定义双向绑定的实现
2020/06/05 Javascript
原生JS实现相邻月份日历
2020/10/13 Javascript
python求众数问题实例
2014/09/26 Python
Python实现在线程里运行scrapy的方法
2015/04/07 Python
详解Python中的序列化与反序列化的使用
2015/06/30 Python
用Anaconda安装本地python包的方法及路径问题(图文)
2019/07/16 Python
浅谈python已知元素,获取元素索引(numpy,pandas)
2019/11/26 Python
Pytorch之contiguous的用法
2019/12/31 Python
python中常见错误及解决方法
2020/06/21 Python
python 用struct模块解决黏包问题
2020/11/07 Python
HTML5实现页面切换激活的PageVisibility API使用初探
2016/05/13 HTML / CSS
Farfetch香港官网:汇集全球时尚奢侈品购物平台
2017/11/26 全球购物
Gap英国官网:Gap UK
2018/07/18 全球购物
股东协议书范本
2014/04/14 职场文书
法定代表人免职证明
2015/06/24 职场文书
任命书格式范文
2015/09/22 职场文书
小学班主任研修日志
2015/11/13 职场文书
学校2016年圣诞节活动总结
2016/03/31 职场文书
spring注解 @PropertySource配置数据源全流程
2022/03/25 Java/Android
Golang MatrixOne使用介绍和汇编语法
2022/04/19 Golang
Java存储没有重复元素的数组
2022/04/29 Java/Android