《Redis入门指南》一5.4 Node.js与Redis

5.4 Node.js与Redis

Redis入门指南
Redis官方推荐的Node.js客户端是node_redis1。

5.4.1 安装

使用npm install redis命令安装最新版本的node_redis,目前版本是0.8.2。

5.4.2 使用方法

首先加载node_redis模块:

var redis = require('redis');

下面的代码将创建一个默认连接到地址127.0.0.1,端口6379的Redis连接:

var client = redis.createClient();

也可以显式地指定需要连接的地址:

var client = redis.createClient('6379', '127.0.0.1')

由于 Node.js 的异步特性,在处理返回值的时候与其他客户端差别较大。还是以GET/SET命令为例:

client.set('foo', 'bar', function () {
   // 此时SET命令执行完并返回结果,
   // 因为这里并不关心SET命令的结果,所以我们省略了回调函数的形参。
   client.get('foo', function (error, fooValue) {
     // error参数存储了命令执行时返回的错误信息,如果没有错误则返回null。
     // 回调函数的第二个参数存储的是命令执行的结果
     console.log(fooValue); // 'bar'
   });
});

使用node_redis执行命令时需要传入回调函数(callback function)来获得返回值,当命令执行完返回结果后node_redis会调用该函数,并将命令的错误信息作为第一个参数、返回值作为第二个参数传递给该函数。关于Node.js的异步模型的介绍超出了本书的范围,有兴趣的读者可以访问Node.js的官网2了解更多信息。

Node.js的异步模型使得通过node_redis调用Redis命令的表现与Redis的底层管道协议十分相似:调用命令函数时(如client.set())并不会等待Redis返回命令执行结果,而是直接继续执行下一条语句,所以在Node.js中通过异步模型就能实现与管道类似的效果(也因此node_redis没有提供管道相关的命令)。上面的例子中我们并不需要SET命令的返回值,只要保证SET命令在GET命令前发出即可,所以完全不用等待SET命令返回结果后再执行GET命令。因此上面的代码可以改写成:

// 不需要返回值时可以省略回调函数
client.set('foo', 'bar');
client.get('foo', function (error, fooValue) {
   console.log(fooValue); // 'bar'
});

不过由于SET和GET并未真正使用Redis的管道协议发送,所以当有多个客户端同时向Redis发送命令时,上例中的两个命令之间可能会被插入其他命令,换句话说,GET命令得到的值未必是“bar”。

虽然Node.js的异步特性给我们带来了相对更高的性能,然而另一方面使用Redis实现某个功能时我们经常需要读写若干个键,而且很多情况下都会依赖之前命令的返回结果。这时就会出现嵌套多重回调函数的情况,影响代码可读性。就像这样:

client.get('people:2:home', function (error, home) {
   client.hget('locations', home, function (error, address) {
     client.exists('address:' + address, function (errror, addressExists) {
        if (addressExists) {
          console.log('地址存在。');
        } else {
          client.exists('backup.address:' + address, function (error,
                  backupAddress Exists) {
            if (backupAddressExists) {
              console.log('备用地址存在。');
            } else {
              console.log('地址不存在。');
            }
          });
        }
     });
   })
});

上面的代码并不是极端的情况,相反在实际开发中经常会遇到这种多层嵌套。为了减少嵌套,可以考虑使用Async3、Step4等第三方模块。如上面的代码可以稍微修改后使用Async重写为:

async.waterfall([
   function (callback) {
     client.get('people:2:home', callback);
   },
   function (home, callback) {
     client.hget('locations', home, callback);
   },
   function (address, callback) {
     async.parallel([
       function (callback) {
          client.exists('address:' + address, callback);
       },
       function (callback) {
          client.exists('backup.address:' + address, callback);
       },
     ], function (err, results) {
       if (results[0]) {
          console.log('地址存在。');
       } else if (results[1]) {
          console.log('备用地址存在。');
       } else {
          console.log('地址不存在。');
       }
     });
   }
]);

5.4.3 简便用法

1.HMSET/HGETALL
node_redis同样支持在HMSET命令中使用对象作参数(对象的属性值只能是字符串),相应的HGETALL命令会返回一个对象。

2.事务
事务的用法如下:

var multi = client.multi();
multi.set('foo', 'bar');
multi.sadd('set', 'a');
mulit.exec(function (err, replies) {
   // replies是一个数组,依次存放事务队列中命令的结果
   console.log(replies);
});

或者使用链式调用:

client.multi()
     .set('foo', 'bar')
     .sadd('set', 'a')
     .exec(function (err, replies) {
       console.log(replies);
     });

3.“发布/订阅”模式
Node.js 使用事件的方式实现“发布/订阅”模式。现在创建两个连接分别充当发布者和订阅者:

var pub = redis.createClient();
var sub = redis.createClient();

然后让sub订阅chat频道:

sub.subscribe('chat');
定义当接收到消息时要执行的回调函数:

sub.on('message', function (channel, message) {
  console.log('收到' + channel + '频道的消息:' + message);
});
在sub订阅成功后,我们让pub向chat频道发送一个问候信息:

sub.on('subscribe', function (channel, count) {
  pub.publish('chat', 'hi!');
})

