[译]Redis Cookbook(3.2)

用Redis的发布订阅功能创建一个聊天系统

问题

         利用Redis的发布订阅功能,加上Node.js和Socket.IO,创建一个轻量级的实时聊天系统。

解决方案

     Redis原生支持发布订阅模型,我们可以很容易地把它跟Node.js和Socket.IO结合在一起,快速地创建一个实时聊天系统。

         发布订阅模型定义了接收者和触发者的工作方式,接收者订阅符合指定模式的消息,触发者把消息发送到消息云。当一个消息到达云,订阅这种类型消息的客户端会收到这个消息。这种模式降低了触发者和接收者之间的耦合关系——它们不需要知道对方的细节,只要他们能够以这种方式发送消息或者接收消息。

         为了更好地理解发布订阅模型,可以参考wiki页。

         Redis直接支持发布订阅模型,也就是说,它允许客户端订阅满足指定模式的频道,也可以向指定的频道发布消息。比如,我们可以很容易地为汽车相关话题创建"chat:cars"频道,为食物相关话题创建"chat:sausage"频道。频道的名字跟Redis的键空间没有关系,所以不用担心会跟已有的键存在冲突。Redis通过以下命令支持发布订阅功能:

        PUBLISH    发布到指定频道
        SUBSCRIBE    订阅指定频道
        UNSUBSCRIBE    取消订阅指定频道
        PSUBSCRIBE    订阅符合指定模式的频道
        PUNSUBSCRIBE    取消订阅符合指定模式的频道

         有了这些命令,可以很容易地创建一个聊天系统或一个通知系统。发布订阅模型甚至可以被用于构建队列系统,不过先让我们来看看如何实现聊天系统吧。

         在服务器端,Node.js和Socket.IO负者处理网络层,Redis作为发布订阅模型的实现,在客户端之间转发消息。在客户端,我们借助jQuery来处理消息,或者发送数据到服务端。

讨论

         我们假设你最近安装过Node.js,还有npm,因为我们需要用它来安装聊天系统所需要的node依赖包。我们先从如何安装聊天系统所需要的软件开始,再过一下服务端及客户端的代码。

安装所需软件

         我们先用npm来安装所需要的软件包:

npm install socket.io
npm install redis

实现服务端代码

         在服务器端,我们会运行一个Redis,并且创建一个js文件,然后用Node.js来执行它。这段代码主要是发起到Redis的连接,同时监听来自客户端的连接端口。让我们来看看代码:

var http = require('http'),
io = require('socket.io'),
redis = require('redis'),
rc = redis.createClient();

         这几行代码引入了我们所需要的包,并且创建了一个变量,我们会用它来访问Redis跟Socket.IO。我们会用"redis"这个变量访问Redis,而变量"io"允许我们访问所有连接到服务器的socket。

         下一步我们要对我们的代码做一些修改,启动一个HTTP服务,Socket.IO会基于这个发挥它的魔力。

server = http.createServer(function(req, res){
    // we may want to redirect a client that hits this page
    // to the chat URL instead
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end('<h1>Hello world</h1>');
});
// Set up our server to listen on 8000 and serve socket.io
server.listen(8000);
var socketio = io.listen(server);

         如果你有Node.js或Socket.IO的使用经验,这段代码对你来说应该很简单。它启动了一个HTTP服务,指定了处理请求的方式,对8000端口进行监听,并粘附了Socket.IO,这样就可以处理来自Socket.IO的请求,同时开启了websocket功能。

         现在我们再加进去一点Redis代码来完善我们的功能。用Node.js写的Redis客户端必须订阅聊天频道,并处理来自频道的消息。

// if the Redis server emits a connect event, it means we're ready to work,
// which in turn means we should subscribe to our channels. Which we will.
rc.on("connect", function() {
    rc.subscribe("chat");
    // we could subscribe to more channels here
});
// When we get a message in one of the channels we're subscribed to,
// we send it over to all connected clients.
rc.on("message", function (channel, message) {
    console.log("Sending: " + message);
    socketio.sockets.emit('message', message);
})

         正如你所看到的,Redis代码超级简单。我们所要做的就是监听来自特定频道的消息,当有消息到来,我们就把它广播到所有客户端。

