NOSQL數據庫淺析(三):Redis

   Redis是一個遠程內存數據庫,它不僅性能強勁,而且還具有複製特性以及爲解決問題而生的獨一無二的數據模型。Redis提供了5種不同類型的數據結構,各式各樣的問題都可以很自然地映射到這些數據結構上:Redis的數據結構致力於幫助用戶解決問題,而不會像其他數據庫那樣,要求用戶扭曲問題來適應數據庫。除此之外,通過複製、持久化(persistence)和客戶端分片(client-side sharding)等特性,用戶可以很方便地將Redis擴展成一個能夠包含數百GB數據、每秒處理上百萬次請求的系統。


在其他編程語言裏面使用Redis 本書只展示了使用Python語言編寫的示例代碼,使用Ruby、Java和JavaScript(Node.js)編寫的示例代碼可以在這裏找到:https://github.com/josiahcarlson/redis-in-action。使用Spring框架的讀者可以通過查看http://www.springsource.org/spring-data/redis來學習如何在Spring框架中使用Redis。

1.1 Redis簡介

前面對於Redis數據庫的描述只說出了一部分真相。Redis是一個速度非常快的非關係數據庫(non-relational database),它可以存儲鍵(key)與5種不同類型的值(value)之間的映射(mapping),可以將存儲在內存的鍵值對數據持久化到硬盤,可以使用複製特性來擴展讀性能,還可以使用客戶端分片來擴展寫性能,接下來的幾節將分別介紹Redis的這幾個特性。

1.1.1 Redis與其他數據庫和軟件的對比

如果你熟悉關係數據庫,那麼你肯定寫過用來關聯兩個表的數據的SQL查詢。而Redis則屬於人們常說的NoSQL數據庫或者非關係數據庫:Redis不使用表,它的數據庫也不會預定義或者強制去要求用戶對Redis存儲的不同數據進行關聯。

高性能鍵值緩存服務器memcached也經常被拿來與Redis進行比較:這兩者都可用於存儲鍵值映射,彼此的性能也相差無幾,但是Redis能夠自動以兩種不同的方式將數據寫入硬盤,並且Redis除了能存儲普通的字符串鍵之外,還可以存儲其他4種數據結構,而memcached只能存儲普通的字符串鍵。這些不同之處使得Redis可以用於解決更爲廣泛的問題,並且既可以用作主數據庫(primary database)使用,又可以作爲其他存儲系統的輔助數據庫(auxiliary database)使用。

本書的後續章節會分別介紹將Redis用作主存儲(primary storage)和二級存儲(secondary storage)時的用法和查詢模式。一般來說,許多用戶只會在Redis的性能或者功能是必要的情況下,纔會將數據存儲到Redis裏面:如果程序對性能的要求不高,又或者因爲費用原因而沒辦法將大量數據存儲到內存裏面,那麼用戶可能會選擇使用關係數據庫,或者其他非關係數據庫。在實際中,讀者應該根據自己的需求來決定是否使用Redis,並考慮是將Redis用作主存儲還是輔助存儲,以及如何通過複製、持久化和事務等手段保證數據的完整性。

表1-1展示了一部分在功能上與Redis有重疊的數據庫服務器和緩存服務器,從這個表可以看出Redis與這些數據庫及軟件之間的區別。

表1-1 一些數據庫和緩存服務器的特性與功能

名稱

類型

數據存儲選項

查詢類型

附加功能

Redis

使用內存存儲(in-memory)的非關係數據庫

字符串、列表、集合、散列表、有序集合

每種數據類型都有自己的專屬命令,另外還有批量操作(bulk operation)和不完全(partial)的事務支持

發佈與訂閱,主從複製(master/slave replication),持久化,腳本(存儲過程,stored procedure)

memcached

使用內存存儲的鍵值緩存

鍵值之間的映射

創建命令、讀取命令、更新命令、刪除命令以及其他幾個命令

爲提升性能而設的多線程服務器

MySQL

關係數據庫

每個數據庫可以包含多個表,每個表可以包含多個行;可以處理多個表的視圖(view);支持空間(spatial)和第三方擴展

SELECTINSERTUPDATE DELETE、函數、存儲過程

支持ACID性質(需要使用InnoDB),主從複製和主主複製 (master/master replication)

PostgreSQL

