天天看點

《Redis實戰》一1.3 你好Redis

本節書摘來異步社群《redis實戰》一書中的第1章,第1.3節,作者: 【美】josiah l. carlson(約西亞 l.卡爾森)譯者: 黃健宏 責編: 楊海玲,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

在對redis提供的5種結構有了基本的了解之後,現在是時候來學習一下怎樣使用這些結構來解決實際問題了。最近幾年,越來越多的網站開始提供對網頁連結、文章或者問題進行投票的功能,其中包括圖1-6展示的reddit以及圖1-7展示的stackoverflow。這些網站會根據文章的釋出時間和文章獲得的投票數量計算出一個評分,然後按照這個評分來決定如何排序和展示文章。本節将展示如何使用redis來建構一個簡單的文章投票網站的後端。

《Redis實戰》一1.3 你好Redis

要建構一個文章投票網站,我們首先要做的就是為了這個網站設定一些數值和限制條件:如果一篇文章獲得了至少200張支援票(up vote),那麼網站就認為這篇文章是一篇有趣的文章;假如這個網站每天釋出1000篇文章,而其中的50篇符合網站對有趣文章的要求,那麼網站要做的就是把這50篇文章放到文章清單前100位至少一天;另外,這個網站暫時不提供投反對票(down vote)的功能。

為了産生一個能夠随着時間流逝而不斷減少的評分,程式需要根據文章的釋出時間和目前時間來計算文章的評分,具體的計算方法為:将文章得到的支援票數量乘以一個常數,然後加上文章的釋出時間,得出的結果就是文章的評分。

我們使用從utc時區1970年1月1日到現在為止經過的秒數來計算文章的評分,這個值通常被稱為unix時間。之是以選擇使用unix時間,是因為在所有能夠運作redis的平台上面,使用程式設計語言擷取這個值都是一件非常簡單的事情。另外,計算評分時與支援票數量相乘的常量為432,這個常量是通過将一天的秒數(86 400)除以文章展示一天所需的支援票數量(200)得出的:文章每獲得一張支援票,程式就需要将文章的評分增加432分。

建構文章投票網站除了需要計算文章評分之外,還需要使用redis結構存儲網站上的各種資訊。對于網站裡的每篇文章,程式都使用一個散列來存儲文章的标題、指向文章的網址、釋出文章的使用者、文章的釋出時間、文章得到的投票數量等資訊,圖1-8展示了一個使用散列來存儲文章資訊的例子。

《Redis實戰》一1.3 你好Redis

使用冒号作為分隔符 本書使用冒号(:)來分隔名字的不同部分:比如圖 1-8 裡面的鍵名article:92617就使用了冒号來分隔單詞article和文章的id号92617,以此來建構命名空間(namespace)。使用:作為分隔符隻是我的個人喜好,不過大部分redis使用者也都是這麼做的,另外還有一些常見的分隔符,如句号(.)、斜線(/),有些人甚至還會使用管道符号(|)。無論使用哪個符号來做分隔符,都要保持分隔符的一緻性。同時,請讀者注意觀察和學習本書使用冒号建立嵌套命名空間的方法。

我們的文章投票網站将使用兩個有序集合來有序地存儲文章:第一個有序集合的成員為文章 id,分值為文章的釋出時間;第二個有序集合的成員同樣為文章 id,而分值則為文章的評分。通過這兩個有序集合,網站既可以根據文章釋出的先後順序來展示文章,又可以根據文章評分的高低來展示文章,圖1-9展示了這兩個有序集合的一個示例。

《Redis實戰》一1.3 你好Redis

為了防止使用者對同一篇文章進行多次投票,網站需要為每篇文章記錄一個已投票使用者名單。為此,程式将為每篇文章建立一個集合,并使用這個集合來存儲所有已投票使用者的id,圖1-10展示了一個這樣的集合示例。

《Redis實戰》一1.3 你好Redis

為了盡量節約記憶體,我們規定當一篇文章釋出期滿一周之後,使用者将不能再對它進行投票,文章的評分将被固定下來,而記錄文章已投票使用者名單的集合也會被删除。

