文章投票網站的redis相關實現

需求:

要構建一個文章投票網站,文章須要在一天內至少得到200張票,才能優先顯示在當天文章列表前列。python

可是爲了不發佈時間較久的文章因爲累計的票數較多而一直停留在文章列表前列,咱們須要有隨着時間流逝而不斷減分的評分機制。git

因而具體的評分計算方法爲:將文章獲得的支持票數乘以一個常量432(由一天的秒數86400除以文章展現一天所需的支持票200得出),而後加上文章的發佈時間,得出的結果就是文章的評分。github

Redis設計

(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()

代碼地址

https://github.com/NancyLin/r...

相關文章
相關標籤/搜索