關係數據庫

每個數據庫可以包含多個表,每個表可以包含多個行;可以處理多個表的視圖;支持空間和第三方擴展;支持可定製類型

SELECTINSERTUPDATE DELETE、內置函數、自定義的存儲過程

支持ACID性質,主從複製,由第三方支持的多主複製(multi-master replication)

MongoDB

使用硬盤存儲(on-disk)的非關係文檔存儲

每個數據庫可以包含多個表,每個表可以包含多個無schema(schema-less)的BSON文檔

創建命令、讀取命令、更新命令、刪除命令、條件查詢命令等

支持map-reduce操作,主從複製,分片,空間索引(spatial index)

1.1.2 附加特性

在使用類似Redis這樣的內存數據庫時,一個首先要考慮的問題就是“當服務器被關閉時,服務器存儲的數據將何去何從呢?”Redis擁有兩種不同形式的持久化方法,它們都可以用小而緊湊的格式將存儲在內存中的數據寫入硬盤:第一種持久化方法爲時間點轉儲(point-in-time dump),轉儲操作既可以在“指定時間段內有指定數量的寫操作執行”這一條件被滿足時執行,又可以通過調用兩條轉儲到硬盤(dump-to-disk)命令中的任何一條來執行;第二種持久化方法將所有修改了數據庫的命令都寫入一個只追加(append-only)文件裏面,用戶可以根據數據的重要程度,將只追加寫入設置爲從不同步(sync)、每秒同步一次或者每寫入一個命令就同步一次。我們將在第4章中更加深入地討論這些持久化選項。

另外,儘管Redis的性能很好,但受限於Redis的內存存儲設計,有時候只使用一臺Redis服務器可能沒有辦法處理所有請求。因此,爲了擴展Redis的讀性能,併爲Redis提供故障轉移(failover)支持,Redis實現了主從複製特性:執行復制的從服務器會連接上主服務器,接收主服務器發送的整個數據庫的初始副本(copy);之後主服務器執行的寫命令,都會被髮送給所有連接着的從服務器去執行,從而實時地更新從服務器的數據集。因爲從服務器包含的數據會不斷地進行更新,所以客戶端可以向任意一個從服務器發送讀請求,以此來避免對主服務器進行集中式的訪問。我們將在第4章中更加深入地討論Redis從服務器。

1.1.3 使用Redis的理由

有memcached使用經驗的讀者可能知道,用戶只能用APPEND命令將數據添加到已有字符串的末尾。memcached的文檔中聲明,可以用APPEND命令來管理元素列表。這很好!用戶可以將元素追加到一個字符串的末尾,並將那個字符串當作列表來使用。但隨後如何刪除這些元素呢?memcached採用的辦法是通過黑名單(blacklist)來隱藏列表裏面的元素,從而避免對元素執行讀取、更新、寫入(包括在一次數據庫查詢之後執行的memcached寫入)等操作。相反地,Redis的LISTSET允許用戶直接添加或者刪除元素。

使用Redis而不是memcached來解決問題,不僅可以讓代碼變得更簡短、更易懂、更易維護,而且還可以使代碼的運行速度更快(因爲用戶不需要通過讀取數據庫來更新數據)。除此之外,在其他許多情況下,Redis的效率和易用性也比關係數據庫要好得多。

數據庫的一個常見用法是存儲長期的報告數據,並將這些報告數據用作固定時間範圍內的聚合數據(aggregates)。收集聚合數據的常見做法是:先將各個行插入一個報告表裏面,之後再通過掃描這些行來收集聚合數據,並根據收集到的聚合數據來更新聚合表中已有的那些行。之所以使用插入行的方式來存儲,是因爲對於大部分數據庫來說,插入行操作的執行速度非常快(插入行只會在硬盤文件末尾進行寫入)。不過,對錶裏面的行進行更新卻是一個速度相當慢的操作,因爲這種更新除了會引起一次隨機讀(random read)之外,還可能會引起一次隨機寫(random write)。而在Redis裏面,用戶可以直接使用原子的(atomic)INCR命令及其變種來計算聚合數據,並且因爲Redis將數據存儲在內存裏面,而且發送給Redis的命令請求並不需要經過典型的查詢分析器(parser)或者查詢優化器(optimizer)進行處理,所以對Redis存儲的數據執行隨機寫的速度總是非常迅速的。