运行后可以看到打印的结果:

$ node testpubsub.js
收到chat频道的消息:'hi!'

补充知识

在 node_redis 中建立连接的过程同样是异步的,即执行 client = redis.createClient()后并未立即建立连接。在连接建立完成前执行的命令会被加入到离线任务队列中,当连接建立成功后node_redis会按照加入的顺序依次执行离线任务队列中的命令。

5.4.4 实践:IP地址查询

很多场合下网站都需要根据访客的IP地址判断访客所在地。假设我们有一个地名和IP地址段的对应表5:

上海: 202.127.0.0 ~ 202.127.4.255
北京: 122.200.64.0 ~ 122.207.255.255

如果用户的IP地址为122.202.2.0,我们就能根据这个表知道他的地址位于北京。Redis可以使用一个有序集合类型的键来存储这个表。

首先将表中的IP地址转换成十进制数字:

上海: 3397320704 ~ 3397321983
北京: 2059943936 ~ 2060451839

然后使用有序集合类型记录这个表。方式为每个地点存储两条数据:一条的元素值是地点名,分数是该地点对应的最大IP地址。另一条是“*”加上地点名,分数是该地点对应的最小IP地址,如图5-9所示。

在查找某个IP地址属于哪个地点时先将该IP地址转换成十进制数字,然后在有序集合中找到大于该数字的最小的一个元素,如果该元素不是以“*”开头则表示找到了,如果是则表示数据库中并未记录该IP地址对应的地名。

如我们想找到“122.202.2.0”的所在地,首先将其转换成数字“2060059136”,然后在有序集合中找到第一个大于它的分数为“2060451839”,对应的元素值为“北京”,不是以“*”开头,所以该地址的所在地是北京。

下面介绍使用Node.js实现这一过程。首先将表转换成CSV格式并存为ip.csv:

上海,202.127.0.0,202.127.4.255
北京,122.200.64.0,122.207.255.255

而后使用node-csv-parser模块6加载该csv文件:

var fs = require('fs');
var csv = require('csv');
csv().from.stream(fs.createReadStream('ip.csv'))
   .on('record', importIP);

读取每行数据时 node-csv-parser 模块都会调用 importIP 回调函数。该函数实现如下:

var redis = require('redis');
var client = redis.createClient();

// 将IP地址数据加入Redis
// 输入格式:"['上海', '202.127.0.0', '202.127.4.255']"
function importIP (data) {
  var location = data[0];
  var minIP = convertIPtoNumber(data[1]);
  var maxIP = convertIPtoNumber(data[2]);
  // 将数据加入到有序集合中,键名为'ip'
  client.zadd('ip', minIP, '*' + location, maxIP, location);
}

其中convertIPtoNumber函数用来将IP地址转换成十进制数字,

// 将IP地址转换成十进制数字
// convertIPtoNumber('127.0.0.1') => 2130706433
function convertIPtoNumber(ip) {
  var result = '';
  ip.split('.').forEach(function (item) {
   item = ~~item;
   item = item.toString(2);
   item = pad(item, 8);
   result += item;
  });
  return parseInt(result, 2);
}

pad函数用于将二进制数补全为8位:

// 在字符串前补'0'。
// pad('11', 3) => '011'
function pad(num, n) {
   var len = num.length;
   while(len < n) {
     num = '0' + num;
     len++;
   }
   return num;
}

至此数据准备工作完成了,现在我们提供一个接口来供用户查询:

var readline = require('readline');

var rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

rl.setPrompt('IP> ');
rl.prompt();

rl.on('line', function (line) {
  ip = convertIPtoNumber(line);
  client.zrangebyscore('ip', ip, '+inf', 'LIMIT', '0', '1', function (err,result) {
   if (!Array.isArray(result) || result.length === 0) {
    // 该IP地址超出了数据库记录的最大IP地址
    console.log('No data.');
   } else {
    var location = result[0];
    if (location[0] === '*') {
      // 该IP地址不属于任何一个IP地址段
      console.log('No data.');
    } else {
     console.log(location);
    }
   }
   rl.prompt();
  });
});

运行后的结果如下:

$ node ip_search.js
IP> 127.0.0.1
No data.
IP> 122.202.23.34
北京
IP> 202.127.3.3
上海

上面的代码的实际查找范围是一个半开半闭区间。如果想实现闭区间查找,读者可以在比对“*”时同时比较元素的分数和查找的IP地址是否相同。

时间: 2024-08-03 16:08:36

《Redis入门指南》一5.4 Node.js与Redis的相关文章

《Redis入门指南》一第5章 实践

第5章 实践 Redis入门指南 小白把宋老师向自己讲解的知识总结成了一篇帖子发在了学校的网站上,引起了强烈的反响.很多同学希望宋老师能够再写一些关于Redis实践方面的教程,宋老师爽快地答应了. 在此之前我们进行的操作都是通过Redis的命令行客户端redis-cli进行的,并没有介绍实际编程时如何操作Redis.本章将会通过4个实例分别介绍Redis的PHP.Python.Ruby和 Node.js 客户端的使用方法,即使你不了解其中的某些语言,粗浅的阅读一下也能收获很多实践方面的技巧.

