原文地址: http://www.moye.me/?p=565

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


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

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

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

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

  1. Node + Redis 实现分布式Session方案(转载)

    Session是什么? Session 是面向连接的状态信息,是对 Http 无状态协议的补充. Session 怎么工作? Session 数据保留在服务端,而为了标识具体 Session 信息指向 ...

  2. 在centos7中安装redis,并通过node.js操作redis

    引言 最近在学习node.js 连接redis的模块,所以尝试了一下在虚拟机中安装cent OS7,并安装redis,并使用node.js 操作redis.所以顺便做个笔记. 如有不对的地方,欢迎大家 ...

  3. 170222、使用Spring Session和Redis解决分布式Session跨域共享问题

    使用Spring Session和Redis解决分布式Session跨域共享问题 原创 2017-02-27 徐刘根 Java后端技术 前言 对于分布式使用Nginx+Tomcat实现负载均衡,最常用 ...

  4. 使用Spring Session和Redis解决分布式Session跨域共享问题

    http://blog.csdn.net/xlgen157387/article/details/57406162 使用Spring Session和Redis解决分布式Session跨域共享问题

  5. Tornado 自定义session,与一致性哈希 ,基于redis 构建分布式 session框架

    Tornado 自定义session,与一致性哈希 ,基于redis 构建分布式 session import tornado.ioloop import tornado.web from myhas ...

  6. redis生成分布式id方案

    分布式Id - redis方式   本篇分享内容是关于生成分布式Id的其中之一方案,除了redis方案之外还有如:数据库,雪花算法,mogodb(object_id也是数据库)等方案,对于redis来 ...

  7. node.js使用redis储存session(详细步骤)

    转储session的原因 网上有许多session需要用数据库储存的原因,对我来说原因很简单,仅仅只是node的生产环境不允许将session存到服务器的内存中.会报一个内存溢出的风险警告.所以我决定 ...

  8. node.js应用Redis数据库

    node.js下使用Redis,首先: 1.有一台安装了Redis的服务器,当然,安装在本机也行 2.本机,也就是客户端,要装node.js 3.项目要安装nodejs_redis模块 注意第 3 点 ...

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

    出处:http://blog.csdn.net/unityoxb/article/details/8532028 push服务是一项很有用处的技术,它能改善交互,提升用户体验.要实现这项服务通常有两种 ...

随机推荐

  1. HOWTO: InstallScript MSI工程取Log

    InstallShield的各种类型安装包如果遇到安装问题(尤其是在客户安装时遇到问题),获取Log分析是最有效的方法之一. 对于封装一个Setup.exe的InstallScript MSI工程,我 ...

  2. Java的自动装箱和拆箱的简单讲解

     装箱就是把基础类型封装成一个类.比如把int封装成Integer,这时你就不能把他当成一个数了,而是一个类了,对他的操作就需要用它的方法了. 拆箱就是把类转换成基础类型.比如你算个加法什么的是不能用 ...

  3. PHP多次调用Mysql存储过程报错解决办法

    PHP多次调用Mysql数据库的存储过程会出现问题,主要问题为存储过程中执行多次SQL语句不能一一释放导致的,网上找了一些解决办法,比如使用 multi_query 然后一个一个释放,但是发现根本不适 ...

  4. Difference between LET and LET* in Common LISP

    Difference between LET and LET* in Common LISP   LET   Parallel binding which means the bindings com ...

  5. 【Cocos2d-Js基础教学(2)类的使用和面向对象】

    类的使用和面向对象 大家都知道在cocos2d-x 底层是C++编写的,那么就有类的概念和继承机制. 但是在JS中,是没有类这个概念的,没有提供类,没有C++的类继承机制. 那么JS是通过什么方式实现 ...

  6. Spring源码追踪2——xml解析入口

    解析xml节点入口 org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDe ...

  7. html5网页动画总结--jQuery旋转插件jqueryrotate

    CSS3 提供了多种变形效果,比如矩阵变形.位移.缩放.旋转和倾斜等等,让页面更加生动活泼有趣,不再一动不动.然后 IE10 以下版本的浏览器不支持 CSS3 变形,虽然 IE 有私有属性滤镜(fil ...

  8. Could not find the Visual SourceSafe Internet Web Service connection information for the specified database Would you like to launch the Visual sourceSafe connection wizard?

    今天同事遇到个奇怪问题,以前也遇到过,不过重新绑定一下就OK了,不知道为什么今天不行了. 错误提示:===============================================Cou ...

  9. Java IO 之 OutputStream源码

    Writer      :BYSocket(泥沙砖瓦浆木匠) 微         博:BYSocket 豆         瓣:BYSocket FaceBook:BYSocket Twitter   ...

  10. Idea 201601注册码

    参考链接 http://blog.csdn.net/u010310183/article/details/51162137 在license 下面输入 http://www.iteblog.com/i ...