使用 Redis 而不是關係數據庫或者其他硬盤存儲數據庫,可以避免寫入不必要的臨時數據,也免去了對臨時數據進行掃描或者刪除的麻煩,並最終改善程序的性能。雖然上面列舉的都是一些簡單的例子,但它們很好地證明了“工具會極大地改變人們解決問題的方式”這一點。

在瞭解了Redis是什麼、它能做什麼以及我們爲什麼要使用它之後,是時候來實際地使用一下它了。接下來的一節將對Redis提供的數據結構進行介紹,說明這些數據結構的作用,並展示操作這些數據結構的其中一部分命令。

1.2 Redis數據結構簡介

正如之前的表1-1所示,Redis可以存儲鍵與5種不同數據結構類型之間的映射,這5種數據結構類型分別爲STRING(字符串)、LIST(列表)、SET(集合)、HASH(散列)和ZSET(有序集合)。有一部分Redis命令對於這5種結構都是通用的,如DELTYPERENAME等;但也有一部分Redis命令只能對特定的一種或者兩種結構使用,第3章將對Redis提供的命令進行更深入的介紹。

大部分程序員應該都不會對Redis的STRINGLISTHASH這3種結構感到陌生,因爲它們和很多編程語言內建的字符串、列表和散列等結構在實現和語義(semantics)方面都非常相似。有些編程語言還有集合數據結構,在實現和語義上類似於Redis的SETZSET在某種程度上是一種Redis特有的結構,但是當你熟悉了它之後,就會發現它也是一種非常有用的結構。表1-2對比了Redis提供的5種結構,說明了這些結構存儲的值,並簡單介紹了它們的語義。

表1-2 Redis提供的5種結構

結構類型

結構存儲的值

結構的讀寫能力

STRING

可以是字符串、整數或者浮點數

對整個字符串或者字符串的其中一部分執行操作;對整數和浮點數執行自增(increment)或者自減(decrement)操作

LIST

一個鏈表,鏈表上的每個節點都包含了一個字符串

從鏈表的兩端推入或者彈出元素;根據偏移量對鏈表進行修剪(trim);讀取單個或者多個元素;根據值查找或者移除元素

SET

包含字符串的無序收集器(unordered collection),並且被包含的每個字符串都是獨一無二、各不相同的

添加、獲取、移除單個元素;檢查一個元素是否存在於集合中;計算交集、並集、差集;從集合裏面隨機獲取元素

HASH

包含鍵值對的無序散列表

添加、獲取、移除單個鍵值對;獲取所有鍵值對

ZSET(有序集合)

字符串成員(member)與浮點數分值(score)之間的有序映射,元素的排列順序由分值的大小決定

添加、獲取、刪除單個元素;根據分值範圍(range)或者成員來獲取元素


1.2.1 Redis中的字符串

Redis的STRING和其他編程語言或者其他鍵值存儲提供的字符串非常相似。本書在使用圖片表示鍵和值的時候,通常會將鍵名(key name)和值的類型放在方框的頂部,並將值放在方框的裏面。圖1-1以鍵爲hello、值爲worldSTRING爲例,分別標記了方框的各個部分。

..\15-0832 圖\0101.tif

圖1-1 一個STRING示例,鍵爲hello,值爲world

STRING擁有一些和其他鍵值存儲相似的命令,比如GET(獲取值)、SET(設置值)和DEL(刪除值)。如果讀者已經按照附錄A中給出的方法安裝了Redis,那麼可以根據代碼清單1-1展示的例子,嘗試使用redis-cli執行SETGETDEL,表1-3描述了這3個命令的基本用法。

表1-3 字符串命令

命令

行爲

GET

獲取存儲在給定鍵中的值

SET

設置存儲在給定鍵中的值

DEL

刪除存儲在給定鍵中的值(這個命令可以用於所有類型)

代碼清單1-1 SETGETDEL的使用示例

1-1.jpg

使用 redis-cli 爲了讓讀者在一開始就能便捷地與 Redis 進行交互,本章將使用redis-cli這個交互式客戶端來介紹Redis命令。

