jsGen技術總結之:在Node.js中構建redis同步緩存

之前版本的jsGen直接利用Node.js的Buffer內存緩存數據,這樣帶來的一個問題是沒法開啓Cluster,多個Node.js進程的內存都是相互獨立的,不能相互訪問,不能及時更新數據變更。git

新本(0.6.0)jsGen使用了第三方內存數據庫redis做爲緩存,如此以來多進程或多機運行jsGen成爲可能。redis做爲內存緩存的惟一缺陷就是——異步驅動,讀取或寫入數據都得callback!。github

var myData;

redisCache.get(key, function (err, data) {
    // callback讀取緩存數據
    myData = data;
});

redisCache.put(key, myData, function (err, reply) {
    // 寫入緩存,callback確認寫入結果
});

 

那麼,有沒有辦法構建一個「同步」的redis緩存呢,使得讀取、寫入緩存像下面同樣簡單:redis

// 從緩存讀取數據
var myData = redisCache.data;

// 往緩存寫入數據
redisCache.data = myData;

 

redis同步緩存原理

我採用JavaScript的getter、setter和閉包構建了這個redis同步緩存數據庫

利用閉包建立一個緩存數據鏡像,讀取緩存時,getter從鏡像讀取;寫入緩存時,setter把值寫入鏡像,再寫入redis數據庫。json

若是開啓多進程,緩存鏡像仍然是分佈在各個進程中,是相互獨立的。若是一個進程更新了緩存數據,如何及時更新其它進程的緩存鏡像呢?這就用到了redis的Pub/Sub系統,setter更新緩存時,更新數據寫入數據庫後,發佈更新通知,其它redis進程收到通知就從redis數據庫讀取數據來更新鏡像。緩存

各進程的緩存雖然不是真正的同步更新,但也算及時更新了,能夠知足通常業務須要。缺點是多消耗了一倍的內存。對於頻繁訪問更新的小數據,如config數據,很適合採用這個方案。下面是來自jsGen/lib/redia.js的源代碼,經過一個config的json數據模板構建一個redis同步緩存的config對象,數據不但寫入了redis數據庫,還按照必定頻率寫入MongoDB數據庫。閉包

jsGen源代碼片斷

// clientSub:專用於訂閱的redis client
// client[globalCacheDb]:存取數據的redis client
// 異步任務函數then及then.each,見 https://github.com/zensh/then.js

function initConfig(configTpl, callback) {
    var config = {},
        // 新構建的config緩存
        _config = union(configTpl),
        // 從configTpl克隆的config閉包鏡像
        subPubId = MD5('' + Date.now() + Math.random(), 'base64');
        // 本進程的惟一識別ID

    callback = callback || callbackFn;

    var update = throttle(function () {
        jsGen.dao.index.setGlobalConfig(_config);
    }, 300000); // 將config寫入MongoDB,每五分鐘內最多執行一次

    function updateKey(key) {
    // 更新鏡像的key鍵值
        return then(function (defer) {
            client[globalCacheDb].hget('config.hash', key, defer);
            // 從redis讀取更新的數據
        }).then(function (defer, reply) {
            reply = JSON.parse(reply);
            _config[key] = typeof _config[key] === typeof reply ? reply : _config[key];
            // 數據寫入config鏡像
            defer(null, _config[key]);
        }).fail(errorHandler);
    }

    clientSub.on('message', function (channel, key) {
        var ID = key.slice(0, 24);
        key = key.slice(24);
        // 分離識別ID和key
        if (channel === 'updateConfig' && ID !== subPubId) {
        // 來自於updateConfig頻道且不是本進程發出的更新通知
            if (key in _config) {
                updateKey(key);  // 更新一個key
            } else {
                each(_config, function (value, key) {  // 更新整個config鏡像
                    updateKey(key);
                });
            }
        }
    });
    clientSub.subscribe('updateConfig');
    // 訂閱updateConfig頻道

    each(configTpl, function (value, key) {
    // 從configTpl模板構建getter/setter,利用Hash類型存儲config
        Object.defineProperty(config, key, {
            set: function (value) {
                then(function (defer) {
                    if ((value === 1 || value === -1) && typeof _config[key] === 'number') {
                        _config[key] += value;
                        // 按1遞增或遞減,更新鏡像,再更新redis
                        client[globalCacheDb].hincrby('config.hash', key, value, defer);
                    } else {
                        _config[key] = value;
                        // 由於redis存儲字符串,下面先序列化。
                        client[globalCacheDb].hset('config.hash', key, JSON.stringify(value), defer);
                    }
                }).then(function () {
                // redis數據庫更新完成,向其餘進程發出更新通知
                    client[globalCacheDb].publish('updateConfig', subPubId + key);
                }).fail(jsGen.thenErrLog);
                update();  // 更新MongoDB
            },
            get: function () {
                return _config[key];
                // 從鏡像讀取數據
            },
            enumerable: true,
            configurable: true
        });
    });
    // 初始化config對象的值,如重啓進程後,若是redis數據庫原來存有數據,讀取該數據
    then.each(Object.keys(configTpl), function (next, key) {
        updateKey(key).then(function (defer, value) {
            return next ? next() : callback(null, config);
            // 異步返回新的config對象,已初始化數據值
        }).fail(function (defer, err) {
            callback(err);
        });
    });
    return config;  // 同步返回新的config對象
}

 

初始化代碼,詳見jsGen/app.js

then(function (defer) {
    redis.initConfig(jsGen.lib.json.GlobalConfig, defer);
    // 異步初始化config緩存
}).then(function (defer, config) {
    jsGen.config = config;
    // config緩存引用到全局變量jsGen
    // ...
}).then(function (defer, config) {
    // ...
});

 

調用示例

// 從config緩存取配置值並new一個LRU緩存
jsGen.cache.user = new CacheLRU(jsGen.config.userCache);

// 更新網站訪問次數
jsGen.config.visitors = 1; // 網站訪問次數+1
相關文章
相關標籤/搜索