5.4 Node.js 与 Redis
Redis官方推荐的Node.js客户端是node_redis①。
注释:①见https://github.com/mranney/node_redis。
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的官网①了解更多信息。
注释:①见http://nodejs.org。
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('地址不存在。');
}
});
}
});
})
});
上面的代码并不是极端的情况,相反在实际开发中经常会遇到这种多层嵌套。为了减少嵌套,可以考虑使用Async②、Step③等第三方模块。如上面的代码可以稍微修改后使用Async重写为:
注释:②见https://github.com/caolan/async。
注释:③见https://github.com/creationix/step。
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地址段的对应表①:
注释:①该表只用于演示用途,其中的数据并不准确。
上海: 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所示。
图5-9 使用有序集合键存储地点和相应IP范围的存储结构
在查找某个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模块①加载该csv文件:
注释:①见https://github.com/wdavidw/node-csv-parser。安装方法为npm install 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地址是否相同。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论