除了能夠GETSETDEL字符串值之外,Redis還提供了一些可以對字符串的其中一部分內容進行讀取和寫入的命令,以及一些能對字符串存儲的數值執行自增或者自減操作的命令。第3章將對這些命令進行介紹,但是在此之前,我們還有許多基礎知識需要了解,下面來看一下Redis的列表及其功能。

1.2.2 Redis中的列表

Redis對鏈表(linked-list)結構的支持使得它在鍵值存儲的世界中獨樹一幟。一個列表結構可以有序地存儲多個字符串,和表示字符串時使用的方法一樣,本節使用帶有標籤的方框來表示列表,並將列表包含的元素放在方框裏面。圖1-2展示了一個這樣的示例。

..\15-0832 圖\0102.tif

圖1-2 list-key是一個包含3個元素的列表鍵,注意列表裏面的元素是可以重複的

Redis列表可執行的操作和很多編程語言裏面的列表操作非常相似:LPUSH命令和RPUSH命令分別用於將元素推入列表的左端(left end)和右端(right end);LPOP命令和RPOP命令分別用於從列表的左端和右端彈出元素;LINDEX命令用於獲取列表在給定位置上的一個元素;LRANGE命令用於獲取列表在給定範圍上的所有元素。代碼清單1-2展示了一些列表命令的使用示例,表1-4簡單介紹了示例中用到的各個命令。

表1-4 列表命令

命令

行爲

RPUSH

將給定值推入列表的右端

LRANGE

獲取列表在給定範圍上的所有值

LINDEX

獲取列表在給定位置上的單個元素

LPOP

從列表的左端彈出一個值,並返回被彈出的值

代碼清單1-2 RPUSHLRANGELINDEXLPOP的使用示例

1-2.jpg

即使Redis的列表只支持以上提到的幾個命令,它也已經可以用來解決很多問題了,但Redis並沒有就此止步——除了上面提到的命令之外,Redis列表還擁有從列表裏面移除元素的命令、將元素插入列表中間的命令、將列表修剪至指定長度(相當於從列表的其中一端或者兩端移除元素)的命令,以及其他一些命令。第3章將介紹許多列表命令,但是在此之前,讓我們先來了解一下Redis的集合。

1.2.3 Redis的集合

Redis 的集合和列表都可以存儲多個字符串,它們之間的不同在於,列表可以存儲多個相同的字符串,而集合則通過使用散列表來保證自己存儲的每個字符串都是各不相同的(這些散列表只有鍵,但沒有與鍵相關聯的值)。本書表示集合的方法和表示列表的方法基本相同,圖1-3展示了一個包含3個元素的示例集合。

..\15-0832 圖\0103.tif

圖1-3 set-key是一個包含3個元素的集合鍵

因爲Redis的集合使用無序(unordered)方式存儲元素,所以用戶不能像使用列表那樣,將元素推入集合的某一端,或者從集合的某一端彈出元素。不過用戶可以使用SADD命令將元素添加到集合,或者使用SRAM命令從集合裏面移除元素。另外還可以通過SISMEMBER命令快速地檢查一個元素是否已經存在於集合中,或者使用SMEMBERS命令獲取集合包含的所有元素(如果集合包含的元素非常多,那麼SMEMBERS命令的執行速度可能會很慢,所以請謹慎地使用這個命令)。代碼清單1-3展示了一些集合命令的使用示例,表1-5簡單介紹了代碼清單裏面用到的各個命令。

代碼清單1-3 SADDSMEMBERSSISMEMBERSREM的使用示例

1-3

表1-5 集合命令

命令

行爲

SADD

將給定元素添加到集合

SMEMBERS

返回集合包含的所有元素

SISMEMBER

檢查給定元素是否存在於集合中

SREM

如果給定的元素存在於集合中,那麼移除這個元素

跟字符串和列表一樣,集合除了基本的添加操作和移除操作之外,還支持很多其他操作,比如SINTERSUNIONSDIFF這3個命令就可以分別執行常見的交集計算、並集計算和差集計算。第3章將對集合的相關命令進行更詳細的介紹,另外第7章還會展示如何使用集合來解決多個問題。不過別心急,因爲在Redis提供的5種數據結構中,還有兩種我們尚未了解,讓我們先來看看Redis的散列。

1.2.4 Redis的散列

