关闭 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 的示例,但基于哈希算法和缓存设施的分布式思路,其实是放之四海而皆准的

    上一篇返回首页 下一篇

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

    别人在看

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

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

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

    帝国CMS7.5编辑器上传图片取消宽高的三种方法

    帝国cms如何自动生成缩略图的实现方法

    Windows 12即将到来,将彻底改变人机交互

    帝国CMS 7.5忘记登陆账号密码怎么办?可以phpmyadmin中重置管理员密码

    帝国CMS 7.5 后台编辑器换行,修改回车键br换行为p标签

    Windows 11 版本与 Windows 10比较,新功能一览

    Windows 11激活产品密钥收集及专业版激活方法

    IT头条

    无线路由大厂 TP-Link突然大裁员:补偿N+3

    02:39

    Meta 千万美金招募AI高级人才

    00:22

    更容易爆炸?罗马仕充电宝被北京多所高校禁用,公司紧急回应

    17:19

    天衍”量子计算云平台,“超算+量算” 告别“算力孤岛时代”

    18:18

    华为Pura80系列新机预热,余承东力赞其复杂光线下的视频拍摄实力

    01:28

    技术热点

    MySQL基本调度策略浅析

    MySQL使用INSERT插入多条记录

    SQL Server高可用的常见问题

    3D立体图片展示幻灯片JS特效

    windows 7上网看视频出现绿屏的原因及解决方法

    windows 7 64位系统的HOSTS文件在哪里?想用它加快域名解析

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

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