Node + Redis 實現分布式Session方案(轉載)
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》):
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方案(轉載)