雪崩问题就是在浏览器缓存失效后,并发访问量大量涌入数据库执行查询操作,导致数据库无法同时承受如此大的访问量,从而影响网站效果。
在 Node.js 中一句数据库查询的代码为:
var select = function (callback) {
db.select('SQL', function (results) {
// 比如传入的函数是展示数据,那么该句的作用就是将查询后返回的数据展示出来
callback(results);
});
};
以上是一句数据库查询的调用,如果站点刚好启动,这时候缓存中是不存在数据的,而如果访问量巨大,同一句 SQL 会被发送到数据库中反复查询,影响到服务的整体性能。一个改进是添加一个状态锁。
var select = function (callback) {
if (status === 'ready') {
status = 'pending';
db.select('SQL', function (results) {
// 比如传入的函数是展示数据,那么该句的作用就是将查询后返回的数据展示出来
callback(results);
status = 'ready';
});
}
};
但是这种情景,连续的多次调用 select,只有第一次调用是生效的,后续的 select 是没有数据服务的。所以这个时候引入事件队列吧:
var proxy = new EventProxy();
var status = 'ready';
var select = function (callback) {
// 将该实例的该操作放入队列,并且操作只执行一次
proxy.once('selected', callback);
if (status === 'ready') {
status = 'pending';
db.select('SQL', function (results) {
// 将该操作返回的数据作为回调函数的输入参数,执行回调函数
proxy.emit('selected', results);
status = 'ready';
});
}
};
代码分析:
对于每一次查询,都会通过 once 方法订阅 selected 事件。当某个查询正在进行时,其他同时到达的查询处于 pending 状态,在订阅了 selected 事件后什么都不做,而请求的回调被压入事件队列中。当那个查询结束时,执行 emit 方法发布 selected 事件并更新状态。此时,那些订阅了 selected 事件的查询的回调函数被依次调用,并传入查询结果 results 作为参数。由于 once 方法的(执行一次就会将监视器移除)特点,每个查询的回调只会被执行一次。执行回调函数以后,由于 status 变为 ready,又可以响应其他的查询。
在这个过程中,对于相同的 SQL 语句,保证在同一个查询开始到结束的时间中永远只有一次,在这查询期间到来的相同查询,只需在队列中等待数据就绪即可。这些相同查询只是利用了这次查询的结果执行了回调而已,并没有查询数据库,节省了重复的数据库调用开销。
由于 Node.js 单线程执行的原因,此处无需担心状态问题。这种方式其实也可以应用到其他远程调用的场景中,即使外部没有缓存策略,也能有效节省重复开销。
此处也可以用 EventEmitter 替代 EventProxy,不过可能存在侦听器过多,引发警告,需要调用 setMaxListeners(0) 移除掉警告,或者设更大的警告阀值。