繁体   English   中英

防止NodeJS中的并发处理

[英]Prevent concurrent processing in NodeJS

我需要NodeJS来防止相同请求的并发操作。 据我了解,如果NodeJS收到多个请求,则会发生以下情况:

REQUEST1 ---> DATABASE_READ
REQUEST2 ---> DATABASE_READ
DATABASE_READ complete ---> EXPENSIVE_OP() --> REQUEST1_END
DATABASE_READ complete ---> EXPENSIVE_OP() --> REQUEST2_END

这导致运行两个昂贵的操作。 我需要的是这样的:

REQUEST1 ---> DATABASE_READ
DATABASE_READ complete ---> DATABASE_UPDATE
DATABASE_UPDATE complete ---> REQUEST2 ---> DATABASE_READ ––> REQUEST2_END
                         ---> EXPENSIVE_OP() --> REQUEST1_END

这就是代码中的样子。 问题出在应用开始读取缓存值和完成写入缓存之间的窗口。 在此窗口中,并发请求不知道已经有一个正在运行相同itemID的请求。

app.post("/api", async function(req, res) {
    const itemID = req.body.itemID

    // See if itemID is processing
    const processing = await DATABASE_READ(itemID)
    // Due to how NodeJS works, 
    // from this point in time all requests
    // to /api?itemID="xxx" will have processing = false 
    // and will conduct expensive operations

    if (processing == true) {
        // "Cheap" part
        // Tell client to wait until itemID is processed
    } else {
        // "Expensive" part
        DATABASE_UPDATE({[itemID]: true})
        // All requests to /api at this point
        // are still going here and conducting 
        // duplicate operations.
        // Only after DATABASE_UPDATE finishes, 
        // all requests go to the "Cheap" part
        DO_EXPENSIVE_THINGS();
    }
}

编辑

我当然可以做这样的事情:

const lockedIDs = {}
app.post("/api", function(req, res) {
    const itemID = req.body.itemID
    const locked = lockedIDs[itemID] ? true : false // sync equivalent to async DATABASE_READ(itemID)
    if (locked) {
        // Tell client to wait until itemID is processed
        // No need to do expensive operations
    } else {
        lockedIDs[itemID] = true // sync equivalent to async DATABASE_UPDATE({[itemID]: true})
        // Do expensive operations
        // itemID is now "locked", so subsequent request will not go here
    }
}

在这里, lockedIDs行为就像一个内存中同步键值数据库。 如果它只是一台服务器,那就可以了。 但是,如果有多个服务器实例怎么办? 我需要有一个单独的缓存存储,例如Redis。 而且我只能异步访问Redis。 因此,不幸的是,这将行不通。

您可以创建一个本地Map对象(在内存中用于同步访问),该对象包含任何itemID作为正在处理的键。 您可以使该密钥的值成为一个承诺,该承诺可以解决以前处理该密钥的任何人的结果。 我认为这就像是守门员。 它跟踪正在处理的itemID。

该方案告诉将来等待相同itemID的请求,并且不会阻止其他请求-我认为这很重要,而不是仅对与itemID处理相关的所有请求使用全局锁定。

然后,作为处理的一部分,您首先要检查本地Map对象。 如果该密钥在其中,则当前正在处理它。 然后,您可以等待来自Map对象的promise,以查看何时完成处理并从先前的处理中获取任何结果。

如果它不在Map对象中,则说明它现在不在处理中,您可以立即将其放在Map中以将其标记为“处理中”。 如果将promise设置为值,则可以通过该对象处理得到的任何结果来解析该promise。

随之而来的任何其他请求都将仅在等待该诺言时结束,因此您将只处理一次该ID。 以该ID开头的第一个请求将对其进行处理,并且在处理该过程时出现的所有其他请求将使用相同的共享结果(从而节省了繁重的计算工作)。

我试图编写一个示例,但并没有真正理解您的伪代码试图做的足够好以提供一个代码示例。

这样的系统必须具有完美的错误处理,以便所有可能的错误路径都可以处理Map并保证正确嵌入Map

根据您相当轻巧的伪代码示例,下面是一个类似的伪代码示例,它说明了上述概念:

const itemInProcessCache = new Map();

app.get("/api", async function(req, res) {
    const itemID = req.query.itemID
    let gate = itemInProcessCache.get(itemID);
    if (gate) {
        gate.then(val => {
            // use cached result here from previous processing
        }).catch(err => {
            // decide what to do when previous processing had an error
        });
    } else {
        let p = DATABASE_UPDATE({itemID: true}).then(result => {
            // expensive processing done
            // return final value so any others waiting on the gate can just use that value
            // decide if you want to clear this item from itemInProcessCache or not
        }).catch(err => {
            // error on expensive processing

            // remove from the gate cache because we didn't get a result
            // expensive processing will have to be done by someone else
            itemInProcessCache.delete(itemID);
        });
        // mark this item as being processed
        itemInProcessCache.set(itemID, p);
    }
});

注意:这依赖于node.js的单线程。 在这里请求处理程序返回之前,没有其他请求可以开始,因此itemInProcessCache.set(itemID, p); 在对该itemID的任何其他请求开始之前被调用。


另外,我也不是很了解数据库,但是这似乎很像一个好的多用户数据库可能内置的功能,或者具有使之更容易的支持功能,因为不想有多个数据库不是一个不常见的想法请求所有尝试做相同数据库工作的人(或更糟糕的是,互相挫败对方的工作)。

好吧,让我对此付诸行动。

因此,我对此问题的困扰是您对问题的抽象如此之多,以至于很难帮助您进行优化。 目前尚不清楚您的“长期运行的流程”在做什么,它在做什么将影响如何解决处理多个并发请求的挑战。 您担心消耗资源的API在做什么?

从您的代码开始,我首先猜想您正在开展某种长期运行的工作(例如文件转换等),但是随后的一些编辑和注释使我认为这可能只是针对数据库,需要进行大量计算才能正确计算,因此您想缓存查询结果。 但是我也可以看到它是另外一回事,例如针对您正在聚合的一堆第三方API的查询或其他内容。 每个方案都有一些细微差别,可以改变最佳方案。

也就是说,我将解释“缓存”场景,您可以告诉我是否对其他解决方案之一更感兴趣。

基本上,您已经在缓存的正确位置。 如果您还没有的话,我建议您看一下cache-manager ,它在这些情况下可以简化您的样板(让我们设置缓存失效甚至具有多层缓存)。 您缺少的部分是,您基本上应该始终使用缓存中的内容进行响应,并将缓存填充到任何给定请求范围之外。 使用您的代码作为起点,类似以下内容(省去了所有try..catches和错误检查等),以简化操作:

// A GET is OK here, because no matter what we're firing back a response quickly, 
//      and semantically this is a query
app.get("/api", async function(req, res) {
    const itemID = req.query.itemID

    // In this case, I'm assuming you have a cache object that basically gets whatever
    //    is cached in your cache storage and can set new things there too.  
    let item = await cache.get(itemID)

    // Item isn't in the cache at all, so this is the very first attempt.  
    if (!item) {
        // go ahead and let the client know we'll get to it later. 202 Accepted should 
        //   be fine, but pick your own status code to let them know it's in process. 
        //   Other good options include [503 Service Unavailable with a retry-after 
        //   header][2] and [420 Enhance Your Calm][2] (non-standard, but funny)
        res.status(202).send({ id: itemID });

        // put an empty object in there so we know it's working on it. 
        await cache.set(itemID, {}); 

        // start the long-running process, which should update the cache when it's done
        await populateCache(itemID); 
        return;
    }
    // Here we have an item in the cache, but it's not done processing.  Maybe you 
    //     could just check to see if it's an empty object or not, but I'm assuming 
    //     that we've setup a boolean flag on the cached object for when it's done.
    if (!item.processed) {
        // The client should try again later like above.  Exit early. You could 
        //    alternatively send the partial item, an empty object, or a message. 
       return res.status(202).send({ id: itemID });
    } 

    // if we get here, the item is in the cache and done processing. 
    return res.send(item);
}

现在,我不知道您的全部工作是什么,但是如果是我,那么populateCache是一个非常简单的函数,它仅调用我们正在使用的服务来执行长时间运行的工作,然后将其放入缓存中。

async function populateCache(itemId) {
   const item = await service.createThisWorkOfArt(itemId);
   await cache.set(itemId, item); 
   return; 
}

让我知道是否不清楚,或者您的情况是否与我的猜测确实不同。

如评论中所述,这种方法将涵盖您所描述的方案可能遇到的大多数正常问题,但是,如果它们的执行速度比写入高速缓存存储的速度快,它仍将允许两个请求同时触发长时间运行的进程(例如Redis)。 我认为发生这种情况的几率很低,但是如果您真的对此感到担心,那么下一个更偏执的版本将是从Web API中完全删除长时间运行的过程代码。 相反,您的API只是记录有人请求该事件发生,并且如果高速缓存中没有任何内容,则像我上面所做的那样做出响应,但完全删除实际调用populateCache的块。

相反,您将运行一个单独的工作进程,该工作进程将定期(取决于您的业务情况)检查缓存中是否有未处理的作业,并启动处理这些作业的工作。 通过这种方式,即使您对同一项目有1000个并发请求,也可以确保只处理一次。 当然,不利的一面是您将检查的周期性添加到获取完全处理的数据的延迟中。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM