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