在使用vue作一個項目的時候,有些須要keep-alive的內容,這些數據請求一次就不會再變,並且大部分用戶的數據都是同樣的,因此這塊加個緩存再好不過了。javascript
部署好redis,很是歡快的加上了node redis的插件,而後包裝一下,跑通了,happy得不得了。但隨即而來的問題是這樣:html
在服務剛起來的時候,或者數據過時的時候,須要從新請求數據庫而後再緩存。vue
這個時候有10個用戶同時發起一樣的請求(參數徹底一致的請求爲一樣的請求),會同時去redis中拿數據(由於redis中尚未數據)。java
10個一樣的請求都沒有從緩存裏面拿到數據,最終這10個請求都去後臺數據庫請求了,而後一遍一遍的又寫到緩存裏面去了。node
這就發生了緩存擊穿問題,嚴重的資源浪費!redis
既然有10個一樣的請求,那麼其實只讓第一個請求去數據庫拿數據,而後其他9個請求只須要等待第一個請求回來就行了,而後10個請求一塊兒拿着第一個請求回來的數據返回到vue。這樣10個請求在服務器端只發生了一次http請求(數據庫在另外一臺機器),數據庫只處理了一個查詢,減小資源浪費又減輕了數據庫壓力。數據庫
後端使用的node+koa2,衆所周知node是單線程,對於這種問題,在多線程語言中解決起來及其方便,node的問題就在於如何讓其他9個問題處於掛起等待狀態,使其等待第一個請求回來。後端
既然是要掛起等待,那確定是要異步了,那要異步確定要Promise + async/await
了。不得不說koa對於異步流程的處理真的很棒。api
那只有讓着9個請求進入異步模式就能解決這個問題了。想來想去仍是藉助了node Events
模塊。一種訂閱/發佈模式的高級實現。events對事件的封裝很是完美,在node內部也大量使用了events模塊。緩存
這樣使用Events的once 與 emit
,與Promise配合起來,基本上就解決問題了。
由於使用了koa2,對於異步的處理機器方便。
首先,須要一個key,這個key能夠表明一個請求鏈接,相同的請求那麼key也是一個了。
這個key會在events.once中使用。先寫一個events的公共方法:
import EventEmitter from 'events'; const emitter = new EventEmitter(); /** * 獲取等待的數據 * * @export * @param {String} key * @returns {any} */ export async function awaitData(key) { //返回一個Promise,外層已被async包裝 return new Promise(resolve => { //由於 emitter 註冊監聽器默認的最大限制是10,因此在併發多的時候出問題。須要動態調整數量 emitter.setMaxListeners(emitter.getMaxListeners() + 1); emitter.once(key, (data) => { //返回數據 resolve(data); //減去當前監聽器的數量 emitter.setMaxListeners(Math.max(emitter.getMaxListeners() - 1, 0)); }); }); } /** * 第一個請求向後臺發起查詢請求 * 而且佔位,告知後面的請求,這件事情我去辦了,大家等着我回來就能夠了 * * @export * @param {string} key * @param {any} params * @returns {any} */ export async function queryData(key, params) { // 這裏是個關鍵,起到佔位的用途,後面的請求會經過emitter.eventNames()去判斷前面有沒有請求去數據庫了。也可使用其餘方式實現這個步驟 emitter.once(key, () => { }); return new Promise(resolve => { //這裏爲去後臺數據庫請求的操做,這塊使用setTimeout模擬異步操做 setTimeout(() => { const data = 'just a test.'; //eimt 觸發事件,將data傳遞給其餘監聽這個key的函數 myEE.emit(key, data); //返回給第一個請求 resolve(data); }, 3000); // 爲了效果明顯能夠時間再長點 }); } /** * 查詢當前事件是否被監聽,若是被監據說明有請求去數據庫了,我也繼續監聽等待第一個回來 * * @export * @param {any} key * @returns {boolean} */ export function hasEvent(key){ //查詢全部事件監聽器中有沒有這個key return emitter.eventNames().includes(key); }
關於監聽器數量的默認限制能夠看官方文檔的說法https://nodejs.org/api/events.html#events_eventemitter_defaultmaxlisteners
基本上能用到的都封裝好了,開始業務代碼:
//koa2 app.use(async (ctx, next) => { //這裏不寫路由了,直接path判斷模擬下路由 if (ctx.path === '/getData') { //使用md5去生產key,md5怎麼來的就不寫了 const key = md5(ctx.path + JSON.stringify(ctx.query)); //判斷當前key有沒有被監聽 if (event.hasEvent(key)) { //監聽事件 等待被觸發,這裏使用異步與事件結合,使當前請求處於pendding掛起狀態 return ctx.body = await event.awaitData(key); } else { //這裏做爲第一個請求,去數據庫拿數據,而後觸發其餘等待的事件。 return ctx.body = await event.queryData(key); } }else{ return next(); } })
這樣基本上完成了10個請求,一個發出,九個等待的要求。但這種方式也有個缺點,這個方式只能在單節點生效
,在有負載均衡的多節點
中,這個方法是不行的,多節點之間
也會有稍微的資源浪費。