在實作投票功能之前,讓我們來看看圖 1-11:這幅圖展示了當115423号使用者給100408号文章投票的時候,資料結構發生的變化。

《Redis實戰》一1.3 你好Redis

既然我們已經知道了網站計算文章評分的方法,也知道了網站存儲資料所需的資料結構,那麼現在是時候實際地實作這個投票功能了!當使用者嘗試對一篇文章進行投票時,程式需要使用zscore指令檢查記錄文章釋出時間的有序集合,判斷文章的釋出時間是否未超過一周。如果文章仍然處于可以投票的時間範圍之内,那麼程式将使用sadd指令,嘗試将使用者添加到記錄文章已投票使用者名單的集合裡面。如果添加操作執行成功的話,那麼說明使用者是第一次對這篇文章進行投票,程式将使用zincrby指令為文章的評分增加432分(zincrby指令用于對有序集合成員的分值執行自增操作),并使用hincrby指令對散列記錄的文章投票數量進行更新(hincrby指令用于對散列存儲的值執行自增操作),代碼清單1-6展示了投票功能的實作代碼。

代碼清單1-6 article_vote()函數

《Redis實戰》一1.3 你好Redis

redis事務 從技術上來講,要正确地實作投票功能,我們需要将代碼清單1-6裡面的sadd、zincrby和hincrby這3個指令放到一個事務裡面執行,不過因為本書要等到第4章才介紹redis事務,是以我們暫時忽略這個問題。

這個投票功能還是很不錯的,對吧?那麼釋出文章的功能要怎麼實作呢?

釋出一篇新文章首先需要建立一個新的文章id,這項工作可以通過對一個計數器(counter)執行incr指令來完成。接着程式需要使用sadd将文章釋出者的id添加到記錄文章已投票使用者名單的集合裡面,并使用expire指令為這個集合設定一個過期時間,讓redis在文章釋出期滿一周之後自動删除這個集合。之後,程式會使用hmset指令來存儲文章的相關資訊,并執行兩個zadd指令,将文章的初始評分(initial score)和釋出時間分别添加到兩個相應的有序集合裡面。代碼清單1-7展示了釋出新文章功能的實作代碼。

代碼清單1-7 post_article()函數

《Redis實戰》一1.3 你好Redis

好了,我們已經陸續實作了文章投票功能和文章釋出功能,接下來要考慮的就是如何取出評分最高的文章以及如何取出最新釋出的文章了。為了實作這兩個功能,程式需要先使用zrevrange指令取出多個文章id,然後再對每個文章id執行一次hgetall指令來取出文章的詳細資訊,這個方法既可以用于取出評分最高的文章,又可以用于取出最新釋出的文章。這裡特别要注意的一點是,因為有序集合會根據成員的分值從小到大地排列元素,是以使用zrevrange指令,以“分值從大到小”的排列順序取出文章id才是正确的做法,代碼清單1-8展示了文章擷取功能的實作函數。

代碼清單1-8 get_articles()函數

《Redis實戰》一1.3 你好Redis

雖然我們建構的網站現在已經可以展示最新釋出的文章和評分最高的文章了,但它還不具備目前很多投票網站都支援的群組(group)功能:這個功能可以讓使用者隻看見與特定話題有關的文章,比如與“可愛的動物”有關的文章、與“政治”有關的文章、與“java程式設計”有關的文章或者介紹“redis用法”的文章等等。接下來的一節将向我們展示為文章投票網站添加群組功能的方法。

群組功能由兩個部分組成,一個部分負責記錄文章屬于哪個群組,另一個部分負責取出群組裡面的文章。為了記錄各個群組都儲存了哪些文章,網站需要為每個群組建立一個集合,并将所有同屬一個群組的文章id都記錄到那個集合裡面。代碼清單1-9展示了将文章添加到群組裡面的方法,以及從群組裡面移除文章的方法。

代碼清單1-9 add_remove_groups()函數

