最近一些粉丝和学员对兔哥说:兔哥,你的Redis讲的挺好的。但是我更想知道如何解决实际问题? 因此兔哥决定从今天开始每周写一两篇Redis实战系列
最近几年,越来越多的网站开始提供对网页链接、文章或问题进行投票的功能。网站会根据文章的发布时间和文章获得的投票计算出一个评分,然后按照这个评分来决定如何排序和展示文章。
本篇就展示如何使用Redis来够建一个简单的文章投票网站的后端。
1 对文章进行投票
要构建一个文章投票网站,我们首先要做的就是为了这个网站设置一些数值和限制条件∶如果一篇文章获得了至少200 张支持票,那么网站就认为这篇文章是一篇有趣的文章;假如这个网站每天发布 1000篇文章,而其中的50篇符合网站对有趣文章的要求,那么网站要做的就是把这 50 篇文章放到文章列表前 100 位至少一天;另外,这个网站暂时不提供投反对票的功能。
为了产生一个能够随着时间流逝而不断减少的评分,程序需要根据文章的发布时间和当前时间来计算文章的评分,具体的计算方法为∶将文章得到的支持票数量乘以一个常量,然后加上文章的发布时间,得出的结果就是文章的评分。
我们使用从 UTC 时区 1970年1月1日到现在为止经过的秒数来计算文章的评分,这个值通常被称为 Unix时间。之所以选择使用 Unix时间,是因为在所有能够运行Redis的平台上面,使用编程语言获取这个值都是一件非常简单的事情。另外,计算评分时与支持票数量相乘的常量为 432,这个常量是通过将一天的秒数(86 400)除以文章展示一天所需的支持票数量(200)得出的∶文章每获得一张支持票,程序就需要将文章的评分增加 432 分。
构建文章投票网站除了需要计算文章评分之外,还需要使用Redis 结构存储网站上的各种信息。对于网站里的每篇文章,程序都使用一个散列来存储文章的标题、指向文章的网址、发布文章的用户、文章的发布时间、文章得到的投票数量等信息,下图一个使用hash来存储文章信息的例子
我们的文章投票网站将使用两个有序集合来有序地存储文章∶第一个有序集合的成员为文章ID,分值为文章的发布时间;第二个有序集合的成员同样为文章ID,而分值则为文章的评分。通过这两个有序集合,网站既可以根据文章发布的先后顺序来展示文章,又可以根据文章评分的高低来展示文章,下图 展示了这两个有序集合的一个示例。
为了防止用户对同一篇文章进行多次投票,网站需要为每篇文章记录一个已投票用户名单。为此,程序将为每篇文章创建一个集合,并使用这个集合来存储所有已投票用户的ID,下图展示了一个这样的集合示例。
在实现投票功能之前,让我们来看看下图∶这幅图展示了当 115423 号用户给 100408 号文章投票的时候,数据结构发生的变化。
我们已经知道了网站计算文章评分的方法,也知道了网站存储数据所需的数据结构,那么现在是时候实际地实现这个投票功能了!当用户尝试对一篇文章进行投票时,程序需要使用zScORE 命令检查记录文章发布时间的有序集合,判断文章的发布时间是否未超过一周。如果文章仍然处于可以投票的时间范围之内,那么程序将使用SADD命令,尝试将用户添加到记录文章已投票用户名单的集合里面。如果添加操作执行成功的话,那么说明用户是第一次对这篇文章进行投票,程序将使用ZINCRBY命令为文章的评分增加432分(ZINCRBY 命令用于对有序集合成员的分值执行自增操作),并使用HINCRBY命令对散列记录的文章投票数量进行更新(HINCRBY命令用于对散列存储的值执行自增操作)。伪代码如下
Redis 事务 从技术上来讲,要正确地实现投票功能,我们需要将代码 里面的 SADD、 zINCRBY和HINCRBY这3个命令放到一个事务里面执行,不过因为后续文章才会介绍Redis事务,所以我们暂时忽略这个问题。
这个投票功能还是很不错的,对吧?那么发布文章的功能要怎么实现呢?
2 发布并获取文章
发布一篇新文章首先需要创建一个新的文章 ID,这项工作可以通过对一个计数器(counter)执行 INCR 命令来完成。接着程序需要使用 SADD将文章发布者的 ID 添加到记录文章已投票用户名单的集合里面,并使用ExPIRE 命令为这个集合设置一个过期时间,让Redis在文章发布期满一周之后自动删除这个集合。之后,程序会使用 HMSET 命令来存储文章的相关信息,并执行两个zADD命令,将文章的初始评分(initia score)和发布时间分别添加到两个相应的有序集合里面。下面的代码展示了发布新文章功能。
好了,我们已经陆续实现了文章投票功能和文章发布功能,接下来要考虑的就是如何取出评分最高的文章以及如何取出最新发布的文章了。为了实现这两个功能,程序需要先使用 zREVRANGE命令取出多个文章ID,然后再对每个文章ID执行一次HGETALL命令来取出文章的详细信息,这个方法既可以用于取出评分最高的文章,又可以用于取出最新发布的文章。这里特别要注意的一点是,因为有序集合会根据成员的分值从小到大地排列元素,所以使用 zREVRANGE 命令,以"分值从大到小"的排列顺序取出文章ID才是正确的做法,下边的展示了文章获取功能的实现函数。
虽然我们构建的网站现在已经可以展示最新发布的文章和评分最高的文章了,但它还不具备目前很多投票网站都支持的群组(group)功能∶这个功能可以让用户只看见与特定话题有关的文章,比如与"并发编程"有关的文章、与"Python"有关的文章、与"Java编程"有关的文章或者介绍"Redis 用法"的文章等等。接下来兔哥展示为文章投票网站添加群组功能的方法。
3 对文章进行分组
群组功能由两个部分组成,一个部分负责记录文章属于哪个群组,另一个部分负责取出群组里面的文章。为了记录各个群组都保存了哪些文章,网站需要为每个群组创建一个集合,并将所有同属一个群组的文章 ID 都记录到那个集合里面。下面的代码展示了将文章添加到群组里面的方法,以及从群组里面移除文章的方法。
初看上去,可能会有人觉得使用集合来记录群组文章并没有多大用处。到目前为止,大家只看到了集合结构检查某个元素是否存在的能力,但实际上Redis 不仅可以对多个集合执行操作,甚至在一些情况下,还可以在集合和有序集合之间执行操作。 为了能够根据评分对群组文章进行排序和分页(paging ),网站需要将同一个群组里面的所有文章都按照评分有序地存储到一个有序集合里面。Redis 的 ZINTERSTORE 命令可以接受多个集合和多个有序集合作为输入,找出所有同时存在于集合和有序集合的成员,并以几种不同的方式来合并(combine)这些成员的分值(所有集合成员的分值都会被视为是 1)。对于我们的文章投票网站来说,程序需要使用ZTNTERSTORE 命令选出相同成员中最大的那个分值来作为交集成员的分值∶取决于所使用的排序选项,这些分值既可以是文章的评分,也可以是文章的发布时间。
下图一下对一个包含少量文章的群组集合和一个包含大量文章及评分的有序集合执行 zINTERSTORE 命令的过程,注意观察那些同时出现在集合和有序集合里面的文章是怎样被添加到结果有序集合里面的。
上图对集合groups∶programming 和有序集合score∶进行交集计算得出了新的有序集合 score∶programming,它包含了所有同时存在于集合groups∶programming和有序集合score∶的成员。因为集合groups∶progzamming的所有成员的分值都被视为是1,而有序集合score∶的所有成员的分值都大于1,并且这次交集计算挑选的分值为相同成员中的最大分值,所以有序集合 score∶programming的成员的分值实际上是由有序集合score∶的成员的分值来决定的
通过对存储群组文章的集合和存储文章评分的有序集合执行zINTERSTORE 命令,程序可以得到按照文章评分排序的群组文章;而通过对存储群组文章的集合和存储文章发布时间的有序集合执行ZINTERSTORE 命令,程序则可以得到按照文章发布时间排序的群组文章。如果群组包含的文章非常多,那么执行 zINTERSTORE命令就会比较花时间,为了尽量减少Redis的工作量,程序会将这个命令的计算结果缓存60秒。另外,我们还重用了已有的get_articles()函数来分页并获取群组文章,下面的代码展示一下网站从群组里面获取一整页文章的方法。
有些网站只允许用户将文章放在一个或者两个群组里面(其中一个是"所有文章"群组,另一个是最适合文章的群组)。在这种情况下,最好直接将文章所在的群组记录到存储文章信息的散列里面,并在 article_vote()函数的末尾增加一个zINCRBY命令调用,用于更新文章在群组中的评分。但是在这个示例里面,我们构建的文章投票网站允许一篇文章同时属于多个群组(比如一篇文章可以同时属于"编程"和"算法"两个群组),所以对于一篇同时属于多个群组的文章来说,更新文章的评分意味着程序需要对文章所属的全部群组执行自增操作。在这种情况下,如果一篇文章同时属于很多个群组,那么更新文章评分这一操作可能会变得相当耗时,因此,我们在 get_group_articles()函数里面对zINTERSTORE命令的执行结果进行了缓存处理,以此来尽量减少zINTERSTORE 命令的执行次数。开发者在灵活性或限制条件之间的取舍将改变程序存储和更新数据的方式,这一点对于任何数据库都是适用的,Redis 也不例外。
好的,现在我们已经成功地构建起了一个展示最受欢迎文章的网站后端,这个网站可以获取文章、发布文章、对文章进行投票甚至还可以对文章进行分组。如果你觉得前面展示的内容不好理解,或者没弄懂这些示例,都可以留言或加兔哥微信:yangfujie0214
练习∶实现投反对票的功能 尚敏示例目前只实现了投支持票的功能,但是在很多实际的网站里面,反对票也能给用户提供有用的反馈信息。因此,请想办法在article_vote()函数和post_article()函数里面添加投反对票的功能。除此之外,大家还可以尝试为用户提供对调投票的功能∶比如将支持票转换成反对票,或者将反对票转换成支持票。提示。如果读者在实现对调投票功能时出现了困难,可以参考一下Redis 的 SMOVE 命令。