客户端代码实现

         完成了服务端的代码,接下来我们要创建一个页面来连接到Node.js。我们要先在客户端把Socket.IO设置好,然后处理进出消息。

         我们开始创建页面,这里是页面的主要代码:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Chat with Redis</title>
    </head>
    <body>
        <ul id="messages">
            <!-- chat messages go here -->
        </ul>
    </body>
</html>

         为了完成功能,现在我们要把jQuery和Socket.IO包含进来。我们会先从Google CDN去获取,然后是Node.js服务器。把这两行插入到页面的head区间:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
<script src="http://localhost:8000/socket.io/socket.io.js"></script>

         现在可以开始连接到Node.js,并监听和处理消息了。把下面的代码加到head区间:

<script>
    var socket = io.connect('localhost', { port: 8000});
    socket.on('message', function(data){
         var li = new Element('li').insert(data);
         $('messages').insert({top: li});
    }
</script>

         这段js代码让客户端Socket.IO连接到Node.js的8000端口,并监听消息事件。当一个消息到达,它会创建一个列表元素,并添加到之前已经存在的无序列表里。要注意,代码很简单,所以聊天界面是很难看的,不过要让它变好看也不难。

         我们还差一个表单,还有就是如何把消息从客户端发送到服务端。这个需要Socket.IO发送功能的支持,之前在服务器端已经用过了。

<form id="chatform" action="">
    <input id="chattext" type="text" value="" />
    <input type="submit" value="Send" />
</form>
<script>
    $('#chatform').submit(function() {
        socket.emit('message', $('chattext').val());
        $('chattext').val(""); // cleanup the field
        return false;
    });
</script>

         当用户填好表单并按下发送按钮,jQuery将使用socket变量往服务端发送一个消息事件,这个消息会被广播到其它客户端。脚本里最后一个"return false"语句是为了防止表单被提交,因为Socket.IO已经处理了这部分内容。

更多改进

         到目前为止,我们使用Node.js和Socket.IO创建了聊天系统的主要部分,不过还可以从很多方面对我们的代码进行改进。比如,我们可以在消息里使用包含用户名或头像元数据的JSON片段,而不是一般的字符串。我们也可以在服务器端做一些改进,比如使用多个频道,允许通过模式匹配订阅多个频道。Redis的发布订阅模型让实现聊天或通知系统变得很容易。

用Redis实现倒排索引文本搜索

问题

         倒排索引是一种索引数据结构,它存储的是单词到它们所在位置的映射。位置指的是单词在文件里,文档里,或数据库里的位置。它一般被用来实现全文搜索,不过需要事先对文档建立索引。

         在这一章节,我们将使用Redis作为后端存储来实现一个全文搜索。

解决方案

         我们会为每一个单词使用一个集合,集合里包含文档的ID。为了让搜索更快,我们会事先为所有文档建立索引。在查找的时候,输入的关键字被分割成单词,然后进行集合的交集操作,返回所有包含这些单词的文档ID。

讨论

建立索引

         假设我们有成百上千的文档或web页面。第一步是对它们建立索引,我们要把文本分割成单词,还要把无用词以及长度低于3的单词排除掉。我们用Ruby脚本来做:

def id_for_document(filename)
    doc_id= $redis.hget("documents", filename)
    if doc_id.nil?
        doc_id= $redis.incr("next_document_id")
        $redis.hset("documents", filename, doc_id)
        $redis.hset("filena
    end
    STOP_WORDS = ["the", "of", "to", "and", "a", "in", "is","it", "you", "that"]
    f = File.open(filename)
    doc_id= id_for_document(filename)
    f.each_line do |l|
        l.strip.split(/ |,|\)|\(|\;|mes", doc_id, filename)
    end
    doc_id\./).each do |word|
        continue if word.size <= 3 || STOP_WORDS.include?(word)
        add_word(word, doc_id)
    end
end

        我们把需要加到索引的单词过滤了出来,并且为每个文档生成了唯一ID。我们还需要一个增加索引的函数:

def add_word(word, doc_id)
    $redis.sadd("word:#{word}", doc_id)
end

         对文档里的每一个单词,我们创建了相应的集合,集合里包含了可以找到这个单词的文档ID。

查找

         倒排索引的查找速度很快,因为大部分工作在为文档建立索引的时候都做掉了。查找的时候只需要对集合进行并集操作。下面的代码使用redis-rb来执行Redis命令:

def search(*terms)
    document_ids = $redis.sinter(*terms.map{|t| "word:#{t}"})
    $redis.hmget("filenames", *document_ids)
end

分数排名

         上面的实现很简单,也有局限性,不过对它进行扩展也很容易。我们可以对文档设置分数,当返回查找结果的时候,我们可以根据分数来判断一些情况:更高的分数意味结果更匹配,或者说明被搜索的频度越高。让我们对索引函数做一些修改:

def add_word(word, doc_id)
    $redis.zincrby("word:#{word}", 1, doc_id)
end

         查找会复杂一些:

def search(*terms)
    document_ids = $redis.multi do
        $redis.zinterstore("temp_set", terms.map{|t|"word:#{t}"})
        $redis.zrevrange("temp_set", 0, -1)
    end.last
    $redis.hmget("filenames", *document_ids)
end

         注意代码里的multi函数,因为在temp_set集合里可能存在潜在的并发问题。当有两个或多个命令要对数据进行更新,而它们更新的数据会被其它进程访问,并且要求这些命令在被访问之前完成,那么就会存在竞态条件问题。

         为了在并行查找的时候避免这种竞态条件,我们要么使用Redis的MULTI/EXEC命令,要么为每一次查找操作生成唯一的键。

         MUTLI和EXEC命令让Redis具备了事务能力。出现在MULTI/EXEC代码块中的命令保证被串行执行,也就是说,在代码块执行期间不会处理其它的客户端请求。就这个例子来说,temp_set的竞态条件问题被消除了,因为其它客户端无法在ZINTERSTORE和ZREVRANGE操作期间对相关的值进行修改。在一个事务里使用DISCARD会让事务结束,事务里的所有命令会被取消,然后返回到正常状态。

         事务里的命令只在调用EXEC之后才开始被执行,也只有在这个时候,才会收到所有命令的执行结果。因此,在同一个事务里,一个命令无法使用上一个命令的执行结果。不过要想达到这种效果,可以使用WATCH命令。

         redis-rb里没有显式的EXEC调用。multi函数的开头和结尾就标记了事务的开始和结束,在代码块结束的地方,redis-rb会隐式地调用EXEC。

其它改进

         搜索功能还有很多可以改进的地方。

大小写敏感

         我们可以让搜索功能对大小写不敏感,所以在建立索引的时候对单词进行统一的大小写转换,在查找的时候也需要对关键字进行转换。

模糊查找

         也许你也会对实现模糊查找感兴趣。这需要考虑到一些经常发生的拼写错误的情况。在我们的例子里,我们需要在建立索引的时候把拼写错误的关键字也进行索引。

部分匹配

         这个很有用,不过会增加内存的使用,而且可能会多出一些不想要的搜索结果。为了达到这个目的,我们需要获取单词的子字符串,然后对它们进行索引。比如为了给matching建立索引,需要把这些都加到索引里: matching,mat,matc,match,matchi,matchin。

         前提是已经确定最小的字符串长度是3,而且至少已经对单词的开头部分进行了匹配。如果你想得到所有可能的组合,需要对单词的其它子串进行索引。

          用有序集合来实现部分匹配和模糊匹配很管用。你可以给不完整和拼写错误的单词设置低一点的分数,这样让搜索得到的结果更准确。

转载于:https://my.oschina.net/xuemingdeng/blog/739776

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值