【问题标题】:Execute a forEach like a waterfall in async像瀑布一样异步执行 forEach
【发布时间】:2023-03-16 01:56:01
【问题描述】:

我正在尝试使用 Google API 通过 Node.js 脚本从地址列表中检索经度和纬度。调用本身工作正常,但因为我有大约 100 个地址要提交。我在数组上使用了async.forEach,但调用速度太快,并且出现错误“您已超出此 API 的速率限制。”

我发现呼叫次数限制为每 24 小时 2500 次,每秒最多 10 次。虽然我可以接受每天 2500 次的通话,但由于速率限制,我的通话速度太快了。

我现在必须编写一个函数来延迟调用,使其不会达到限制。这是我的代码示例:

async.forEach(final_json, function(item, callback) {
    var path = '/maps/api/geocode/json?address='+encodeURIComponent(item.main_address)+'&sensor=false';
    console.log(path);
    var options = {
      host: 'maps.googleapis.com',
      port: 80,
      path: path,
      method: 'GET',
      headers: {
        'Content-Type': 'application/json'
      }
    }
    // a function I have who makes the http GET
    rest.getJSON(options, function(statusCode, res) {
      console.log(res);
      callback();
    });
}, function() {
  // do something once all the calls have been made
});

您将如何继续实现这一目标?我尝试将我的rest.getJSON 放在 100 毫秒内 setTimeoutforEach 迭代所有行的速度非常快,以至于它几乎同时启动了所有setTimeout,因此它不会改变任何东西......

async.waterfall 看起来可以解决问题,但问题是我不知道我将拥有多少行,所以我无法对所有函数调用进行硬编码。老实说,这会让我的代码非常难看

【问题讨论】:

  • 你试过递归吗?使用回调设置超时,100ms 后递归调用 fetch 函数。
  • 这似乎是一个足够通用的问题,因此通用解决方案是合适的。速率限制是一种常见的构造,内联 setTimeout hack 可能不是正确的方法。瀑布可能比必要的更慢或更快,因为它所做的只是等待最后一个调用返回。如果通话时间不到 1/10 秒,你还是太快了。
  • 为什么不使用像parallel 这样简单的东西,有 10 个队列,当它们完成时,确保在开始下一批之前总共经过了 10 秒?
  • 这是一个简洁的版本。我一直在思考和编写循环,等待 125 毫秒 + 一个用于尚未返回结果的存储桶。循环会将项目放入存储桶中,http 回调将删除它们。不要让桶变得大于10,并且不要以超过8/秒的速度推入桶。当它完成并且桶是空的(等待它,等待它......)执行你的“全部完成功能”

标签: javascript node.js asynchronous google-api


【解决方案1】:

这个想法是,您可以创建一个 rateLimited 函数,该函数的行为与 throttleddebounced 函数非常相似,但任何不立即执行的调用都会排队并按速率限制时间段顺序运行过期了。

基本上,它创建并行的 1 秒间隔,通过计时器重新安排自行管理,但最多允许 perSecondLimit 间隔。

function rateLimit(perSecondLimit, fn) {
    var callsInLastSecond = 0;
    var queue = [];
    return function limited() {
        if(callsInLastSecond >= perSecondLimit) {
            queue.push([this,arguments]);
            return;
        }

        callsInLastSecond++;
        setTimeout(function() {
            callsInLastSecond--;
            var parms;
            if(parms = queue.shift()) {
                limited.apply(parms[0], parms[1]);
            }
        }, 1010);

        fn.apply(this, arguments);
    };
}

用法:

function thisFunctionWillBeCalledTooFast() {}
var limitedVersion = rateLimit(10, thisFunctionWillBeCalledTooFast);

// 10 calls will be launched immediately, then as the timer expires
// for each of those calls a new call will be launched in it's place.
for(var i = 0; i < 100; i++) {
    limitedVersion();
}

【讨论】:

  • 这是一个漂亮的通用函数!我照原样接受它,它就像一个魅力!好吧,这不是第一次,但我不是 10 次,而是将其限制为每秒 5 次,现在它可以工作了(10 次我仍然会遇到一些错误,9 次更少,但 5 次则完美无缺)。谢谢!
【解决方案2】:

这是我将如何破解它(注意:arr 是您的位置数组):

function populate(arr, callback, pos) {
    if(typeof pos == "undefined")
        pos=0;
    var path = '/maps/api/geocode/json?address='+encodeURIComponent(arr[pos].main_address)+'&sensor=false';
    console.log(path);
    var options = {
      host: 'maps.googleapis.com',
      port: 80,
      path: path,
      method: 'GET',
      headers: {
        'Content-Type': 'application/json'
      }
    }
    // a function I have who makes the http GET
    rest.getJSON(options, function(statusCode, res) {
      console.log(res);
    });
    pos++;

    if(pos<arr.length)
        setTimeout(function(){
            populate(arr,callback,pos);
        },110); //a little wiggle room since setTimeout isn't exact
    else
        callback();
}

您可以添加一个速率限制功能,但是恕我直言,它引入了不必要的复杂性。你真正想做的就是每隔十分之一秒左右调用一次函数,直到你完成你的列表,所以就这样做吧。

它当然不像替代方案那样可扩展,但我喜欢简单。

【讨论】:

  • 我试图弄清楚如何使用递归函数来做到这一点,但从未提出过功能性的东西。我没有意识到我必须完全删除循环并自己模拟它!我仍然使用上面的通用函数,但你的例子是如何用递归函数解决问题的一个很好的例子!
猜你喜欢
  • 2018-11-13
  • 2020-09-06
  • 2015-07-18
  • 2015-04-28
  • 1970-01-01
  • 1970-01-01
  • 2019-01-02
  • 2017-02-27
  • 1970-01-01
相关资源
最近更新 更多