无论是 Node 还是其它的 web 应用,缓存都不可或缺。这里探讨一个简单的 Node 缓存实现。

对很多人来说,简单的 Node 应用就像下面这样,后面把接口 /list 开放出去就 OK 啦。

1
2
3
4
5
6
7
8
9
10
11
const app = require('koa')();
const Router = require('koa-router');
const router = new Router();
app.use(router.routes());
router.get('/list', function* (next) {
let list = yield DB.query(...);
this.body = list;
return;
});

然而,当我们再次回过头来反观这个代码的时候,会不会有问题呢?当 QPS 上涨或遭受 DOS 攻击的时候,就会造成 DB 在执行大量的读取操作(当然,现在 DB 引擎内置会对这层作缓存,所以实际压力不会太大),但类似的场景其实很多。

怎么解决呢?引入缓存概念即可大幅减轻这个问题。当然,这里不仅仅指的是通过增加 HTTP 响应头使前端资源 304 缓存,更多的是指后端数据的缓存。

由于在很多页面应用中,大部分请求其实是读取数据渲染页面相关的,所以基于这个假设,我们会发现,很多时候其实是在反复读取同一份数据。对我们已经访问过的数据,在数据没有更新的前提下,我们还有必要再次进行反复的 IO 操作么?

事实上,开源社区有不少相关的 NPM 组件可供使用,集成到 Express 或 KOA 应用。这里,我们基于内存思考一个很简单的抽象缓存实现。具体思路如下:当我们请求到来时,我们优先看缓存里是否存在数据,如果存在则直接返回缓存中的数据;如果已经有该数据的读取操作正在进行,则可以将该请求代理到相应的回调中,当数据读取完成后,自动调用回调完成请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// cache.js
const debug = console.log;
const nextTick = process.nextTick;
const CACHE = {
check(key, req, res) {
if (this._cache[key]) {
debug('cache %s hit', key);
nextTick(() => {
res.end(this._cache[key]);
});
return;
}
if (this._queue[key]) {
debug('batch operation in queue for key %s', key);
this._queue[key].push((text) => {
res.end(text);
});
return;
}
this._queue[key] = [(text) => {
res.end(text);
}];
setTimeout(() => {
this._cache[key] = "ABCDEFG"; // a large long text
// 缓存过期
setTimeout(() => {
delete this._cache[key];
}, 30 * 1000);
const queue = this._queue;
const q = queue[key];
queue[key] = null;
q.forEach(cb => cb(this._cache[key]));
}, 1000);
},
_cache: {},
_queue: {}
}
// HOW TO USE?
// should only work for API request
const http = require('http');
http.createServer(function (req, res) {
const key = req.url;
console.log(key);
// key can be a hash of url and query data
CACHE.check(key, req, res);
return;
}).listen(3040);

当然,我们这里考虑的其实只是简单的缓存实现,一个完整的缓存组件应该不对业务代码有任何侵入,同时需要考虑到缓存的失效时机、缓存的刷新等等。有空再研究一下吧。

注,缓存最好只对 GET 请求生效。