zoukankan      html  css  js  c++  java
  • 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
  • 相关阅读:
    【BZOJ3626】【LNOI2014】—Lca(树链剖分)
    【BZOJ2434】【NOI2011】—阿狸的打字机(AC自动机+线段树)
    【UVA10498】—Happiness(线性规划/单纯形算法)
    【BZOJ4736】【清华集训2016】—温暖会指引我们前行(LCT)
    【BZOJ3451】【Tyvj1953】—Normal(点分治+NTT)
    【SCOI2019】—DAY2T1 湖之精灵的游戏(凸包+二分)
    【BZOJ4817】【SDOI2017】—树点涂色(LCT+树链剖分+线段树)
    【SCOI2019】—DAY2T1 RGB(容斥)
    Stargazer的分治讲义
    python datetime 模块
  • 原文地址:https://www.cnblogs.com/zensh/p/3281692.html
Copyright © 2011-2022 走看看