《Redis實戰》一1.3 你好Redis

初看上去,可能會有讀者覺得使用集合來記錄群組文章并沒有多大用處。到目前為止,讀者隻看到了集合結構檢查某個元素是否存在的能力,但實際上redis不僅可以對多個集合執行操作,甚至在一些情況下,還可以在集合和有序集合之間執行操作。

為了能夠根據評分對群組文章進行排序和分頁(paging),網站需要将同一個群組裡面的所有文章都按照評分有序地存儲到一個有序集合裡面。redis的zinterstore指令可以接受多個集合和多個有序集合作為輸入,找出所有同時存在于集合和有序集合的成員,并以幾種不同的方式來組合(combine)這些成員的分值(所有集合成員的分值都會被視為是1)。對于我們的文章投票網站來說,程式需要使用zinterstore指令選出相同成員中最大的那個分值來作為交內建員的分值:取決于所使用的排序選項,這些分值既可以是文章的評分,也可以是文章的釋出時間。

圖 1-12 展示了對一個包含少量文章的群組集合和一個包含大量文章及評分的有序集合執行zinterstore指令的過程,注意觀察那些同時出現在集合和有序集合裡面的文章是怎樣被添加到結果有序集合裡面的。

《Redis實戰》一1.3 你好Redis

通過對存儲群組文章的集合和存儲文章評分的有序集合執行zinterstore指令,程式可以得到按照文章評分排序的群組文章;而通過對存儲群組文章的集合和存儲文章釋出時間的有序集合執行zinterstore指令,程式則可以得到按照文章釋出時間排序的群組文章。如果群組包含的文章非常多,那麼執行zinterstore指令就會比較花時間,為了盡量減少redis的工作量,程式會将這個指令的計算結果緩存60秒。另外,我們還重用了已有的get_articles()函數來分頁并擷取群組文章,代碼清單1-10展示了網站從群組裡面擷取一整頁文章的方法。

代碼清單1-10 get_group_articles()函數

《Redis實戰》一1.3 你好Redis

有些網站隻允許使用者将文章放在一個或者兩個群組裡面(其中一個是“所有文章”群組,另一個是最适合文章的群組)。在這種情況下,最好直接将文章所在的群組記錄到存儲文章資訊的散列裡面,并在article_vote()函數的末尾增加一個zincrby指令調用,用于更新文章在群組中的評分。但是在這個示例裡面,我們建構的文章投票網站允許一篇文章同時屬于多個群組(比如一篇文章可以同時屬于“程式設計”和“算法”兩個群組),是以對于一篇同時屬于多個群組的文章來說,更新文章的評分意味着程式需要對文章所屬的全部群組執行自增操作。在這種情況下,如果一篇文章同時屬于很多個群組,那麼更新文章評分這一操作可能會變得相當耗時,是以,我們在get_group_articles()函數裡面對zinterstore指令的執行結果進行了緩存處理,以此來盡量減少zinterstore指令的執行次數。開發者在靈活性或限制條件之間的取舍将改變程式存儲和更新資料的方式,這一點對于任何資料庫都是适用的,redis也不例外。

練習:實作投反對票的功能 我們的示例目前隻實作了投支援票的功能,但是在很多實際的網站裡面,反對票也能給使用者提供有用的回報資訊。是以,請讀者能想辦法在article_vote()函數和post_article()函數裡面添加投反對票的功能。除此之外,讀者還可以嘗試為使用者提供對調投票的功能:比如将支援票轉換成反對票,或者将反對票轉換成支援票。提示:如果讀者在實作對調投票功能時出現了困難,可以參考一下第3章介紹的smove指令。

好的,現在我們已經成功地建構起了一個展示最受歡迎文章的網站後端,這個網站可以擷取文章、釋出文章、對文章進行投票甚至還可以對文章進行分組。如果你覺得前面展示的内容不好了解,或者弄不懂這些示例,又或者沒辦法運作本書提供的源代碼,那麼請閱讀下一節來了解如何擷取幫助。