要构建一个文章投票网站,文章须要在一天内至少得到200张票,才能优先显示在当天文章列表前列。python
可是为了不发布时间较久的文章因为累计的票数较多而一直停留在文章列表前列,咱们须要有随着时间流逝而不断减分的评分机制。git
因而具体的评分计算方法为:将文章获得的支持票数乘以一个常量432(由一天的秒数86400除以文章展现一天所需的支持票200得出),而后加上文章的发布时间,得出的结果就是文章的评分。github
(1)对于网站里的每篇文章,须要使用一个散列来存储文章的标题、指向文章的网址、发布文章的用户、文章的发布时间、文章获得的投票数量等信息。redis
为了方便网站根据文章发布的前后顺序和文章的评分高低来展现文章,咱们须要两个有序集合来存储文章:
(2)有序集合,成员为文章ID,分值为文章的发布时间。缓存
(3)有序集合,成员为文章ID,分值为文章的评分。数据结构
(4)为了防止用户对同一篇文章进行屡次投票,须要为每篇文章记录一个已投票用户名单。使用集合来存储已投票的用户ID。因为集合是不能存储多个相同的元素的,因此不会出现同个用户对同一篇文章屡次投票的状况。app
(5)文章支持群组功能,可让用户只看见与特定话题相关的文章,好比“python”有关或者介绍“redis”的文章等,这时,咱们须要一个集合来记录群组文章。例如 programming群组函数
为了节约内存,当一篇文章发布期满一周以后,用户将不能对它进行投票,文章的评分将被固定下来,而记录文章已投票用户名单的集合也会被删除。post
1.当用户要发布文章时,
(1)经过一个计数器counter执行INCR命令来建立一个新的文章ID。
(2)使用SADD将文章发布者ID添加到记录文章已投票用户名单的集合中,并用EXPIRE命令为这个集合设置一个过时时间,让Redis在文章发布期满一周后自动删除这个集合。
(3)使用HMSET命令来存储文章的相关信息,并执行两ZADD命令,将文章的初始评分和发布时间分别添加到两个相应的有序集合中。测试
import time # 截止时间,一周 ONE_WEEK_IN_SECONDS = 7 * (24 * 60 * 60) # 计分常量 VOTE_SCORE = 432 """ 发布文章 @param {object} @param {string} 用户 @param {string} 文章title @param @return {string} 文章id """ def postArticle(conn, user, title, link): # 建立一个新的文章ID article_id = str(conn.incr('article:')) # 将文章发布者ID添加到记录文章已投票用户名单的集合中,并用EXPIRE为这个集合设置过时时间 voted = 'voted:' + article_id conn.sadd(voted, user) conn.expire(voted, ONE_WEEK_IN_SECONDS) now = time.time() # 用HMSET存储文章的相关信息 article = 'article:' + article_id conn.hmset(article, { 'title': title, 'link': link, 'poster': user, 'time': now, 'votes': 1 }) # 执行两个ZADD,将文章的初始评分与发布时间添加到两个相应的有序集合中 conn.zadd('time:', article, now) conn.zadd('score:', article, now + VOTE_SCORE) return article_id
2.当用户尝试对一篇文章进行投票时,
(1)用ZSCORE命令检查记录文章发布时间的有序集合(redis设计2),判断文章的发布时间是否未超过一周。
(2)若是文章仍然处于能够投票的时间范畴,那么用SADD将用户添加到记录文章已投票用户名单的集合(redis设计4)中。
(3)若是上一步操做成功,那么说明用户是第一次对这篇文章进行投票,那么使用ZINCRBY命令为文章的评分增长432(ZINCRBY命令用于对有序集合成员的分值执行自增操做);
并使用HINCRBY命令对散列记录的文章投票数量进行更新
""" 用户投票功能 @param {object} @param {string} 用户 @param {string} 文章 """ def voteArticle(conn, user, article): # 判断文章是否超过了投票截止时间,若是超过,则不容许投票 outoff = time.time() - ONE_WEEK_IN_SECONDS if conn.zscore('time:', article) < outoff: return article_id = article.partition(':')[-1] # 将用户添加到记录已投票用户名单的集合中 if conn.sadd('voted:' + article_id, user): # 增长该文章的评分和投票数量 conn.zincrby('score:', article, VOTE_SCORE) conn.hincrby(article, 'votes', 1)
3.咱们已经实现了文章投票功能和文章发布功能,接下来就要考虑如何取出评分最高的文章以及如何取出最新发布的文章
(1)咱们须要使用ZREVRANGE命令取出多个文章ID。(因为有序集合会根据成员的分值从小到大地排列元素,使用ZREVRANGE以分值从大到小的排序取出文章ID)
(2)对每一个文章ID执行一次HGETALL命令来取出文章的详细信息。
这个方法既能够用于取出评分最高的文章,又能够用于取出最新发布的文章。
""" 取出评分最高的文章,或者最新发布的文章 @param {object} @param {int} 页码 @param {string} 有序集合名称,能够是score:,time: @return array """ # 每页的文章数 ARTICLES_PER_PAGE = 25 def getArticles(conn, page, order = 'score:'): # 获取指定页码文章的起始索引和结束索引 start = (page - 1) * ARTICLES_PER_PAGE end = start + ARTICLES_PER_PAGE - 1 # 取出指定位置的文章id article_ids = conn.zrevrange(order, start, end) articles = [] for id in article_ids: article_data = conn.hgetall(id) article_data['id'] = id articles.append(article_data) return articles
4. 对文章进行分组,用户能够只看本身感兴趣的相关主题的文章。
群组功能主要有两个部分:一是负责记录文章属于哪一个群组,二是负责取出群组中的文章。
为了记录各个群组都保存了哪些文章,须要为每一个群组建立一个集合,并将全部同属一个群组的文章ID都记录到那个集合中。
""" 添加移除文章到指定的群组中 @param {object} @param {int} 文章ID @param {array} 添加的群组 @param {array} 移除的群组 """ def addRemoveGroups(conn, article_id, to_add = [], to_remove = []): article = 'article:' + article_id # 添加文章到群组中 for group in to_add: conn.sadd('group:' + group, article) # 从群组中移除文章 for group in to_remove: conn.srem('group:' + group, article)
因为咱们还须要根据评分或者发布时间对群组文章进行排序和分页,因此须要将同一个群组中的全部文章按照评分或者发布时间有序地存储到一个有序集合中。
但咱们已经有全部文章根据评分和发布时间的有序集合,咱们不须要再从新保存每一个群组中相关有序集合,咱们能够经过取出群组文章集合与相关有序集合的交集,就能够获得各个群组文章的评分和发布时间的有序集合。
Redis的ZINTERSTORE命令能够接受多个集合和多个有序集合做为输入,找出全部同时存在于集合和有序集合的成员,并以几种不一样的方式来合并这些成员的分值(全部集合成员的分支都会视为1)。
对于文章投票网站来讲,可使用ZINTERSTORE命令选出相同成员中最大的那个分值来做为交集成员的分值:取决于所使用的排序选项,这些分值既能够是文章的评分,也能够是文章的发布时间。
以下的示例图,显示了执行ZINTERSTORE命令的过程:
对集合groups:programming和有序集合score:进行交集计算得出了新的有序集合score:programming,它包含了全部同时存在于集合groups:programming和有序集合score:的成员。由于集合groups:programming的全部成员分值都被视为1,而有序集合score:的全部成员分值都大于1,此次交集计算挑选出来的分值为相同成员中的最大分值,因此有序集合score:programming的成员分值其实是由有序集合score:的成员的分值来决定的。
因此,咱们的操做以下:
(1)经过群组文章集合和评分的有序集合或发布时间的有序集合执行ZINTERSTORE命令,而获得相关的群组文章有序集合。
(2)若是群组文章不少,那么执行ZINTERSTORE须要花费较多的时间,为了尽可能减小redis的工做量,咱们将查询出的有序集合进行缓存处理,尽可能减小ZINTERSTORE命令的执行次数。
为了保持持续更新后咱们能获取到最新的群组文章有序集合,咱们只将结果缓存60秒。
(3)使用上一步的getArticles函数来分页并获取群组文章。
""" 根据评分或者发布时间对群组文章进行排序和分页 @param {object} @param {int} 文章ID @param {array} 添加的群组 @param {array} 移除的群组 """ def getGroupArticles(conn, group, page, order = 'score:'): # 群组有序集合名 key = order + group if not conn.exists(key): conn.zinterstore(key, ['group:' + group, order], aggregate = 'max') conn.expire(key, 60) return getArticles(conn, page, key)
以上就是一个文章投票网站的相关redis实现。
测试代码以下:
import unittest class TestArticle(unittest.TestCase): """ 初始化redis链接 """ def setUp(self): import redis self.conn = redis.Redis(db=15) """ 删除redis链接 """ def tearDown(self): del self.conn print print """ 测试文章的投票过程 """ def testArticleFunctionality(self): conn = self.conn import pprint # 发布文章 article_id = str(postArticle(conn, 'username', 'A titile', 'http://www.baidu.com')) print "我发布了一篇文章,id为:", article_id print self.assertTrue(article_id) article = 'article:' + article_id # 显示文章保存的散列格式 print "文章保存的散列格式以下:" article_hash = conn.hgetall(article) print article_hash print self.assertTrue(article) # 为文章投票 voteArticle(conn, 'other_user', article) print '咱们为该文章投票,目前该文章的票数:' votes = int(conn.hget(article, 'votes')) print votes print self.assertTrue(votes > 1) print '当前得分最高的文章是:' articles = getArticles(conn, 1) pprint.pprint(articles) print self.assertTrue(len(articles) >= 1) # 将文章推入到群组 addRemoveGroups(conn, article_id, ['new-group']) print "咱们将文章推到新的群组,其余文章包括:" articles = getGroupArticles(conn, 'new-group', 1) pprint.pprint(articles) print self.assertTrue(len(articles) >= 1) 测试结束,删除全部的数据结构 to_del = ( conn.keys('time:*') + conn.keys('voted:*') + conn.keys('score:*') + conn.keys('articles:*') + conn.keys('group:*') ) if to_del: conn.delete(*to_del) if __name__ == '__main__': unittest.main()