NodeJS+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(''); });
小結
雖然本文提供的是基於 Express 的示例,但基於雜湊演算法和快取設施的分散式思路,其實是放之四海而皆準的
相關文章
- 分散式系統Session 實現方式分散式Session
- SpringSession系列-分散式 session 實現方案及 SpringSession 功能分析SpringGseSession分散式
- java web 中分散式 session 的實現JavaWeb分散式Session
- 分散式鎖的實現方案分散式
- redis實現分散式id方案Redis分散式
- HttpServletRequestWrapper模擬實現分散式SessionHTTPServletAPP分散式Session
- 分散式中使用 Redis 實現 Session 共享(上)分散式RedisSession
- 分散式中使用 Redis 實現 Session 共享(中)分散式RedisSession
- 分散式中使用 Redis 實現 Session 共享(下)分散式RedisSession
- 許可權處理 - 用redis實現分散式session~ (cookie && session )Redis分散式SessionCookie
- 分散式鎖實現方案(REDIS,ZOOKEEPER,TAIR)分散式RedisAI
- Redis實現分散式鎖的幾種方案Redis分散式
- Session分散式共享 = Session + Redis + NginxSession分散式RedisNginx
- 如何通過J2Cache實現分散式session儲存分散式Session
- 分散式中灰度方案實踐分散式
- 聊聊Seata分散式解決方案AT模式的實現原理分散式模式
- 實現分散式鎖分散式
- 分散式鎖實現分散式
- 基於Seata探尋分散式事務的實現方案分散式
- 分散式鎖----Redis實現分散式Redis
- Redis實現分散式鎖Redis分散式
- 分散式鎖及其實現分散式
- Redis分散式實現原理Redis分散式
- LightDB分散式實現分散式
- 分散式鎖的實現分散式
- memcached 分散式實現原理分散式
- MinIO分散式叢集的擴充套件方案及實現分散式套件
- 分散式事務之最終一致性實現方案分散式
- 分散式方案求解.分散式
- 「分散式」實現分散式鎖的正確姿勢?!分散式
- Elasticsearch系列---實現分散式鎖Elasticsearch分散式
- Redis之分散式鎖實現Redis分散式
- 分散式鎖之Redis實現分散式Redis
- memcached分散式原理與實現分散式
- 分散式鎖之Zookeeper實現分散式
- 分散式鎖實現(二):Zookeeper分散式
- Redisson實現分散式鎖---原理Redis分散式
- Redisson實現分散式鎖—RedissonLockRedis分散式