很抱歉我无法想出更好的标题。
我总是遇到这个问题(当在 node.js 和 python 中编码时),但我认为我的解决方案有点脏。
我来这里是为了寻求更好的解决方案来解决这个问题。
场景如下:
您的服务器正在根据特殊的http请求执行非常非常繁重的任务(例如为URL生成浏览器屏幕截图/生成带有统计信息的游戏服务器横幅)。无论谁向您的服务器发出 HTTP 请求,都会得到相同的响应。响应将被缓存很长时间。
例如,在浏览器屏幕截图生成HTTP请求时,您的服务器应该生成一个phantomjs,捕获屏幕截图,保存并长期缓存,然后用捕获的PNG进行响应。此后的 HTTP 请求应该会命中缓存。
场景伪代码:
server.get(":urlname.png", function(req, res, next) {
var cached = cache.get(req.params_urlname);
if (cached) {
res.send(cached);
return;
}
// This will take very long time
generateScreenshot(req.params_urlname, function(pngData) {
cache.set(req.params_urlname, pngData, LONG_TIME);
res.send(cached);
});
});
问题是这样的:
假设您有一个屏幕截图生成 URL (http://yourserver.com/generate-screenshot/google.png)。截图 尚未生成或缓存。
您在一个非常受欢迎的论坛中发布了该 URL,并且同时有 1000 个 HTTP 请求对该 URL!这意味着你的服务器必须生成 1000 个 phantomjs,它们一起将同时生成 google.com 的屏幕截图,这太疯狂了!
换句话说,重函数应该只执行一次来生成缓存。
我当前问题的代码解决方案:
var pendingResponse = {};
server.get(":urlname.png", function(req, res, next) {
var cached = cache.get(req.params_urlname);
if (cached) {
res.send(cached);
return;
}
// The screenshot is currently generating for other request. Let's mark this response as pending.
if (req.params_urlname in pendingResponse) {
pendingResponse[req.params_urlname].push(res);
return;
}
// The screenshot needs to be generated now. Let's mark the future response as pending.
req.params_urlname[req.params_urlname] = [];
// This will take very long time
generateScreenshot(req.params_urlname, function(pngData) {
cache.set(req.params_urlname, pngData, LONG_TIME);
res.send(cached);
// Let's respond all the pending responses with the PNG data as well.
for (var i in pendingResponse[req.params_urlname]) {
var pRes = pendingResponse[req.params_urlname][i];
pRes.send(cached);
}
// No longer mark the future responses as pending.
delete pendingResponse[req.params_urlname];
});
});
这个解决方案有效。但是,我认为这个解决方案很脏,因为它根本不可重用。另外,我认为这可能会导致资源泄漏。有更好的解决方案/库吗?
这是一个概念验证服务器,使用 memoizee 包进行结果缓存(不仅消除了缓存正在进行的计算的必要性,而且还允许完全删除“缓存”):
var express = require('express');
var memoize = require('memoizee');
function longComputation(urlName, cb) {
console.log('called for ' + urlName);
setTimeout(function () {
console.log('done for ' + urlName);
cb();
}, 5000);
}
var memoizedLongComputation = memoize(longComputation, {async: true, maxAge: 20000});
var app = express();
app.get('/hang/:urlname', function (req, res, next) {
memoizedLongComputation(req.params.urlname, function () {
res.send('hang over');
});
});
app.listen(3000);
这里我们让结果缓存20秒。
当我启动服务器然后在 shell 中运行时
for i in `seq 1 10`; do curl http://localhost:3000/hang/url1; done
(或者只是打开几个浏览器选项卡,然后快速将它们全部导航到 http://localhost:3000/hang/url1),我在控制台中看到一个
"called for url1"
,并在 5 秒内看到一条 "done for url1"
消息,仅表示打了一个“真正的”longComputation
电话。如果我在不久后(不到 20 秒)重复它,则不会有其他消息,并且结果会立即返回,因为它们已被缓存。如果我稍后(超过 20 秒)重复该命令,则又只有一次调用。
正如您所指出的,您所描述的问题的核心是
换句话说,重函数应该只执行一次来生成缓存。
为了实现这种行为,您可以阅读已知的设计模式Request Coalescing,它可以准确地实现此行为。
添加对 Discord 开发团队制作的博客的参考,该博客在“数据服务服务数据”部分下进一步深入了解详细信息此处