Session是什么?
Session 是面向连接的状态信息,是对 Http 无状态协议的补充。
Session 怎么工作?
Session 数据保留在服务端,而为了标识具体 Session 信息指向哪个连接,需要客户端传递向服务端发送一个连接标识,比如存在Cookies 中的session_id值(也可以通过URL的QueryString传递),服务端根据这个id 存取状态信息。
在服务端存储 Session,可以有很多种方案:
- 内存存储
- 数据库存储
- 分布式缓存存储
分布式Session
随着网站规模(访问量/复杂度/数据量)的扩容,针对单机的方案将成为性能的瓶颈,分布式应用在所难免。所以,有必要研究一下 Session 的分布式存储。
如前述, Session使用的标识其实是客户端传递的 session_id,在分布式方案中,一般会针对这个值进行哈希,以确定其在 hashing ring 的存储位置。
Session_id
在 Session 处理的事务中,最重要的环节莫过于 客户端与服务端 关于 session 标识的传递过程:
- 服务端查询客户端Cookies 中是否存在 session_id
- 有session_id,是否过期?过期了需要重新生成;没有过期则延长过期
- 没有 session_id,生成一个,并写入客户端的 Set-Cookie 的 Header,这样下一次客户端发起请求时,就会在 Request Header 的 Cookies带着这个session_id
比如我用 Express, 那么我希望这个过程是自动完成的,不需要每次都去写 Response Header,那么我需要这么一个函数(摘自朴灵的《深入浅出Node.js》):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
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:
1 2 3 4 5 6 7 8 |
{ "session_key": "session_id", "SECRET": "myapp_moyerock", "nodes": [ {"address": "127.0.0.1", "port": "6379"} ] } |
在Node 中 序列化/反序列化JSON 是件令人愉悦的事,写个配置读取器也相当容易(configUtils.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
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:
1 2 |
var session = require('../sessionUtils'); app.use(session()); |
这个被引用的 session 模块暴露了一些操作 session 的方法,在需要时可以这样使用:
1 2 3 4 5 6 7 8 9 |
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 的示例,但基于哈希算法和缓存设施的分布式思路,其实是放之四海而皆准的 🙂
碉堡了
谢谢~
最近在想怎么弄Redis集群 这篇文章如雪中送炭 谢谢 龙哥
不客气
Cool!
Tks
确实很厉害~~~