假如网站的并发访问量很高,那么我们通常会使用缓存对用得多的数据进行缓存。使用缓存会遇到缓存失效的情况,比如说程序重启,或者缓存到期清空。失效的情况下我们需要去数据库取出数据加载到缓存中然后再回应访客的请求,这个时候同时有很多人同时请求情况就会变得麻烦起来。很可能会造成多个缓存构建工作同时进行的情况,从而大幅影响到网站的响应速度。特别是在高并发的情况下,这种情况几乎是一定会发生的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
router.get('/', function(req, res) {
  if (!cache.exists("KEY") {
    db.query("SQL", function(err, result) {
      cache.set("KEY", result.rows);
      res.render("TPL", { "data": result.rows });
    });
  } else {
    res.render("TPL", { "data": cache.get("KEY") });
  }
});


在同步式多进程/线程的编程语言中,比如像 asp.net ,可以通过锁定特定的全局静态变量,控制对 cache 的访问来解决这个问题。而 Node.js 是单进程单线程语句,可以直接采用变量进行控制而不必担心线程同步的问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var state = "ready";

router.get('/', function(req, res) {
  if (!cache.exists("KEY") && state == "ready") {
    state = "preparing";
    db.query("SQL", function(err, result) {
      state = "ready";
      cache.set("KEY", result.rows);
      res.render("TPL", { "data": result.rows });
    });
  } else {
    res.render("TPL", { "data": cache.get("KEY") });
  }
});

但是这样一来,在并发的情况下,只有第一个请求会被正常处理,同时进来的其它请求将不会得到正确的结果。我们需要利用 EventEmitter 的事件机制来对付这种情形。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var emitter =  new events.EventEmitter();
var state = "ready"; // 控制缓存只加载一次
router.get('/', function(req, res, next) {
  if (!cache.exists("KEY")) {
    emitter.once('load', function(rows) {
      res.render("TPL", { "data": rows });
    });
    if (state == "ready") {
      state = "preparing";
      db.query("SQL", function(err, result) {
        if (err) return next(err);
        cache.set("KEY", result.rows);
        emitter.emit("load", result.rows);
        state = "ready";
      });
    }
  } else {
    res.render("TPL", { "data": cache.get("KEY") });
  }
});

我们通过 state 变量去控制缓存只加载一次,在缓存成功加载后 emitter 将会触发 load 事件,调用所有侦听器,这样,所有并发请求都能得到正确的响应。由于 EventEmitter 对侦听器数量有限制,根据使用的实际情况,可能要调 emitter.setMaxListeners(0) 解除侦听器数量限制。

once 与 on 的区别在于 once 只被调用一次,相关的侦听函数在被调用之后即从 emitter 中被移除。在这个上下文中这个区别相当重要,Node.js 不同于 php 那样的实时解析运行,它是驻留在内存中的。emitter 在程序开始直到结束中都是同一个对象,而缓存失效在这运行过程中可能发生多次。如果每次侦听器不被移除将会存在许多无效的回调,对性能和健壮性都有影响。

这种处理方式只适用于单进程的运行方式,实际生产环境如果是多进程的情况又该怎么去控制这个缓存加载呢?继续研究。