在路上

 找回密码
 立即注册
在路上 站点首页 学习 查看内容

NodeJS+Redis实现分布式Session方案

2016-12-20 13:17| 发布者: zhangjf| 查看: 455| 评论: 0

摘要: Session是什么? Session 是面向连接的状态信息,是对 Http 无状态协议的补充。 Session 怎么工作? Session 数据保留在服务端,而为了标识具体 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》):

  1. var setHeader = function (req, res, next) {
  2. var writeHead = res.writeHead;
  3. res.writeHead = function () {
  4. var cookies = res.getHeader('Set-Cookie');
  5. cookies = cookies || [];
  6. console.log('writeHead, cookies: ' + cookies);
  7. var session = serialize('session_id', req.session.id);
  8. cookies = Array.isArray(cookies) ? cookies.concat(session) :
  9. [cookies, session];
  10. res.setHeader('Set-Cookie', cookies);
  11. return writeHead.apply(this, arguments);
  12. };
  13. next();
  14. };
复制代码

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

Hashing Ring

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

Hashing Ring

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

我的哈希环实现( hashringUtils.js:

  1. var INT_MAX = 0x7FFFFFFF;
  2. var node = function (nodeOpts) {
  3. nodeOpts = nodeOpts || {};
  4. if (nodeOpts.address) this.address = nodeOpts.address;
  5. if (nodeOpts.port) this.port = nodeOpts.port;
  6. };
  7. node.prototype.toString = function () {
  8. return this.address + ':' + this.port;
  9. };
  10. var ring = function (maxNodes, realNodes) {
  11. this.nodes = [];
  12. this.maxNodes = maxNodes;
  13. this.realNodes = realNodes;
  14. this.generate();
  15. };
  16. ring.compareNode = function (nodeA, nodeB) {
  17. return nodeA.address === nodeB.address &&
  18. nodeA.port === nodeB.port;
  19. };
  20. ring.hashCode = function (str) {
  21. if (typeof str !== 'string')
  22. str = str.toString();
  23. var hash = 1315423911, i, ch;
  24. for (i = str.length - 1; i >= 0; i--) {
  25. ch = str.charCodeAt(i);
  26. hash ^= ((hash << 5) + ch + (hash >> 2));
  27. }
  28. return (hash & INT_MAX);
  29. };
  30. ring.prototype.generate = function () {
  31. var realLength = this.realNodes.length;
  32. this.nodes.splice(0); //clear all
  33. for (var i = 0; i < this.maxNodes; i++) {
  34. var realIndex = Math.floor(i / this.maxNodes * realLength);
  35. var realNode = this.realNodes[realIndex];
  36. var label = realNode.address + '#' +
  37. (i - realIndex * Math.floor(this.maxNodes / realLength));
  38. var virtualNode = ring.hashCode(label);
  39. this.nodes.push({
  40. 'hash': virtualNode,
  41. 'label': label,
  42. 'node': realNode
  43. });
  44. }
  45. this.nodes.sort(function(a, b){
  46. return a.hash - b.hash;
  47. });
  48. };
  49. ring.prototype.select = function (key) {
  50. if (typeof key === 'string')
  51. key = ring.hashCode(key);
  52. for(var i = 0, len = this.nodes.length; i<len; i++){
  53. var virtualNode = this.nodes[i];
  54. if(key <= virtualNode.hash) {
  55. console.log(virtualNode.label);
  56. return virtualNode.node;
  57. }
  58. }
  59. console.log(this.nodes[0].label);
  60. return this.nodes[0].node;
  61. };
  62. ring.prototype.add = function (node) {
  63. this.realNodes.push(node);
  64. this.generate();
  65. };
  66. ring.prototype.remove = function (node) {
  67. var realLength = this.realNodes.length;
  68. var idx = 0;
  69. for (var i = realLength; i--;) {
  70. var realNode = this.realNodes[i];
  71. if (ring.compareNode(realNode, node)) {
  72. this.realNodes.splice(i, 1);
  73. idx = i;
  74. break;
  75. }
  76. }
  77. this.generate();
  78. };
  79. ring.prototype.toString = function () {
  80. return JSON.stringify(this.nodes);
  81. };
  82. module.exports.node = node;
  83. module.exports.ring = ring;
复制代码
配置

配置信息是需要根据环境而变化的,某些情况下它又是不能公开的(比如Session_id 加密用的私钥),所以需要一个类似的配置文件( config.cfg:

  1. {
  2. "session_key": "session_id",
  3. "SECRET": "myapp_moyerock",
  4. "nodes":
  5. [
  6. {"address": "127.0.0.1", "port": "6379"}
  7. ]
  8. }
复制代码

在Node 中序列化/反序列化JSON 是件令人愉悦的事,写个配置读取器也相当容易(configUtils.js:

  1. var fs = require('fs');
  2. var path = require('path');
  3. var cfgFileName = 'config.cfg';
  4. var cache = {};
  5. module.exports.getConfigs = function () {
  6. if (!cache[cfgFileName]) {
  7. if (!process.env.cloudDriveConfig) {
  8. process.env.cloudDriveConfig = path.join(process.cwd(), cfgFileName);
  9. }
  10. if (fs.existsSync(process.env.cloudDriveConfig)) {
  11. var contents = fs.readFileSync(
  12. process.env.cloudDriveConfig, {encoding: 'utf-8'});
  13. cache[cfgFileName] = JSON.parse(contents);
  14. }
  15. }
  16. return cache[cfgFileName];
  17. };
复制代码
分布式Redis 操作

有了上述的基础设施,实现一个分布式 Redis 分配器就变得相当容易了。为演示,这里只简单提供几个操作 Hashes 的方法(redisMatrix.js:

  1. var hashringUtils = require('../hashringUtils'),
  2. ring = hashringUtils.ring,
  3. node = hashringUtils.node;
  4. var config = require('../configUtils');
  5. var nodes = config.getConfigs().nodes;
  6. for (var i = 0, len = nodes.length; i < len; i++) {
  7. var n = nodes[i];
  8. nodes[i] = new node({address: n.address, port: n.port});
  9. }
  10. var hashingRing = new ring(32, nodes);
  11. module.exports = hashingRing;
  12. module.exports.openClient = function (id) {
  13. var node = hashingRing.select(id);
  14. var client = require('redis').createClient(node.port, node.address);
  15. client.on('error', function (err) {
  16. console.log('error: ' + err);
  17. });
  18. return client;
  19. };
  20. module.exports.hgetRedis = function (id, key, callback) {
  21. var client = hashingRing.openClient(id);
  22. client.hget(id, key, function (err, reply) {
  23. if (err)
  24. console.log('hget error:' + err);
  25. client.quit();
  26. callback.call(null, err, reply);
  27. });
  28. };
  29. module.exports.hsetRedis = function (id, key, val, callback) {
  30. var client = hashingRing.openClient(id);
  31. client.hset(id, key, val, function (err, reply) {
  32. if (err)
  33. console.log('hset ' + key + 'error: ' + err);
  34. console.log('hset [' + key + ']:[' + val + '] reply is:' + reply);
  35. client.quit();
  36. callback.call(null, err, reply);
  37. });
  38. };
  39. module.exports.hdelRedis = function(id, key, callback){
  40. var client = hashingRing.openClient(id);
  41. client.hdel(id, key, function (err, reply) {
  42. if (err)
  43. console.log('hdel error:' + err);
  44. client.quit();
  45. callback.call(null, err, reply);
  46. });
  47. };
复制代码
分布式Session操作

session_id 的事务和 分布式的Redis都有了,分布式的 Session 操作呼之欲出(sessionUtils.js:

  1. var crypto = require('crypto');
  2. var config = require('../config/configUtils');
  3. var EXPIRES = 20 * 60 * 1000;
  4. var redisMatrix = require('./redisMatrix');
  5. var sign = function (val, secret) {
  6. return val + '.' + crypto
  7. .createHmac('sha1', secret)
  8. .update(val)
  9. .digest('base64')
  10. .replace(/[/+=]/g, '');
  11. };
  12. var generate = function () {
  13. var session = {};
  14. session.id = (new Date()).getTime() + Math.random().toString();
  15. session.id = sign(session.id, config.getConfigs().SECRET);
  16. session.expire = (new Date()).getTime() + EXPIRES;
  17. return session;
  18. };
  19. var serialize = function (name, val, opt) {
  20. var pairs = [name + '=' + encodeURIComponent(val)];
  21. opt = opt || {};
  22. if (opt.maxAge) pairs.push('Max-Age=' + opt.maxAge);
  23. if (opt.domain) pairs.push('Domain=' + opt.domain);
  24. if (opt.path) pairs.push('Path=' + opt.path);
  25. if (opt.expires) pairs.push('Expires=' + opt.expires);
  26. if (opt.httpOnly) pairs.push('HttpOnly');
  27. if (opt.secure) pairs.push('Secure');
  28. return pairs.join('; ');
  29. };
  30. var setHeader = function (req, res, next) {
  31. var writeHead = res.writeHead;
  32. res.writeHead = function () {
  33. var cookies = res.getHeader('Set-Cookie');
  34. cookies = cookies || [];
  35. console.log('writeHead, cookies: ' + cookies);
  36. var session = serialize(config.getConfigs().session_key, req.session.id);
  37. console.log('writeHead, session: ' + session);
  38. cookies = Array.isArray(cookies) ? cookies.concat(session) : [cookies, session];
  39. res.setHeader('Set-Cookie', cookies);
  40. return writeHead.apply(this, arguments);
  41. };
  42. next();
  43. };
  44. exports = module.exports = function session() {
  45. return function session(req, res, next) {
  46. var id = req.cookies[config.getConfigs().session_key];
  47. if (!id) {
  48. req.session = generate();
  49. id = req.session.id;
  50. var json = JSON.stringify(req.session);
  51. redisMatrix.hsetRedis(id, 'session', json,
  52. function () {
  53. setHeader(req, res, next);
  54. });
  55. } else {
  56. console.log('session_id found: ' + id);
  57. redisMatrix.hgetRedis(id, 'session', function (err, reply) {
  58. var needChange = true;
  59. console.log('reply: ' + reply);
  60. if (reply) {
  61. var session = JSON.parse(reply);
  62. if (session.expire > (new Date()).getTime()) {
  63. session.expire = (new Date()).getTime() + EXPIRES;
  64. req.session = session;
  65. needChange = false;
  66. var json = JSON.stringify(req.session);
  67. redisMatrix.hsetRedis(id, 'session', json,
  68. function () {
  69. setHeader(req, res, next);
  70. });
  71. }
  72. }
  73. if (needChange) {
  74. req.session = generate();
  75. id = req.session.id; // id need change
  76. var json = JSON.stringify(req.session);
  77. redisMatrix.hsetRedis(id, 'session', json,
  78. function (err, reply) {
  79. setHeader(req, res, next);
  80. });
  81. }
  82. });
  83. }
  84. };
  85. };
  86. module.exports.set = function (req, name, val) {
  87. var id = req.cookies[config.getConfigs().session_key];
  88. if (id) {
  89. redisMatrix.hsetRedis(id, name, val, function (err, reply) {
  90. });
  91. }
  92. };
  93. /*
  94. get session by name
  95. @req request object
  96. @name session name
  97. @callback your callback
  98. */
  99. module.exports.get = function (req, name, callback) {
  100. var id = req.cookies[config.getConfigs().session_key];
  101. if (id) {
  102. redisMatrix.hgetRedis(id, name, function (err, reply) {
  103. callback(err, reply);
  104. });
  105. } else {
  106. callback();
  107. }
  108. };
  109. module.exports.getById = function (id, name, callback) {
  110. if (id) {
  111. redisMatrix.hgetRedis(id, name, function (err, reply) {
  112. callback(err, reply);
  113. });
  114. } else {
  115. callback();
  116. }
  117. };
  118. module.exports.deleteById = function (id, name, callback) {
  119. if (id) {
  120. redisMatrix.hdelRedis(id, name, function (err, reply) {
  121. callback(err, reply);
  122. });
  123. } else {
  124. callback();
  125. }
  126. };
复制代码
结合 Express 应用

在 Express 中只需要简单的 use 就可以了( app.js:

  1. var session = require('../sessionUtils');
  2. app.use(session());
复制代码

这个被引用的 session 模块暴露了一些操作 session 的方法,在需要时可以这样使用:

  1. app.get('/user', function(req, res){
  2. var id = req.query.sid;
  3. session.getById(id, 'user', function(err, reply){
  4. if(reply){
  5. //Some thing TODO
  6. }
  7. });
  8. res.end('');
  9. });
复制代码
小结

虽然本文提供的是基于 Express 的示例,但基于哈希算法和缓存设施的分布式思路,其实是放之四海而皆准的

来源:Ye Mo

最新评论

小黑屋|在路上 ( 蜀ICP备15035742号-1 

;

GMT+8, 2025-7-12 13:45

Copyright 2015-2025 djqfx

返回顶部