Node.js 防止NodeJS中的并发处理

Node.js 防止NodeJS中的并发处理,node.js,express,Node.js,Express,我需要NodeJS来防止相同请求的并发操作。据我所知,如果NodeJS接收到多个请求,则会发生以下情况: REQUEST1 ---> DATABASE_READ REQUEST2 ---> DATABASE_READ DATABASE_READ complete ---> EXPENSIVE_OP() --> REQUEST1_END DATABASE_READ complete ---> EXPENSIVE_OP() --> REQUEST2_END 这导

我需要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
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
    }
}
这就是它在代码中的样子。问题在于应用程序开始读取缓存值和完成写入之间的窗口。在此窗口期间,并发请求不知道已经有一个具有相同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();
    }
}
编辑 当然,我可以这样做:

REQUEST1 ---> DATABASE_READ
DATABASE_READ complete ---> DATABASE_UPDATE
DATABASE_UPDATE complete ---> REQUEST2 ---> DATABASE_READ ––> REQUEST2_END
                         ---> EXPENSIVE_OP() --> REQUEST1_END
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
    }
}

lockedds
在这里的行为类似于内存中的同步键值数据库。如果它只是一台服务器,那就可以了。但是如果有多个服务器实例呢?我需要一个单独的缓存存储,比如Redis。我只能异步访问Redis。不幸的是,这将不起作用。

您可以创建一个本地
映射
对象(在内存中用于同步访问),该对象包含任何itemID作为正在处理的密钥。您可以将该密钥的值设置为一个承诺,该承诺将解析之前处理过该密钥的任何人的结果。我认为这就像一个守门员。它跟踪正在处理的ItemId

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

然后,作为处理的一部分,首先检查本地映射对象。如果该密钥在那里,那么它当前正在被处理。然后,您可以等待Map对象的承诺,以查看它何时完成处理,并从之前的处理中获得任何结果

如果它不在地图对象中,那么它现在就不会被处理,您可以立即将它放在地图中,将其标记为“正在处理”。如果将一个承诺设置为值,则可以使用从对象处理中获得的任何结果来解析该承诺

出现的任何其他请求最终都将等待该承诺,因此您将只处理该ID一次。以该ID开头的第一个ID将处理该ID,并且在处理该ID时出现的所有其他请求将使用相同的共享结果(从而避免繁重计算的重复)

我试图编写一个示例,但并不真正理解您的psuedo代码试图做得足够好以提供一个代码示例

像这样的系统必须有完善的错误处理,以便所有可能的错误路径都能正确处理
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)



另外,我对数据库不太了解,但这似乎非常像一个好的多用户数据库可能内置的功能,或者具有支持功能,使这一点更容易实现,因为不希望多个请求都试图执行相同的数据库工作(或者更糟的是,破坏彼此的工作)不是一个罕见的想法,让我试一试

所以,这个问题的问题是,你把问题抽象得太多了,很难帮助你优化。目前还不清楚“长时间运行的流程”在做什么,它在做什么将影响如何解决处理多个并发请求的难题。您担心消耗资源的API是什么

从您的代码中,起初我猜测您正在启动某种长时间运行的工作(例如文件转换或其他),但随后的一些编辑和注释使我认为这可能只是一个针对数据库的复杂查询,需要大量计算才能正确执行,因此您希望缓存查询结果。但我也可以看到它是其他的东西,比如针对一堆正在聚合的第三方API的查询或其他东西。每个场景都有一些细微差别,这些差别会改变什么是最佳的

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

基本上,你已经进入了正确的缓冲区。如果您还没有,我建议您看看,这将简化这些场景的样板文件(让我们设置缓存失效,甚至有多层缓存)。您缺少的一点是,您实际上应该始终使用缓存中的任何内容进行响应,并在任何给定请求的范围之外填充缓存。使用您的代码作为起点,如下所示(为了简单起见,取消所有的try..catch和错误检查):

现在,我不知道你所有的东西都做了什么,但如果是我,上面的
populateCache
是一个非常简单的函数,它只调用我们正在使用的任何服务来完成长期运行的工作,然后将其放入缓存

async function populateCache(itemId) {
   const item = await service.createThisWorkOfArt(itemId);
   await cache.set(itemId, item); 
   return; 
}
如果这还不清楚,或者你的情况与我的猜测真的不同,请告诉我

如评论中所述,此方法将涵盖您所描述场景中可能遇到的大多数正常问题,但如果两个请求的传入速度都快于对缓存存储的写入速度(例如Redis),则仍然允许两个请求同时启动长时间运行的进程。我认为发生这种情况的几率很低,但如果你真的担心这一点,那么下一个更偏执的版本就是删除长时间运行的流程代码