NodeJS+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,那么我需要这么一个函数(摘自朴灵的《深入浅出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 的示例,但基于哈希算法和缓存设施的分布式思路,其实是放之四海而皆准的
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值