Redis的散列可以存儲多個鍵值對之間的映射。和字符串一樣,散列存儲的值既可以是字符串又可以是數字值,並且用戶同樣可以對散列存儲的數字值執行自增操作或者自減操作。圖1-4展示了一個包含兩個鍵值對的散列。

..\15-0832 圖\0104.tif

圖1-4 hash-key是一個包含兩個鍵值對的散列鍵

散列在很多方面就像是一個微縮版的Redis,不少字符串命令都有相應的散列版本。代碼清單1-4展示了怎樣對散列執行插入元素、獲取元素和移除元素等操作,表1-6簡單介紹了代碼清單裏面用到的各個命令。

代碼清單1-4 HSETHGETHGETALLHDEL的使用示例

1-4

表1-6 散列命令

命令

行爲

HSET

在散列裏面關聯起給定的鍵值對

HGET

獲取指定散列鍵的值

HGETALL

獲取散列包含的所有鍵值對

HDEL

如果給定鍵存在於散列裏面,那麼移除這個鍵

熟悉文檔數據庫的讀者可以將Redis的散列看作是文檔數據庫裏面的文檔,而熟悉關係數據庫的讀者則可以將Redis的散列看作是關係數據庫裏面的行,因爲散列、文檔和行這三者都允許用戶同時訪問或者修改一個或多個域(field)。最後,讓我們來了解一下Redis的5種數據結構中的最後一種:有序集合。

1.2.5 Redis的有序集合

有序集合和散列一樣,都用於存儲鍵值對:有序集合的鍵被稱爲成員(member),每個成員都是獨一無二的;而有序集合的值則被稱爲分值(score),分值必須爲浮點數。有序集合是Redis裏面唯一一個既可以根據成員訪問元素(這一點和散列一樣),又可以根據分值以及分值的排列順序來訪問元素的結構。圖1-5展示了一個包含兩個元素的有序集合示例。

..\15-0832 改圖\0105.tif

圖1-5 zset-key是一個包含兩個元素的有序集合鍵

和Redis的其他結構一樣,用戶可以對有序集合執行添加、移除和獲取等操作,代碼清單1-5展示了這些操作的執行示例,表1-7簡單介紹了代碼清單裏面用到的各個命令。

代碼清單1-5 ZADDZRANGEZRANGEBYSCOREZREM的使用示例

1-5

表1-7 有序集合命令

命令

行爲

ZADD

將一個帶有給定分值的成員添加到有序集合裏面

ZRANGE

根據元素在有序排列中所處的位置,從有序集合裏面獲取多個元素

ZRANGEBYSCORE

獲取有序集合在給定分值範圍內的所有元素

ZREM

如果給定成員存在於有序集合,那麼移除這個成員

現在讀者應該已經知道有序集合是什麼和它能幹什麼了,到目前爲止,我們基本瞭解了Redis提供的5種結構。接下來的一節將展示如何通過結合散列的數據存儲能力和有序集合內建的排序能力來解決一個常見的問題。

1.3 你好Redis

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

..\15-0832 圖\0106.tif

圖1-6 Reddit是一個可以對文章進行投票的網站

..\15-0832 改圖\0107.tif

圖1-7 StackOverflow是一個可以對問題進行投票的網站

1.3.1 對文章進行投票

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

爲了產生一個能夠隨着時間流逝而不斷減少的評分,程序需要根據文章的發佈時間和當前時間來計算文章的評分,具體的計算方法爲:將文章得到的支持票數量乘以一個常數,然後加上文章的發佈時間,得出的結果就是文章的評分。

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

構建文章投票網站除了需要計算文章評分之外,還需要使用Redis結構存儲網站上的各種信息。對於網站裏的每篇文章,程序都使用一個散列來存儲文章的標題、指向文章的網址、發佈文章的用戶、文章的發佈時間、文章得到的投票數量等信息,圖1-8展示了一個使用散列來存儲文章信息的例子。

..\15-0832 圖\0108.tif

圖1-8 一個使用散列存儲文章信息的例子

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

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

..\15-0832 圖\0109.tif

圖1-9 兩個有序集合分別記錄了根據發佈時間排序的文章和根據評分排序的文章

爲了防止用戶對同一篇文章進行多次投票,網站需要爲每篇文章記錄一個已投票用戶名單。爲此,程序將爲每篇文章創建一個集合,並使用這個集合來存儲所有已投票用戶的ID,圖1-10展示了一個這樣的集合示例。

