ItGo.me - 专注IT技术分享

首页 > Redis > NodeJS+Redis实现分布式Session方案

NodeJS+Redis实现分布式Session方案

时间:2016-08-28来源:网友分享 点击:

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', );cookies = Array.isArray(cookies) ? cookies.concat(session) :[cookies, session];res.setHeader('Set-Cookie', cookies);return writeHead.apply(this, arguments);};next();};

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


Hashing Ring

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

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

我的哈希环实现( :

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 allfor (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 是件令人愉悦的事,写个配置读取器也相当容易(:

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 的方法(:

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);('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('../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 = {};= (new Date()).getTime() + Math.random().toString();= sign(, 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, );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 = ;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 = ; // id need changevar 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 就可以了( :

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

NodeJS+Redis实现分布式Session方案

NodeJS+Redis实现分布式Session方案  讨论


Redis客户端 Lettuce

Lettuce是一个可伸缩线程安全的Redis客户端。多个线程可以共享同一个RedisConnection。它利用优秀netty NIO框架来高效地管理多个连接。 项目主页: Redis客户端 Lettuce Redis客户端 Lettuce讨论...

初识redis

redis是个存储服务,能够支持k-v等结构,数据能落地(memcache的数据是内存数据,无法落地) 下面进入redis的世界来一探究竟。 命令行进入redis: 用ps aux | grep redis看下redis-server是否开启,对应...

VB.NET 操作 Redis 数据

本代码介绍在 VB.NET 环境下如何使用 Redis 来存取数据 标签: 代码片段 (3) 1. [文件] RedisExample01_demo.zip~236KB(47) 2. [文件] RedisExample01_src.zip~736KB(38) 3. [代码] [ASP/Basic]代码 跳至 Imports ServiceStack.Redis...

Session是什么? Session是面向连接的状态信息,是对 Http 无状态协议的补充。 Session 怎么工作? Session 数据保留在服务端,而为了标识具体 Session 信息指向哪个连接,需要客户端传递向服务端发
------分隔线----------------------------