jsGen技术总结之:在Node.js中构建redis同步缓存

以前版本的jsGen直接利用Node.js的Buffer内存缓存数据,这样带来的一个问题是无法开启Cluster,多个Node.js进程的内存都是相互独立的,不能相互访问,不能及时更新数据变动。

新本(0.6.0)jsGen使用了第三方内存数据库redis作为缓存,如此以来多进程或多机运行jsGen成为可能。redis作为内存缓存的唯一缺陷就是——异步驱动,读取或写入数据都得callback!。

var myData;

redisCache.get(key, function (err, data) {
    // callback读取缓存数据
    myData = data;
});

redisCache.put(key, myData, function (err, reply) {
    // 写入缓存,callback确认写入结果
});

那么,有没有办法构建一个“同步”的redis缓存呢,使得读取、写入缓存像下面一样简单:

// 从缓存读取数据
var myData = redisCache.data;

// 往缓存写入数据
redisCache.data = myData;

redis同步缓存原理

我采用JavaScript的getter、setter和闭包构建了这个redis同步缓存

利用闭包创建一个缓存数据镜像,读取缓存时,getter从镜像读取;写入缓存时,setter把值写入镜像,再写入redis数据库。

如果开启多进程,缓存镜像仍然是分布在各个进程中,是相互独立的。如果一个进程更新了缓存数据,如何及时更新其它进程的缓存镜像呢?这就用到了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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JsGen是用纯JavaScript编写的新一代开源社区网站系统,基于node.js和AngularJS。主要用于搭建SNS类型的专业社区,对客户端AngularJS应用稍作修改也可变成多用户博客系统、论坛或者CMS内容管理系统。 jsGen基于NodeJS编写服务器端程序,提供静态文件响应和REST API接口服务。基于AngularJS编写浏览器端应用,构建交互式网页UI视图。基于MongoDB编写数据存储系统。 特点: 1、前沿的WEB技术,前所未有的网站构架形态,前端与后端完全分离,前端由AngularJS生成视图,后端由Node.js提供REST API数据接口和静态文件服务。只需改动前端AngularJS应用视图形态,即可变成论坛、多用户博客、内容管理系统等。 2、用户数据、文章评论数据、标签数据、分页缓存数据、用户操作间隔限时等都使用LRU缓存 ,降低数据库IO操作,同时保证同步更新数据。 3、前后端利用json数据包进行通信。文章、评论采用Markdown格式编辑、存储,支持GitHub的GFM,AngularJS应用将Markdown解析成HTML DOM。 4、用户帐号系统,关注(follow)用户/粉丝、邮箱验证激活、邮箱重置密码、SHA256加密安全登录、登录失败5次锁定/邮箱解锁、用户标签、用户积分、用户权限等级、用户阅读时间线等功能。用户主页只展现感兴趣的最新文章(关注标签、关注作者的文章)。 5、文章/评论系统,文章、评论使用统一数据结构,均可被评论、支持、反对、标记(mark,即收藏),当评论达到一定条件(精彩评论)可自动提升为文章(进入文章列表展现,类branch功能),同样文章达到一定条件即可自动推荐。自动实时统计文章、评论热度,自动生成最新文章列表、一周内最热文章列表、一周内最热评论列表、最近更新文章列表。强大的文章、评论列表分页导航功能,缓存每个用户的分页导航浏览记录。 6、标签系统,文章和用户均可加标签,可设置文章、用户标签数量上限。用户通过标签设置自己关注话题,文章通过标签形成分类。标签在用户设置标签或文章设置标签时自动生成。自动展现热门标签。 7、文章合集系统,作者、编辑、管理员可将一系列相关文章组成合集,形成有章节大纲目录的在线电子书形态,可用于教程文档、主题合集甚至小说连载等。(待完成) 8、站内短信系统,提供在文章、评论 @用户的功能,重要短信发送邮件通知功能等。(待完成) 9、后台管理系统,网站参数设置缓存设置、网站运行信息、文章、评论、用户、标签、合集、站内短信等管理。 10、Robot SEO系统,由于AngularJS网页内容在客户端动态生成,对搜索引擎robot天生免疫。jsGen针对robot访问,在服务器端动态生成robot专属html页面。搜索引擎Robot名称可在管理后台添加。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值