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('');
});

  

原网址(http://www.moye.me/2014/09/28/%E5%88%86%E5%B8%83%E5%BC%8Fsession/)

Node + Redis 实现分布式Session方案(转载)的更多相关文章

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

    原文地址: http://www.moye.me/?p=565 Session是什么? Session 是面向连接的状态信息,是对 Http 无状态协议的补充. Session 怎么工作? Sessi ...

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

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

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

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

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

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

  5. redis生成分布式id方案

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

  6. 可扩容分布式session方案

    分布式session有以下几种方案: 1. 基于nfs(net filesystem)的session共享 将共享服务器目录mount各服务器的本地session目录,session读写受共享服务器i ...

  7. 使用 Redis 实现分布式锁(转载)

    背景 在一般的分布式应用中,要安全有效地同步多服务器多进程之间的共享资源访问,就要涉及到分布式锁.目前项目是基于 Tornado 实现的分布式部署,同时也使用了 Redis 作为缓存.参考了一些资料并 ...

  8. 基于redis实现分布式Session

    学习到好的知识还是需要记录下来的. 开发环境 asp.net mvc4,iis.asp.net 自带的session机制存在诸多不好的地方.先只要列出几点. asp.net mvc 默认的sessio ...

  9. 【Redis】分布式Session

    一.问题引出 1.1 Session的原理 1.2 问题概述 二.解决方案 三.代码实现-使用Token代替Session 3.1 Service 3.2 TokenController 一.问题引出 ...

随机推荐

  1. JavaScript Practices

    不定时更新:https://github.com/zhengyeye/JS-practices Day1:关于创建对象的几种方式: Day3:原型 早前一篇:https://www.cnblogs.c ...

  2. 使用 Java 程序写文件时,记得要 flush()

    使用 Java 程序往磁盘写文件时碰到了这样的问题:文件写不全. 假如内容(StringBuffer/StringBuilder)有 100W 个字符,但是通过 Java 程序写到文件里的却不到 10 ...

  3. Failed while changing version of Java to 1.8.

    在使用eclipse导入一个新的项目时,项目-->鼠标右键-->Properties 弹出框中修改Project Facets为1.8时,eclipse报错,错误信息如下: <spa ...

  4. python基础类型—字典

    字典 字典是python中唯一的映射类型,采用键值对(key-value)的形式存储数据.python对key进行哈希函数运算,根据计算的结果决定value的存储地址,所以字典是无序存储的,且key必 ...

  5. git纯净提交代码(只提交自己改过的文件)

    添加远程仓库,这个远程仓库是要进行发起合并请求的仓库,简单来说就是项目的主要代码库,不是自己派生的代码库 git remote add main http://xxx  从远端仓库下载新分支与数据gi ...

  6. gym102007 E

    我计划预习五个小时离散,然后hmc补了这道他自认为非常的裸并且很傻逼自己可以一眼秒的简单题,然后给我讲了讲,然后我失去了一整晚的生命迹象. 首先我们可以发现一个神奇的现象,啊,先排个序,然后我们会发现 ...

  7. 支持多文件上传,预览,拖拽,基于bootstrap的上传插件fileinput 的ajax异步上传(转载)

    首先需要导入一些js和css文件 <link href="__PUBLIC__/CSS/bootstrap.css" rel="stylesheet"&g ...

  8. 阿里云 ECS 迁移到七牛 QVM 记

    操作 下载脚本 curl -O http://p70nwjoid.bkt.clouddn.com/go2qvm_client1.5_linux_x86_64.zip 解压填写配置 unzip go2q ...

  9. Android adb调试

    1.首先是adb 修改 devices/amlogic/p201_iptv/system.prop service.adb.tcp.port=5555 或者直接在盒子串口下修改 system/buil ...

  10. java中的静态代理和动态代理

    1.动态代理的定义:为其他对象提供一个代理以控制对这个对象的访问 代理类主要负责委托类的预处理消息,过滤消息,把消息传给委托类以及消息事后处理 按照代理类的创建时期,代理类可以分为2种:静态代理类(在 ...