雪崩問題就是在瀏覽器緩存失效后潮针,并發(fā)訪問量大量涌入數(shù)據(jù)庫執(zhí)行查詢操作暖哨,導致數(shù)據(jù)庫無法同時承受如此大的訪問量畏吓,從而影響網(wǎng)站效果滚婉。
在 Node.js 中一句數(shù)據(jù)庫查詢的代碼為:
var select = function (callback) {
db.select('SQL', function (results) {
// 比如傳入的函數(shù)是展示數(shù)據(jù)图筹,那么該句的作用就是將查詢后返回的數(shù)據(jù)展示出來
callback(results);
});
};
以上是一句數(shù)據(jù)庫查詢的調(diào)用,如果站點剛好啟動,這時候緩存中是不存在數(shù)據(jù)的远剩,而如果訪問量巨大扣溺,同一句 SQL 會被發(fā)送到數(shù)據(jù)庫中反復查詢,影響到服務的整體性能瓜晤。一個改進是添加一個狀態(tài)鎖锥余。
var select = function (callback) {
if (status === 'ready') {
status = 'pending';
db.select('SQL', function (results) {
// 比如傳入的函數(shù)是展示數(shù)據(jù),那么該句的作用就是將查詢后返回的數(shù)據(jù)展示出來
callback(results);
status = 'ready';
});
}
};
但是這種情景痢掠,連續(xù)的多次調(diào)用 select驱犹,只有第一次調(diào)用是生效的,后續(xù)的 select 是沒有數(shù)據(jù)服務的足画。所以這個時候引入事件隊列吧:
var proxy = new EventProxy();
var status = 'ready';
var select = function (callback) {
// 將該實例的該操作放入隊列雄驹,并且操作只執(zhí)行一次
proxy.once('selected', callback);
if (status === 'ready') {
status = 'pending';
db.select('SQL', function (results) {
// 將該操作返回的數(shù)據(jù)作為回調(diào)函數(shù)的輸入?yún)?shù),執(zhí)行回調(diào)函數(shù)
proxy.emit('selected', results);
status = 'ready';
});
}
};
代碼分析:
對于每一次查詢淹辞,都會通過 once 方法訂閱 selected 事件荠医。當某個查詢正在進行時,其他同時到達的查詢處于 pending 狀態(tài)桑涎,在訂閱了 selected 事件后什么都不做,而請求的回調(diào)被壓入事件隊列中兼贡。當那個查詢結(jié)束時攻冷,執(zhí)行 emit 方法發(fā)布 selected 事件并更新狀態(tài)。此時遍希,那些訂閱了 selected 事件的查詢的回調(diào)函數(shù)被依次調(diào)用等曼,并傳入查詢結(jié)果 results 作為參數(shù)。由于 once 方法的(執(zhí)行一次就會將監(jiān)視器移除)特點凿蒜,每個查詢的回調(diào)只會被執(zhí)行一次禁谦。執(zhí)行回調(diào)函數(shù)以后,由于 status 變?yōu)?ready废封,又可以響應其他的查詢州泊。
在這個過程中,對于相同的 SQL 語句漂洋,保證在同一個查詢開始到結(jié)束的時間中永遠只有一次遥皂,在這查詢期間到來的相同查詢,只需在隊列中等待數(shù)據(jù)就緒即可刽漂。這些相同查詢只是利用了這次查詢的結(jié)果執(zhí)行了回調(diào)而已演训,并沒有查詢數(shù)據(jù)庫,節(jié)省了重復的數(shù)據(jù)庫調(diào)用開銷贝咙。
由于 Node.js 單線程執(zhí)行的原因样悟,此處無需擔心狀態(tài)問題。這種方式其實也可以應用到其他遠程調(diào)用的場景中,即使外部沒有緩存策略窟她,也能有效節(jié)省重復開銷陈症。
此處也可以用 EventEmitter 替代 EventProxy,不過可能存在偵聽器過多礁苗,引發(fā)警告爬凑,需要調(diào)用 setMaxListeners(0) 移除掉警告,或者設更大的警告閥值试伙。