..\15-0832 圖\0110.tif

圖1-10 爲100408號文章投過票的一部分用戶

爲了儘量節約內存,我們規定當一篇文章發佈期滿一週之後,用戶將不能再對它進行投票,文章的評分將被固定下來,而記錄文章已投票用戶名單的集合也會被刪除。

在實現投票功能之前,讓我們來看看圖 1-11:這幅圖展示了當115423號用戶給100408號文章投票的時候,數據結構發生的變化。

..\15-0832 圖\0111.tif

圖1-11 當115423號用戶給100408號文章投票的時候,數據結構發生的變化

既然我們已經知道了網站計算文章評分的方法,也知道了網站存儲數據所需的數據結構,那麼現在是時候實際地實現這個投票功能了!當用戶嘗試對一篇文章進行投票時,程序需要使用ZSCORE命令檢查記錄文章發佈時間的有序集合,判斷文章的發佈時間是否未超過一週。如果文章仍然處於可以投票的時間範圍之內,那麼程序將使用SADD命令,嘗試將用戶添加到記錄文章已投票用戶名單的集合裏面。如果添加操作執行成功的話,那麼說明用戶是第一次對這篇文章進行投票,程序將使用ZINCRBY命令爲文章的評分增加432分(ZINCRBY命令用於對有序集合成員的分值執行自增操作),並使用HINCRBY命令對散列記錄的文章投票數量進行更新(HINCRBY命令用於對散列存儲的值執行自增操作),代碼清單1-6展示了投票功能的實現代碼。

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

1-6.jpg

Redis事務 從技術上來講,要正確地實現投票功能,我們需要將代碼清單1-6裏面的SADDZINCRBYHINCRBY這3個命令放到一個事務裏面執行,不過因爲本書要等到第4章才介紹Redis事務,所以我們暫時忽略這個問題。

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

1.3.2 發佈並獲取文章

發佈一篇新文章首先需要創建一個新的文章ID,這項工作可以通過對一個計數器(counter)執行INCR命令來完成。接着程序需要使用SADD將文章發佈者的ID添加到記錄文章已投票用戶名單的集合裏面,並使用EXPIRE命令爲這個集合設置一個過期時間,讓Redis在文章發佈期滿一週之後自動刪除這個集合。之後,程序會使用HMSET命令來存儲文章的相關信息,並執行兩個ZADD命令,將文章的初始評分(initial score)和發佈時間分別添加到兩個相應的有序集合裏面。代碼清單1-7展示了發佈新文章功能的實現代碼。

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

1-7

好了,我們已經陸續實現了文章投票功能和文章發佈功能,接下來要考慮的就是如何取出評分最高的文章以及如何取出最新發布的文章了。爲了實現這兩個功能,程序需要先使用ZREVRANGE命令取出多個文章ID,然後再對每個文章ID執行一次HGETALL命令來取出文章的詳細信息,這個方法既可以用於取出評分最高的文章,又可以用於取出最新發布的文章。這裏特別要注意的一點是,因爲有序集合會根據成員的分值從小到大地排列元素,所以使用ZREVRANGE命令,以“分值從大到小”的排列順序取出文章ID纔是正確的做法,代碼清單1-8展示了文章獲取功能的實現函數。

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

1-8

Python的默認值參數和關鍵字參數 代碼清單1-8中的get_articles()函數爲order參數設置了默認值score:。Python語言的初學者可能會對“默認值參數”以及“根據名字(而不是位置)來傳入參數”的一些細節感到陌生。如果讀者在理解函數定義或者參數傳遞方面有困難,可以考慮去看看《Python語言教程》,教程裏面對這兩個方面進行了很好的介紹,點擊以下短鏈接就可以直接訪問教程的相關章節:http://mng.bz/KM5x

雖然我們構建的網站現在已經可以展示最新發布的文章和評分最高的文章了,但它還不具備目前很多投票網站都支持的羣組(group)功能:這個功能可以讓用戶只看見與特定話題有關的文章,比如與“可愛的動物”有關的文章、與“政治”有關的文章、與“Java編程”有關的文章或者介紹“Redis用法”的文章等等。接下來的一節將向我們展示爲文章投票網站添加羣組功能的方法。

1.3.3 對文章進行分組

