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 IFrame 强制刷新代码
Jul 23 Javascript
实现png图片和png背景透明(支持多浏览器)的方法
Sep 08 Javascript
dess中一个简单的多路委托的实现
Jul 20 Javascript
Ext JS添加子组件的误区探讨
Jun 28 Javascript
简单易用的倒计时js代码
Aug 04 Javascript
javascript学习指南之回调问题
Apr 23 Javascript
JavaScript设计模式开发中组合模式的使用教程
May 18 Javascript
js document.getElementsByClassName的使用介绍与自定义函数
Nov 25 Javascript
ES6 Promise对象概念与用法分析
Apr 01 Javascript
JavaScript纯色二维码变成彩色二维码
Jul 23 Javascript
玩转Koa之koa-router原理解析
Dec 29 Javascript
vue组件三大核心概念图文详解
May 30 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循环检测目录是否存在并创建(循环创建目录)
2011/01/06 PHP
PHP中PDO连接数据库中各种DNS设置方法小结
2016/05/13 PHP
PHP执行系统命令函数实例讲解
2021/03/03 PHP
jQuery中:last-child选择器用法实例
2014/12/31 Javascript
Java Mybatis框架入门基础教程
2015/09/21 Javascript
javascript中return,return true,return false三者的用法及区别
2015/11/17 Javascript
js中scrollTop()方法和scroll()方法用法示例
2016/10/03 Javascript
微信小程序 less文件编译成wxss文件实现办法
2016/12/05 Javascript
微信小程序中实现一对多发消息详解及实例代码
2017/02/14 Javascript
完美解决spring websocket自动断开连接再创建引发的问题
2017/03/02 Javascript
p5.js入门教程之鼠标交互的示例
2018/03/16 Javascript
详解webpack4之splitchunksPlugin代码包分拆
2018/12/04 Javascript
javascript防抖函数debounce详解
2019/06/11 Javascript
弱类型语言javascript中 a,b 的运算实例小结
2019/08/07 Javascript
详解微信小程序图片地扯转base64解决方案
2019/08/18 Javascript
使用Bootstrap做一个朝代历史表
2019/12/10 Javascript
在vue中实现禁止屏幕滚动,禁止屏幕滑动
2020/07/22 Javascript
[03:20]次级联赛厮杀超职业 现超级兵对拆世纪大战
2014/10/30 DOTA
[48:12]Secret vs Optic Supermajor 胜者组 BO3 第三场 6.4
2018/06/05 DOTA
DataFrame 将某列数据转为数组的方法
2018/04/13 Python
浅谈Django中的数据库模型类-models.py(一对一的关系)
2018/05/30 Python
Python绘制并保存指定大小图像的方法
2019/01/10 Python
解决python xx.py文件点击完之后一闪而过的问题
2019/06/24 Python
在django中,关于session的通用设置方法
2019/08/06 Python
简单介绍django提供的加密算法
2019/12/18 Python
PyTorch中topk函数的用法详解
2020/01/02 Python
django有外键关系的两张表如何相互查找
2020/02/10 Python
python GUI库图形界面开发之PyQt5信号与槽机制、自定义信号基础介绍
2020/02/25 Python
自定义Django默认的sitemap站点地图样式
2020/03/04 Python
celery在python爬虫中定时操作实例讲解
2020/11/27 Python
Original Penguin英国官方网站:美国著名休闲时装品牌
2016/10/30 全球购物
澳大利亚第一旅行车和房车配件店:Caravan RV Camping
2020/12/26 全球购物
serialVersionUID具有什么样的特征
2014/02/20 面试题
初级会计求职信范文
2014/02/15 职场文书
结婚纪念日感言
2015/08/01 职场文书
解析Java中的static关键字
2021/06/14 Java/Android