NodeJS+Redis实现分布式Session方案

来源:互联网 发布:网络局域网上不了 编辑:程序博客网 时间:2024/05/28 17:04
  • Session是什么?
  • Session 怎么工作?
  • 分布式Session
  • Session_id
  • Hashing Ring
  • 配置
  • 分布式Redis 操作
  • 分布式Session操作
  • 结合 Express 应用
  • 小结

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,那么我需要这么一个函数(摘自朴灵的《深入浅出Node.js》):

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

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

Hashing Ring

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

Hashing Ring

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

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

?
varINT_MAX = 0x7FFFFFFF;
 
varnode = function(nodeOpts) {
    nodeOpts = nodeOpts || {};
    if(nodeOpts.address) this.address = nodeOpts.address;
    if(nodeOpts.port) this.port = nodeOpts.port;
};
node.prototype.toString =function () {
    returnthis.address +':' + this.port;
};
 
varring = function(maxNodes, realNodes) {
    this.nodes = [];
    this.maxNodes = maxNodes;
    this.realNodes = realNodes;
 
    this.generate();
};
ring.compareNode =function (nodeA, nodeB) {
    returnnodeA.address === nodeB.address &&
        nodeA.port === nodeB.port;
};
ring.hashCode =function (str) {
    if(typeofstr !== 'string')
        str = str.toString();
    varhash = 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 () {
    varrealLength = this.realNodes.length;
    this.nodes.splice(0);//clear all
 
    for(vari = 0; i < this.maxNodes; i++) {
        varrealIndex = Math.floor(i / this.maxNodes * realLength);
        varrealNode = this.realNodes[realIndex];
        varlabel = realNode.address + '#'+
            (i - realIndex * Math.floor(this.maxNodes / realLength));
        varvirtualNode = ring.hashCode(label);
 
        this.nodes.push({
            'hash': virtualNode,
            'label': label,
            'node': realNode
        });
    }
 
    this.nodes.sort(function(a, b){
        returna.hash - b.hash;
    });
};
ring.prototype.select =function (key) {
    if(typeofkey === 'string')
        key = ring.hashCode(key);
    for(vari = 0, len = this.nodes.length; i<len; i++){
        varvirtualNode = this.nodes[i];
        if(key <= virtualNode.hash) {
            console.log(virtualNode.label);
            returnvirtualNode.node;
        }
    }
    console.log(this.nodes[0].label);
    returnthis.nodes[0].node;
};
ring.prototype.add =function (node) {
    this.realNodes.push(node);
 
    this.generate();
};
ring.prototype.remove =function (node) {
    varrealLength = this.realNodes.length;
    varidx = 0;
    for(vari = realLength; i--;) {
        varrealNode = this.realNodes[i];
        if(ring.compareNode(realNode, node)) {
            this.realNodes.splice(i, 1);
            idx = i;
            break;
        }
    }
    this.generate();
};
ring.prototype.toString =function () {
    returnJSON.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:

?
varfs = require('fs');
varpath = require('path');
 
varcfgFileName = 'config.cfg';
varcache = {};
 
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)) {
            varcontents = fs.readFileSync(
                process.env.cloudDriveConfig, {encoding:'utf-8'});
            cache[cfgFileName] = JSON.parse(contents);
        }
    }
    returncache[cfgFileName];
};

分布式Redis 操作

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

?
varhashringUtils = require('../hashringUtils'),
    ring = hashringUtils.ring,
    node = hashringUtils.node;
 
varconfig = require('../configUtils');
 
varnodes = config.getConfigs().nodes;
for(vari = 0, len = nodes.length; i < len; i++) {
    varn = nodes[i];
    nodes[i] =new node({address: n.address, port: n.port});
}
 
varhashingRing = newring(32, nodes);
 
module.exports = hashingRing;
module.exports.openClient =function (id) {
    varnode = hashingRing.select(id);
    varclient = require('redis').createClient(node.port, node.address);
    client.on('error',function (err) {
        console.log('error: '+ err);
    });
    returnclient;
};
module.exports.hgetRedis =function (id, key, callback) {
    varclient = 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) {
    varclient = 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){
    varclient = 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:

?
varcrypto = require('crypto');
varconfig = require('../config/configUtils');
 
varEXPIRES = 20 * 60 * 1000;
varredisMatrix = require('./redisMatrix');
 
varsign = function(val, secret) {
    returnval + '.'+ crypto
        .createHmac('sha1', secret)
        .update(val)
        .digest('base64')
        .replace(/[\/\+=]/g,'');
};
vargenerate = function() {
    varsession = {};
    session.id = (newDate()).getTime() + Math.random().toString();
    session.id = sign(session.id, config.getConfigs().SECRET);
    session.expire = (newDate()).getTime() + EXPIRES;
    returnsession;
};
varserialize = function(name, val, opt) {
    varpairs = [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');
 
    returnpairs.join('; ');
};
 
varsetHeader = function(req, res, next) {
    varwriteHead = res.writeHead;
    res.writeHead =function () {
        varcookies = res.getHeader('Set-Cookie');
        cookies = cookies || [];
        console.log('writeHead, cookies: '+ cookies);
        varsession = 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);
        returnwriteHead.apply(this, arguments);
    };
 
    next();
};
 
exports = module.exports =function session() {
    returnfunction session(req, res, next) {
        varid = req.cookies[config.getConfigs().session_key];
        if(!id) {
            req.session = generate();
            id = req.session.id;
            varjson = 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) {
                varneedChange = true;
                console.log('reply: '+ reply);
                if(reply) {
                    varsession = JSON.parse(reply);
                    if(session.expire > (newDate()).getTime()) {
                        session.expire = (newDate()).getTime() + EXPIRES;
                        req.session = session;
                        needChange =false;
                        varjson = 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
                    varjson = JSON.stringify(req.session);
                    redisMatrix.hsetRedis(id,'session', json,
                        function(err, reply) {
                            setHeader(req, res, next);
                        });
                }
            });
        }
    };
};
 
module.exports.set =function (req, name, val) {
    varid = 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) {
    varid = 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:

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

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

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

小结

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