[英]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.