[Node.js] Node + Redis 实现分布式Session方案

Session是什么?

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

Session 怎么工作?

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

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

  1. 内存存储
  2. 数据库存储
  3. 分布式缓存存储

分布式Session

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

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

Session_id

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

  • 服务端查询客户端Cookies 中是否存在 session_id

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

比如我用 Express, 那么我希望这个过程是自动完成的,不需要每次都去写 Response Header,那么我需要这么一个函数:

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 的哈希值,按顺时针方向就近安排一个小于其值的结点进行存储。


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

我的哈希环实现( 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 操作呼之欲出:

var crypto = require(‘crypto‘);
var config = require(‘../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 || [];
        var session = serialize(config.getConfigs().session_key, req.session.id);
        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;
                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);
        });
    }
};

module.exports.getById = function(id, name, callback){
    if (id) {
        redisMatrix.hgetRedis(id, name, function (err, reply) {
            callback(err, reply);
        });
    }
};
module.exports.deleteById = function(id, name, callback){
    if(id){
        redisMatrix.hdelRedis(id, name, function(err, reply){
            callback(err, reply);
        });
    }
};

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

更多文章请移步我的blog新地址: http://www.moye.me/

时间: 2024-10-13 21:50:44

[Node.js] Node + Redis 实现分布式Session方案的相关文章

Node.js操作Redis的简单示例

Redis是一个key-value类型的数据库,而key全部都是字符串,value可以是集合.hash.list等等. Redis是通过MULTI/DISCARD/EXEC/WATCH这4个命令来实现事务功能.对事务,我们必须知道事务安全性是一个非常重要的. 事务提供了一种"将多个命令打包,然后一次性.按顺序执行"的机制,并且在事务执行期间不会中断--意思就是在事务完成之前,客户端的其他命令都是阻塞状态. var redis = require("redis");

node.js与redis

最近在学习node创建项目,因为一直在用像mysql这样的结构型数据库,想学点新的东西,所以就把数据库换成了redis.redis是非关系型数据库.那关系型数据库跟非关系型数据库有什么区别呢?简单地说,就是一个有表的概念,一个没有.具体的区别自行Google吧.这里我主要介绍一下node.js与redis之间建立连接的过程,就是说如何早node:里面操作redis'数据库.因此,默认你已经装好这两个软件了. 第一步,我们需要打开redis的服务器.打开命令行,切换到redis安装目录,输入命令:

可扩容分布式session方案

分布式session有以下几种方案: 1. 基于nfs(net filesystem)的session共享 将共享服务器目录mount各服务器的本地session目录,session读写受共享服务器io限制,不能满足高并发. 2. 基于关系数据库的session共享 这种方案普遍使用.使用关系数据库存储session数据,对于mysql数据库,建议使用heap引擎. 这种方案性能取决于数据库的性能,在高并发下容易造成表锁(虽然可以采用行锁的存储引擎,性能会下降),并且需要自己实现session过

Redis实战和核心原理详解(5)使用Spring Session和Redis解决分布式Session跨域共享问题

Redis实战和核心原理详解(6)使用Spring Session和Redis解决分布式Session跨域共享问题 前言 对于分布式使用Nginx+Tomcat实现负载均衡,最常用的均衡算法有IP_Hash.轮训.根据权重.随机等.不管对于哪一种负载均衡算法,由于Nginx对不同的请求分发到某一个Tomcat,Tomcat在运行的时候分别是不同的容器里,因此会出现session不同步或者丢失的问题. 实际上实现Session共享的方案很多,其中一种常用的就是使用Tomcat.Jetty等服务器提

redis 实现分布式session配置

Redis分布式session配置 如上图,多实例下可以使用redis实现分布式session管理,客户端请求,经过负载均衡分发至tomcat实例,再经过session管理,实现session在redis中存取,这里暂时只有一台redis机器. 具体代码如下: 1.redis配置 可以使用spring-cache.xml作为redis配置文件名,首先配置redis缓存池: <bean id="jedisPoolConfig" class="redis.clients.j

node.js应用Redis数据库

node.js下使用Redis,首先: 1.有一台安装了Redis的服务器,当然,安装在本机也行 2.本机,也就是客户端,要装node.js 3.项目要安装nodejs_redis模块 注意第 3 点,不是在本机安装就行了,而是说,要在项目中安装(引用). 方法是,DOS窗口,在项目目录下,输入 npm install redis 这样就将nodejs_redis下载一份,放到当前目录下了.看看,多了一个文件夹:node_modules\redis 编写以下代码,保存到当前目录下\hello.j

node.js平台下Express的session与cookie模块包的配置

首先下载两个模块包 session模块包:用于保持登录状态或保持会话状态等. npm install express-session --save-dev cookie模块包:用于解析cookie. npm install cookie-parser --save-dev 接着在app.js(我在node.js的配置中提到的,也就是服务器主文件)中配置: var session = require("express-session"); var cookie = require(&qu

使用Node.js和Redis实现push服务--转载

出处:http://blog.csdn.net/unityoxb/article/details/8532028 push服务是一项很有用处的技术,它能改善交互,提升用户体验.要实现这项服务通常有两种途径,轮询和长连接.轮询就是客户端每隔一段时间就问服务器拿新数据,实现起来很简单但是服务器压力很大,而且大部分请求因为没有新数据都显得很浪费.长连接则是服务器将一个请求挂起,不输出任何内容,直到有新数据产生后才会完成这个请求,浏览器收到响应后则马上再发一个又让服务器挂住,如此反复.这么做的好处是能节

node.js 多异步之间的协作方案

<深入浅出node.js> P77 学习 ///用于处理多个事件对应一个侦听器的情况var count = 0; var results = {}; var done = function (key, value){ results[key] = value; count++; if (count === 3){ ///渲染页面 render(results); } }; fs.readFile(template_path, "utf8", function(err, te