關於node模擬"同步鎖"的方案暢想,解決防止緩存擊穿

背景

在使用vue作一個項目的時候,有些須要keep-alive的內容,這些數據請求一次就不會再變,並且大部分用戶的數據都是同樣的,因此這塊加個緩存再好不過了。javascript

問題-緩存擊穿

部署好redis,很是歡快的加上了node redis的插件,而後包裝一下,跑通了,happy得不得了。但隨即而來的問題是這樣:html

  1. 在服務剛起來的時候,或者數據過時的時候,須要從新請求數據庫而後再緩存。vue

  2. 這個時候有10個用戶同時發起一樣的請求(參數徹底一致的請求爲一樣的請求),會同時去redis中拿數據(由於redis中尚未數據)。java

  3. 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配合起來,基本上就解決問題了。

coding

由於使用了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個請求,一個發出,九個等待的要求。但這種方式也有個缺點,這個方式只能在單節點生效,在有負載均衡的多節點中,這個方法是不行的,多節點之間也會有稍微的資源浪費。

以上,致那顆騷動的心……
相關文章
相關標籤/搜索