关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » UI前端 »NodeJS+Redis实现分布式Session方案

    NodeJS+Redis实现分布式Session方案

    2014-10-30 00:00:00 出处:Long Luo的博客
    分享

    Session是什么?

    Session 是面向连接的状态信息,是对 Http 无状态协议的补充。

    Session 怎么工作?

    Session 数据保留在服务端,而为了标识具体 Session 信息指向哪个连接,需要客户端传递向服务端发送一个连接标识,比如存在Cookies 中的session_id值(也可以通过URL的QueryString传递),服务端根据这个id 存取状态信息。

    在服务端存储 Session,可以有很多种方案:

    内存存储 数据库存储 分布式缓存存储

    分布式Session

    随着网站规模(访问量/复杂度/数据量)的扩容,针对单机的方案将成为性能的瓶颈,分布式应用在所难免。所以,有必要研究一下 Session 的分布式存储。

    如前述, Session使用的标识其实是客户端传递的 session_id,在分布式方案中,一般会针对这个值进行哈希,以确定其在 hashing ring 的存储位置。

    Session_id

    在 Session 处理的事务中,最重要的环节莫过于 客户端与服务端 关于 session 标识的传递过程:

    服务端查询客户端Cookies 中是否存在 session_id 有session_id,是否过期?过期了需要重新生成;没有过期则延长过期 没有 session_id,生成一个,并写入客户端的 Set-Cookie 的 Header,这样下一次客户端发起请求时,就会在 Request Header 的 Cookies带着这个session_id

    比如我用 Express, 那么我希望这个过程是自动完成的,不需要每次都去写 Response Header,那么我需要这么一个函数(摘自朴灵的《深入浅出Node.js》):

    var setHeader = function (req, res, next) {
        var writeHead = res.writeHead;
        res.writeHead = function () {
            var cookies = res.getHeader('Set-Cookie');
            cookies = cookies || [];
            console.log('writeHead, cookies: ' + cookies);
            var session = serialize('session_id', req.session.id);
            cookies = Array.isArray(cookies)   cookies.concat(session) : 
                      [cookies, session];
            res.setHeader('Set-Cookie', cookies);
            return writeHead.apply(this, arguments);
        };
    
        next();
    };

    这个函数替换了writeHead,在每次Response写Header时它都会得到执行机会,所以它是自动化的。这个req.session.id 是怎么得到的,稍候会有详细的代码示例。

    Hashing Ring

    hashing ring 就是一个分布式结点的回路(取值范围:0到232 -1,在零点重合):Session 应用场景中,它根据 session_id 的哈希值,按顺时针方向就近安排一个大于其值的结点进行存储。

    Hashing Ring

    实现这个回路的算法多种多样,比如 一致性哈希。

    我的哈希环实现( hashringUtils.js:

    var INT_MAX = 0x7FFFFFFF;
    
    var node = function (nodeOpts) {
        nodeOpts = nodeOpts || {};
        if (nodeOpts.address) this.address = nodeOpts.address;
        if (nodeOpts.port) this.port = nodeOpts.port;
    };
    node.prototype.toString = function () {
        return this.address + ':' + this.port;
    };
    
    var ring = function (maxNodes, realNodes) {
        this.nodes = [];
        this.maxNodes = maxNodes;
        this.realNodes = realNodes;
    
        this.generate();
    };
    ring.compareNode = function (nodeA, nodeB) {
        return nodeA.address === nodeB.address &&
            nodeA.port === nodeB.port;
    };
    ring.hashCode = function (str) {
        if (typeof str !== 'string')
            str = str.toString();
        var hash = 1315423911, i, ch;
        for (i = str.length - 1; i >= 0; i--) {
            ch = str.charCodeAt(i);
            hash ^= ((hash << 5) + ch + (hash >> 2));
        }
        return  (hash & INT_MAX);
    };
    ring.prototype.generate = function () {
        var realLength = this.realNodes.length;
        this.nodes.splice(0); //clear all
    
        for (var i = 0; i < this.maxNodes; i++) {
            var realIndex = Math.floor(i / this.maxNodes * realLength);
            var realNode = this.realNodes[realIndex];
            var label = realNode.address + '#' + 
                (i - realIndex * Math.floor(this.maxNodes / realLength));
            var virtualNode = ring.hashCode(label);
    
            this.nodes.push({
                'hash': virtualNode,
                'label': label,
                'node': realNode
            });
        }
    
        this.nodes.sort(function(a, b){
            return a.hash - b.hash;
        });
    };
    ring.prototype.select = function (key) {
        if (typeof key === 'string')
            key = ring.hashCode(key);
        for(var i = 0, len = this.nodes.length; i<len; i++){
            var virtualNode = this.nodes[i];
            if(key <= virtualNode.hash) {
                console.log(virtualNode.label);
                return virtualNode.node;
            }
        }
        console.log(this.nodes[0].label);
        return this.nodes[0].node;
    };
    ring.prototype.add = function (node) {
        this.realNodes.push(node);
    
        this.generate();
    };
    ring.prototype.remove = function (node) {
        var realLength = this.realNodes.length;
        var idx = 0;
        for (var i = realLength; i--;) {
            var realNode = this.realNodes[i];
            if (ring.compareNode(realNode, node)) {
                this.realNodes.splice(i, 1);
                idx = i;
                break;
            }
        }
        this.generate();
    };
    ring.prototype.toString = function () {
        return JSON.stringify(this.nodes);
    };
    
    module.exports.node = node;
    module.exports.ring = ring;

    配置

    配置信息是需要根据环境而变化的,某些情况下它又是不能公开的(比如Session_id 加密用的私钥),所以需要一个类似的配置文件( config.cfg:

    {
        "session_key": "session_id",
        "SECRET": "myapp_moyerock",
        "nodes":
        [
           {"address": "127.0.0.1", "port": "6379"}
        ]
    }

    在Node 中序列化/反序列化JSON 是件令人愉悦的事,写个配置读取器也相当容易(configUtils.js:

    var fs = require('fs');
    var path = require('path');
    
    var cfgFileName = 'config.cfg';
    var cache = {};
    
    module.exports.getConfigs = function () {
        if (!cache[cfgFileName]) {
            if (!process.env.cloudDriveConfig) {
                process.env.cloudDriveConfig = path.join(process.cwd(), cfgFileName);
            }
            if (fs.existsSync(process.env.cloudDriveConfig)) {
                var contents = fs.readFileSync(
                    process.env.cloudDriveConfig, {encoding: 'utf-8'});
                cache[cfgFileName] = JSON.parse(contents);
            }
        }
        return cache[cfgFileName];
    };

    分布式Redis 操作

    有了上述的基础设施,实现一个分布式 Redis 分配器就变得相当容易了。为演示,这里只简单提供几个操作 Hashes 的方法(redisMatrix.js:

    var hashringUtils = require('../hashringUtils'),
        ring = hashringUtils.ring,
        node = hashringUtils.node;
    
    var config = require('../configUtils');
    
    var nodes = config.getConfigs().nodes;
    for (var i = 0, len = nodes.length; i < len; i++) {
        var n = nodes[i];
        nodes[i] = new node({address: n.address, port: n.port});
    }
    
    var hashingRing = new ring(32, nodes);
    
    module.exports = hashingRing;
    module.exports.openClient = function (id) {
        var node = hashingRing.select(id);
        var client = require('redis').createClient(node.port, node.address);
        client.on('error', function (err) {
            console.log('error: ' + err);
        });
        return client;
    };
    module.exports.hgetRedis = function (id, key, callback) {
        var client = hashingRing.openClient(id);
        client.hget(id, key, function (err, reply) {
            if (err)
                console.log('hget error:' + err);
            client.quit();
            callback.call(null, err, reply);
        });
    };
    module.exports.hsetRedis = function (id, key, val, callback) {
        var client = hashingRing.openClient(id);
        client.hset(id, key, val, function (err, reply) {
            if (err)
                console.log('hset ' + key + 'error: ' + err);
            console.log('hset [' + key + ']:[' + val + '] reply is:' + reply);
            client.quit();
    
            callback.call(null, err, reply);
        });
    };
    module.exports.hdelRedis = function(id, key, callback){
        var client = hashingRing.openClient(id);
        client.hdel(id, key, function (err, reply) {
            if (err)
                console.log('hdel error:' + err);
            client.quit();
            callback.call(null, err, reply);
        });
    };

    分布式Session操作

    session_id 的事务和 分布式的Redis都有了,分布式的 Session 操作呼之欲出(sessionUtils.js:

    var crypto = require('crypto');
    var config = require('../config/configUtils');
    
    var EXPIRES = 20 * 60 * 1000;
    var redisMatrix = require('./redisMatrix');
    
    var sign = function (val, secret) {
        return val + '.' + crypto
            .createHmac('sha1', secret)
            .update(val)
            .digest('base64')
            .replace(/[/+=]/g, '');
    };
    var generate = function () {
        var session = {};
        session.id = (new Date()).getTime() + Math.random().toString();
        session.id = sign(session.id, config.getConfigs().SECRET);
        session.expire = (new Date()).getTime() + EXPIRES;
        return session;
    };
    var serialize = function (name, val, opt) {
        var pairs = [name + '=' + encodeURIComponent(val)];
        opt = opt || {};
    
        if (opt.maxAge) pairs.push('Max-Age=' + opt.maxAge);
        if (opt.domain) pairs.push('Domain=' + opt.domain);
        if (opt.path) pairs.push('Path=' + opt.path);
        if (opt.expires) pairs.push('Expires=' + opt.expires);
        if (opt.httpOnly) pairs.push('HttpOnly');
        if (opt.secure) pairs.push('Secure');
    
        return pairs.join('; ');
    };
    
    var setHeader = function (req, res, next) {
        var writeHead = res.writeHead;
        res.writeHead = function () {
            var cookies = res.getHeader('Set-Cookie');
            cookies = cookies || [];
            console.log('writeHead, cookies: ' + cookies);
            var session = serialize(config.getConfigs().session_key, req.session.id);
            console.log('writeHead, session: ' + session);
            cookies = Array.isArray(cookies)   cookies.concat(session) : [cookies, session];
            res.setHeader('Set-Cookie', cookies);
            return writeHead.apply(this, arguments);
        };
    
        next();
    };
    
    exports = module.exports = function session() {
        return function session(req, res, next) {
            var id = req.cookies[config.getConfigs().session_key];
            if (!id) {
                req.session = generate();
                id = req.session.id;
                var json = JSON.stringify(req.session);
                redisMatrix.hsetRedis(id, 'session', json,
                    function () {
                        setHeader(req, res, next);
                    });
            } else {
                console.log('session_id found: ' + id);
                redisMatrix.hgetRedis(id, 'session', function (err, reply) {
                    var needChange = true;
                    console.log('reply: ' + reply);
                    if (reply) {
                        var session = JSON.parse(reply);
                        if (session.expire > (new Date()).getTime()) {
                            session.expire = (new Date()).getTime() + EXPIRES;
                            req.session = session;
                            needChange = false;
                            var json = JSON.stringify(req.session);
                            redisMatrix.hsetRedis(id, 'session', json,
                                function () {
                                    setHeader(req, res, next);
                                });
                        }
                    }
    
                    if (needChange) {
                        req.session = generate();
                        id = req.session.id; // id need change
                        var json = JSON.stringify(req.session);
                        redisMatrix.hsetRedis(id, 'session', json,
                            function (err, reply) {
                                setHeader(req, res, next);
                            });
                    }
                });
            }
        };
    };
    
    module.exports.set = function (req, name, val) {
        var id = req.cookies[config.getConfigs().session_key];
        if (id) {
            redisMatrix.hsetRedis(id, name, val, function (err, reply) {
    
            });
        }
    };
    /*
     get session by name
     @req request object
     @name session name
     @callback your callback
     */
    module.exports.get = function (req, name, callback) {
        var id = req.cookies[config.getConfigs().session_key];
        if (id) {
            redisMatrix.hgetRedis(id, name, function (err, reply) {
                callback(err, reply);
            });
        } else {
            callback();
        }
    };
    
    module.exports.getById = function (id, name, callback) {
        if (id) {
            redisMatrix.hgetRedis(id, name, function (err, reply) {
                callback(err, reply);
            });
        } else {
            callback();
        }
    };
    module.exports.deleteById = function (id, name, callback) {
        if (id) {
            redisMatrix.hdelRedis(id, name, function (err, reply) {
                callback(err, reply);
            });
        } else {
            callback();
        }
    };

    结合 Express 应用

    在 Express 中只需要简单的 use 就可以了( app.js:

    var session = require('../sessionUtils');
    app.use(session());

    这个被引用的 session 模块暴露了一些操作 session 的方法,在需要时可以这样使用:

    app.get('/user', function(req, res){
        var id = req.query.sid;
        session.getById(id, 'user', function(err, reply){
            if(reply){
                   //Some thing TODO
            }
        });
        res.end('');
    });

    小结

    虽然本文提供的是基于 Express 的示例,但基于哈希算法和缓存设施的分布式思路,其实是放之四海而皆准的

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    正版 Windows 11产品密钥怎么查找/查看?

    还有3个月,微软将停止 Windows 10 的更新

    Windows 10 终止支持后,企业为何要立即升级?

    Windows 10 将于 2025年10 月终止技术支持,建议迁移到 Windows 11

    Windows 12 发布推迟,微软正全力筹备Windows 11 25H2更新

    Linux 退出 mail的命令是什么

    Linux 提醒 No space left on device,但我的空间看起来还有不少空余呢

    hiberfil.sys文件可以删除吗?了解该文件并手把手教你删除C盘的hiberfil.sys文件

    Window 10和 Windows 11哪个好?答案是:看你自己的需求

    盗版软件成公司里的“隐形炸弹”?老板们的“法务噩梦” 有救了!

    IT头条

    公安部:我国在售汽车搭载的“智驾”系统都不具备“自动驾驶”功能

    02:03

    液冷服务器概念股走强,博汇、润泽等液冷概念股票大涨

    01:17

    亚太地区的 AI 驱动型医疗保健:2025 年及以后的下一步是什么?

    16:30

    智能手机市场风云:iPhone领跑销量榜,华为缺席引争议

    15:43

    大数据算法和“老师傅”经验叠加 智慧化收储粮食尽显“科技范”

    15:17

    技术热点

    商业智能成CIO优先关注点 技术落地方显成效(1)

    用linux安装MySQL时产生问题破解

    JAVA中关于Map的九大问题

    windows 7旗舰版无法使用远程登录如何开启telnet服务

    Android View 事件分发机制详解

    MySQL用户变量的用法

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。