羣組功能由兩個部分組成,一個部分負責記錄文章屬於哪個羣組,另一個部分負責取出羣組裏面的文章。爲了記錄各個羣組都保存了哪些文章,網站需要爲每個羣組創建一個集合,並將所有同屬一個羣組的文章ID都記錄到那個集合裏面。代碼清單1-9展示了將文章添加到羣組裏面的方法,以及從羣組裏面移除文章的方法。

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

1-9

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

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

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

..\15-0832 圖\0112.tif

圖1-12 對集合groups:programming和有序集合score:進行交集計算得出了新的有序集合score:programming,它包含了所有同時存在於集合groups:programming和有序集合score:的成員。因爲集合groups:programming的所有成員的分值都被視爲是1,而有序集合score:的所有成員的分值都大於1,並且這次交集計算挑選的分值爲相同成員中的最大分值,所以有序集合score:programming的成員的分值實際上是由有序集合score:的成員的分值來決定的

通過對存儲羣組文章的集合和存儲文章評分的有序集合執行ZINTERSTORE命令,程序可以得到按照文章評分排序的羣組文章;而通過對存儲羣組文章的集合和存儲文章發佈時間的有序集合執行ZINTERSTORE命令,程序則可以得到按照文章發佈時間排序的羣組文章。如果羣組包含的文章非常多,那麼執行ZINTERSTORE命令就會比較花時間,爲了儘量減少Redis的工作量,程序會將這個命令的計算結果緩存60秒。另外,我們還重用了已有的get_articles()函數來分頁並獲取羣組文章,代碼清單1-10展示了網站從羣組裏面獲取一整頁文章的方法。

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

1-10.jpg

有些網站只允許用戶將文章放在一個或者兩個羣組裏面(其中一個是“所有文章”羣組,另一個是最適合文章的羣組)。在這種情況下,最好直接將文章所在的羣組記錄到存儲文章信息的散列裏面,並在article_vote()函數的末尾增加一個ZINCRBY命令調用,用於更新文章在羣組中的評分。但是在這個示例裏面,我們構建的文章投票網站允許一篇文章同時屬於多個羣組(比如一篇文章可以同時屬於“編程”和“算法”兩個羣組),所以對於一篇同時屬於多個羣組的文章來說,更新文章的評分意味着程序需要對文章所屬的全部羣組執行自增操作。在這種情況下,如果一篇文章同時屬於很多個羣組,那麼更新文章評分這一操作可能會變得相當耗時,因此,我們在get_group_articles()函數裏面對ZINTERSTORE命令的執行結果進行了緩存處理,以此來儘量減少ZINTERSTORE命令的執行次數。開發者在靈活性或限制條件之間的取捨將改變程序存儲和更新數據的方式,這一點對於任何數據庫都是適用的,Redis也不例外。

實現投反對票的功能

我們的示例目前只實現了投支持票的功能,但是在很多實際的網站裏面,反對票也能給用戶提供有用的反饋信息。因此,請讀者能想辦法在article_vote()函數和post_article()函數裏面添加投反對票的功能。除此之外,讀者還可以嘗試爲用戶提供對調投票的功能:比如將支持票轉換成反對票,或者將反對票轉換成支持票。提示:如果讀者在實現對調投票功能時出現了困難,可以參考一下SMOVE命令。

好的,現在我們已經成功地構建起了一個展示最受歡迎文章的網站後端,這個網站可以獲取文章、發佈文章、對文章進行投票甚至還可以對文章進行分組。



 分片是一種將數據劃分爲多個部分的方法,對數據的劃分可以基於鍵包含的ID、基於鍵的散列值,或者基於以上兩者的某種組合。通過對數據進行分片,用戶可以將數據存儲到多臺機器裏面,也可以從多臺機器裏面獲取數據,這種方法在解決某些問題時可以獲得線性級別的性能提升。

 客觀來講,memcached也能用在這個簡單的場景裏,但使用Redis存儲聚合數據有以下3個好處:首先,使用Redis可以將彼此相關的聚合數據放在同一個結構裏面,這樣訪問聚合數據就會變得更爲容易;其次,使用Redis可以將聚合數據放到有序集合裏面,構建出一個實時的排行榜;最後,Redis的聚合數據可以是整數或者浮點數,而memcached的聚合數據只能是整數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章