zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

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

RedisJSNode入门 指南 5.4
2023-09-11 14:17:30 时间

本节书摘来异步社区《Redis入门指南》一书中的第5章,第5.4节,作者: 李子骅 责编: 杨海玲,更多章节内容可以访问云栖社区“异步社区”公众号查看。

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所示。


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地址是否相同。


随着互联网业务对性能需求日益强烈,作为Key/Value存储的Redis具有数据类型丰富和性能表现优异的特点。如果能够熟练地驾驭它,不管是把它用做缓存还是存储,对很多大型应用都很多帮助。新浪作为世界上最大的Redis使用者,体会到了Redis为高并发在线业务带来的好处,但同时也遇到了很多挑战,新浪为推动Redis这种NoSQL产品在中国互联网产品技术架构中的使用做出了卓越的贡献。
《Redis入门指南》一导读 Redis如今已经成为Web开发社区中最火热的内存数据库之一,而它的诞生距现在不过才4年。随着Web 2.0的蓬勃发展,网站数据快速增长,对高性能读写的需求也越来越多,再加上半结构化的数据比重逐渐变大,人们对早已被铺天盖地地运用着的关系数据库能否适应现今的存储需求产生了疑问。
异步社区 异步社区(www.epubit.com)是人民邮电出版社旗下IT专业图书旗舰社区,也是国内领先的IT专业图书社区,致力于优质学习内容的出版和分享,实现了纸书电子书的同步上架,于2015年8月上线运营。公众号【异步图书】,每日赠送异步新书。