当一个网站的访问量越来越大后,增加缓存是提升性能的一个既简单又有效的方式。增加缓存的方式大同小异,无非是将缓存层放到业务逻辑层之前,当请求到达时,首先经过缓存层,如果命中缓存则直接返回,如果没有命中则传递到业务逻辑层。缓存的更新机制也大同小异,通常有两种方式:一是设置一个定时器定时更新缓存,二是当业务逻辑层执行结束后更新缓存。第一种方式简单但却浪费资源,第二种方式代码耦合严重也不优雅。而基于 koa 的中间件特性,我们可以写出即简单又优雅也不耦合的缓存中间件。以 koa-router-cache 为例,我们看看它是如何实现的:

var app = require('koa')();
var cache = require('koa-router-cache');
var MemoryCache = cache.MemoryCache;
 
var count = 0;
 
app.use(cache(app, {
  'GET /': {
    key: 'cache:index',
    expire: 5 * 1000,
    get: MemoryCache.get,
    set: MemoryCache.set,
    passthrough: MemoryCache.passthrough,
    evtName: 'clearIndexCache',
    destroy: MemoryCache.destroy
  }
}));
 
app.use(function* () {
  if (this.path === '/') {
    this.body = count++;
    if (count === 5) {
      count = 0;
      this.app.emit('clearIndexCache');
    }  
  }
});
 
app.listen(3000, function () {
  console.log('listening on 3000.');
});

建议读者亲自运行以上代码调试一下,以上代码的意思是:缓存主页并 5 秒更新一次,第一次请求到来时缓存中间件中没有内容,所以传递到下一个中间件,此时将 this.body 赋值为 0 , count 变为 1,当中间件的 downstream 执行完毕后执行 upstream,此时将 this.body = 0 缓存到内存中,并设置 5 秒的生存期,所以,后续 5 秒之内的所有请求都会因命中缓存而返回 0 。5 秒过后,因为缓存中的内容已经过期被删除,所以下个请求到来时没有命中缓存,此时传递到下一个中间件将 this.body 赋值为 1 , count 变为 2,并更新缓存。直到当 count 变为 5 时,count 被重置为 0,并通过事件触发该路径对应的监听器立即删除缓存中的老数据,这样保证了缓存中的数据都是最新的。用户还可通过传入 get 和 set 参数手动管理缓存的读取和写入,passthrough 函数控制是否缓存数据。koa-router-cache 适用于无状态的页面缓存或者 api 服务器,不适用页面会根据用户的登录状态的不同而渲染不同的情况,而且 koa-router-cache 是针对请求路径的缓存实现,粒度较大,我们可以尝试给每一个数据库读取函数加一层 cache,如 co-cache:

var cache = require('co-cache')();

var getTopicsByPage = cache(function* getTopicsByPage(p) {
  p = p || 1;
  return yield client.db('test').collection('test').find().skip((p - 1) * 10).limit(10).toArray();
}, {
  prefix: 'cache:',
  key: function (p) {
    p = p || 1;
    if (p >= 3) {
      return false; // 只缓存第1、2页
    }
    return this.name + ':' + (p || 1);
  },
  expire: 10000
});
 
co(function* () {
  var topics = yield getTopicsByPage(2);
  ...
}).catch(onerror);

注意:最新的 co-cache 依赖 redis 做缓存,所以你需要先启动 redis,co-cache 接收 client 参数指定 redis 连接,更多配置见 co-cache 的 readme。

getTopicsByPage 是一个通过页码(p)去数据库中查询话题(topic)的函数。不使用 co-cache 的话每次调用 getTopicsByPage 都会查询数据库,用 co-cache 包裹并加上过期时间(10秒)后,10 秒内每次调用该函数(查询第 1、2 页的话题)都是返回缓存中的结果,10 秒后再次调用会执行一次数据库查询操作并缓存结果。co-cache 的适用场景也十分有限,通常用来包裹诸如数据库查询函数。