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 相关文章推荐
Array对象方法参考
Oct 03 Javascript
js实现的日期操作类DateTime函数代码
Mar 16 Javascript
IE8下关于querySelectorAll()的问题
May 13 Javascript
Jquery动态添加及删除页面节点元素示例代码
Jun 16 Javascript
Jquery树插件zTree用法入门教程
Feb 17 Javascript
在easyUI开发中,出现jquery.easyui.min.js函数库问题的解决办法
Sep 11 Javascript
探讨JavaScript语句的执行过程
Jan 28 Javascript
AngularJS路由切换实现方法分析
Mar 17 Javascript
layer弹窗插件操作方法详解
May 19 Javascript
详解在express站点中使用ejs模板引擎
Sep 21 Javascript
Vue.js中使用iView日期选择器并设置开始时间结束时间校验功能
Aug 12 Javascript
layui table 表格模板按钮的实例代码
Sep 21 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 SEO优化之URL优化方法
2011/04/21 PHP
phpmyadmin提示The mbstring extension is missing的解决方法
2014/12/17 PHP
php遍历删除整个目录及文件的方法
2015/03/13 PHP
如何在HTML 中嵌入 PHP 代码
2015/05/13 PHP
php简单socket服务器客户端代码实例
2015/05/18 PHP
Yii实现复选框批量操作实例代码
2017/03/15 PHP
使用Microsoft Ajax Minifier减小JavaScript文件大小的方法
2010/04/01 Javascript
纯CSS打造的导航菜单(附jquery版)
2010/08/07 Javascript
js window.onload 加载多个函数和追加函数详解
2014/01/08 Javascript
js控制页面的全屏展示和退出全屏显示的方法
2015/03/10 Javascript
jQuery实现Div拖动+键盘控制综合效果的方法
2015/03/10 Javascript
超级给力的JavaScript的React框架入门教程
2015/07/02 Javascript
JavaScript常用字符串与数组扩展函数小结
2016/04/24 Javascript
Google Maps基础及实例解析
2016/08/06 Javascript
jQGrid动态填充select下拉框的选项值(动态填充)
2016/11/28 Javascript
jQuery Validate 数组 全部验证问题
2017/01/12 Javascript
微信小程序实战之轮播图(3)
2017/04/17 Javascript
node.js中fs文件系统目录操作与文件信息操作
2018/02/24 Javascript
vue实现鼠标移入移出事件代码实例
2019/03/27 Javascript
vue登录以及权限验证相关的实现
2019/10/25 Javascript
JavaScript实现点击切换功能
2021/01/27 Javascript
Python基础之函数用法实例详解
2014/09/10 Python
Python 搭建Web站点之Web服务器网关接口
2016/11/06 Python
详解如何使用Python编写vim插件
2017/11/28 Python
python实现批量文件重命名
2019/10/31 Python
python 检查数据中是否有缺失值,删除缺失值的方式
2019/12/02 Python
python中的垃圾回收(GC)机制
2020/09/21 Python
如何清空Session
2015/02/23 面试题
俄罗斯商务邀请函
2014/01/26 职场文书
优秀医生事迹材料
2014/02/12 职场文书
三年级学生评语
2014/04/23 职场文书
中秋晚会策划方案
2014/06/12 职场文书
刑事和解协议书范本
2014/11/19 职场文书
2015年采购部工作总结
2015/04/23 职场文书
浅谈redis缓存在项目中的使用
2021/05/20 Redis
redis调用二维码时的不断刷新排查分析
2022/04/01 Redis