《Redis入门指南》一导读

前 言 Redis入门指南Redis如今已经成为Web开发社区中最火热的内存数据库之一,而它的诞生距现在不过才4年.随着Web 2.0的蓬勃发展,网站数据快速增长,对高性能读写的需求也越来越多,再加上半结构化的数据比重逐渐变大,人们对早已被铺天盖地地运用着的关系数据库能否适应现今的存储需求产生了疑问.而Redis的迅猛发展,为这个领域注入了全新的思维. Redis凭借其全面的功能得到越来越多的公司的青睐,从初创企业到新浪微博这样拥有着几百台Redis服务器的大公司,都能看到Redis的身影.Re

《Redis入门指南》一4.3 排序

4.3 排序 Redis入门指南 午后,宋老师正在批改学生们提交的程序,再过几天就会迎来第一次计算机全市联考.他在每个学生的程序代码末尾都用注释详细地做了批注--严谨的治学态度让他备受学生们的爱戴. 一个电话打来."小白的?"宋老师拿出手机,"博客最近怎么样了?"未及小白开口,他就抢先问道. 特别好!现在平均每天都有50多人访问我的博客.不过昨天我收到一个访客的邮件,他向我反映了一个问题:查看一个标签下的文章列表时文章不是按照时间顺序排列的,找起来很麻烦.我看了一下

《Redis入门指南》一5.1 PHP与Redis

5.1 PHP与Redis Redis入门指南 Redis官方推荐的PHP客户端是Predis1和phpredis2.前者是完全使用PHP代码实现的原生客户端,而后者则是使用C语言编写的PHP扩展.在功能上两者区别并不大,就性能而言后者会更胜一筹.考虑到很多主机并未提供安装PHP扩展的权限,本节会以Predis为示例介绍如何在PHP中使用Redis. 虽然Predis的性能逊于phpredis,但是除非执行大量Redis命令,否则很难区分二者的性能.而且实际应用中执行Redis命令的开销更多在网

【实战】基于Nginx、Node.js和Redis的Docker工作流

本文讲的是[实战]基于Nginx.Node.js和Redis的Docker工作流,[编者的话]本文是一篇实践性很强的文章.作者通过一个完整的示例讲述了构建一个基于Nginx.Node.js.Redis的应用服务的Docker流程.推荐所有Docker使用者阅读,并根据文章实践. 在我的前一篇文章中,我已经介绍了关于容器和Docker是如何影响PaaS.微服务和云计算的.如果你刚刚接触Docker和容器,我强烈建议你先读一读我之前的文章.作为之前文章的一个延续,在本文中我仍会讲述一些Docker工

《Redis入门指南》一5.3 Python与Redis

5.3 Python与Redis Redis入门指南 Redis官方推荐的Python客户端是redis-py1. 5.3.1 安装 推荐使用pip install redis安装最新版本的redis-py,也可以使用easy_install:easy_install redis. 5.3.2 使用方法 首先需要引入redis-py: import redis 下面的代码将创建一个默认连接到地址127.0.0.1,端口6379的Redis连接: r = redis.StrictRedis() 也

《Redis入门指南》一4.1 事务

4.1 事务 Redis入门指南 傍晚时候,忙完了一天的教学工作,宋老师坐在办公室的电脑前开始为明天的课程做准备.尽管有着近5年的教学经验,可是宋老师依然习惯在备课时写一份简单的教案.正在网上查找资料时,在浏览器的历史记录里他突然看到了小白的博客.心想:不知道他的博客怎么样了? 于是宋老师点进了小白的博客,页面刚载入完他就被博客最下面的一行大得夸张的文字吸引了:"Powered by Redis".宋老师笑了笑,接着就看到了小白博客中最新的一篇文章: 标题: 使用Redis来存储微博中

《Redis入门指南》一4.4 消息通知

4.4 消息通知 Redis入门指南 凭着小白的用心经营,博客的访问量逐渐增多,甚至有了小白自己的粉丝.这不,小白刚收到一封来自粉丝的邮件,在邮件中那个粉丝强烈建议小白给博客加入邮件订阅功能,这样当小白发布新文章后订阅小白博客的用户就可以收到通知邮件了.在信的末尾,那个粉丝还着重强调了一下:"这个功能对不习惯使用RSS的用户很重要,希望能够加上!" 看过信后,小白心想:"是个好建议!不过话说回来,似乎他还没发现其实我的博客连RSS功能都没有." 邮件订阅功能太好实现

《Redis入门指南》一4.5 管道

4.5 管道 Redis入门指南 客户端和Redis使用TCP协议连接.不论是客户端向Redis发送命令还是Redis向客户端返回命令的执行结果,都需要经过网络传输,这两个部分的总耗时称为往返时延.根据网络性能不同,往返时延也不同,大致来说到本地回环地址(loop back address)的往返时延在数量级上相当于Redis处理一条简单命令(如LPUSH list 1 2 3)的时间.如果执行较多的命令,每个命令的往返时延累加起来对性能还是有一定影响的. 在执行多个命令时每条命令都需要等待上一