1. 程式人生 > >Node + Redis 實現分布式Session方案(轉載)

Node + Redis 實現分布式Session方案(轉載)

getc address max-age red amp use 查詢 repl -o

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方案(轉載)