之前版本的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