004.在一个简单的投票网站中使用Redis

要构建一个文章投票网站,我们首先要做的就是为了这个网站设置一些数值和限制条件:

  1. 如果一篇文章获得了至少200张支持票,那么网站就认为这篇文章是一篇有趣的文章
  2. 假如这个网站每天发布1000篇文章,而其中的50篇符合网站对有趣文章的要求,那么网站要做的就是把这50篇文章放到文章列表前100位至少一天;
  3. 另外,这个网站暂时不提供投反对票的功能

(直观上的第一方案是,记录每篇文章的投票数,然后按照投片进行排序,这样带来的问题是历史投票数最多的文章永远在前面。)


为了产生一个能够随着时间流逝而不断减少的评分,程序需要根据文章的发布时间和当前时间来计算文章的评分,具体的计算方法为:将文章得到的支持票数量乘以一个常量,然后加上文章的发布时间,得出的结果就是文章的评分。

我们使用从UTC时区1970年1月1日到现在为止经过的秒数来计算文章的评分,这个值通常被称为Unix时间。之所以选择使用Unix时间,是因为在所有能够运行Redis的平台上面,使用编程语言获取这个值都是一件非常简单的事情。

另外,计算评分时与支持票数量相乘的常量为432,这个常量是通过将一天的秒数除以文章展示一天所需的支持票数量得出的:文章每获得一张支持票,程序就需要将文章的评分增加432分。


构建文章投票网站除了需要计算文章评分之外,还需要使用Redis结构存储网站上的各种信息。对于网站里的每篇文章,程序都使用一个散列来存储文章的标题、指向文章的网址、发布文章的用户、文章的发布时间、文章得到的投票
数量等信息,

2021-10-25-19-32-18

我们的文章投票网站将使用两个有序集合来有序地存储文章:第一个有序集合的成员为文章ID,分值为文章的发布时间;第二个有序集合的成员同样为文章ID,而分值则为文章的评分。通过这两个有序集合,网站既可以根据文章发布的先后顺序来展示文章,又可以根据文章评分的高低来展示文章。

2021-10-25-19-32-31


为了防止用户对同一篇文章进行多次投票,网站需要为每篇文章记录一个已投票用户名单。为此,程序将为每篇文章创建一个集合,并使用这个集合来存储所有已投票用户的ID。

2021-10-25-19-32-44

为了尽量节约内存,我们规定当一篇文章发布期满一周之后,用户将不能再对它进行投票,文章的评分将被固定下来,而记录文章已投票用户名单的集合也会被删除。


当用户尝试对一篇文章进行投票时,程序需要使用ZSCORE命令检查记录文章发布时间的有序集合,判断文章的发布时间是否未超过一周。如果文章仍然处于可以投票的时间范围之内,那么程序将使用SADD命令,尝试将用户添加到记录文章已投票用户名单的集合里面。如果添加操作执行成功的话,那么说明用户是第一次对这篇文章进行投票,程序将使用ZINCRBY命令为文章的评分增加432分(ZINCRBY命令用于对有序集合成员的分值执行自增操作),并使用HINCRBY命令对散列记录的文章投票数量进行更新(HINCRBY命令用于对散列存储的值执行自增操作)。


从技术上来讲,要正确地实现投票功能,我们需要SADD ZINCRBY和HINCRBY这3个命令放到一个事务里面执行。

小结

功能设计

  1. 如果一篇文章获得了200个赞,则认为其是一个有趣的文章。
  2. 如果网站一天发布1000篇文章,其中50篇满足有趣的文章的定义,则需要将这50篇文章放在文章前100中至少一天。(这个地方就是将昨天的有趣文章,排在列表中)
  3. 不提供反对票的功能。
  4. 文章发布超过一周,则不支持投票。
  5. 不允许用户对同一篇文章进行多次投票。
  6. 支持按时间排序。
  7. 支持按评分排序。
  8. 可以查看文章的详细信息。
  9. 需要有对文章进行分组的功能

数据结构设计

Redis类型:zset
键为:time:
成员为:article:文章编号
评分为:文章发布时间戳

Redis类型:zset
键为:score:
成员为:article:文章编号
评分为:文章的评分

上面的zset可以实现文章按时间排序,按评分排序。

Redis类型:hash
键为:article:文章编号
散列键为: title、link、poster、time、votes、groups

上面的hash可以用于存储文章的详细信息。

Redis类型:set
键为:voted:文章编号

上面的set可以用于记录某篇文章已经投票的用户信息,防止用户多次投递。

Redis类型:set
键为:group:群组名称
值为:article:文章编号

算法设计

用户发表文章时,将发布时间记为文章的初始评分。每有一个用户点赞,则评分加432。当点赞数超过200时,则文章将会比24小时候发布的文章的评分高,所以排名也就可以会比较靠前。

投票流程

用户10001对文章20001进行投票

  1. 使用zscore time:artile:20001获取文章的发布时间,判断是否能够进行投票。
  2. 使用sadd voted:20001判断该用户是否已经投递过了,如果未投递,则该操作返回true。
  3. 使用zincrby scored: artile:20001 1,为文章增加积分
  4. 使用hincrby article:20001 votes 1,为文章增加投票数

发布文章流程

用户10001发布文章20001

  1. 使用incr获取一个文章的Id:20001
  2. 使用sadd voted:20001 user:10001,将发布文章的用户添加到已投票的集合中
  3. 使用sadd groups:群组名称 article:20001,将文章添加到某个分组中(执行多次)
  4. 使用expirevoted:20001设置一个过期时间,因为文章发布超过一周就不可以投票了
  5. 使用hmset article:20001 title '' link ''存储文章的信息
  6. 使用zadd timed: article:20001,记录文章的发布时间
  7. 使用zadd scored: article:20001,记录文章的评分

取出评分最高的文章

  1. 使用zrevrange取出多个文章id
  2. 对上面步骤获取的id使用hgetall id

取出最新发布的文章

  1. 使用zrevrange取出多个文章id
  2. 对上面步骤获取的id使用hgetall id

根据评分对群组文章进行排序和分页

  1. 使用zinterstore命令接受groups:群组名score:,找出同时存在于集合和有序集合中得成员
  2. 在执行zinterstore时,需要注意分值合并的方式