學習Redis從這里開始
本文主要內容
- Redis與其他軟件的相同之處和不同之處
- Redis的用法
- 使用Python示例代碼與Redis進行簡單的互動
- 使用Redis解決實際問題
Redis是一個遠程內存數據庫,它不僅性能強勁,而且還具有復制特性以及為解決問題而生的獨一無二的數據模型。Redis提供了5種不同類型的 數據結構,各式各樣的問題都可以很自然地映射到這些數據結構上:Redis的數據結構致力于幫助用戶解決問題,而不會像其他數據庫那樣,要求用戶扭曲問題 來適應數據庫。除此之外,通過復制、持久化(persistence)和客戶端分片(client-side sharding)等特性,用戶可以很方便地將Redis擴展成一個能夠包含數百GB數據、每秒處理上百萬次請求的系統。
筆者第一次使用Redis是在一家公司里面,這家公司需要對一個保存了6萬個客戶聯系方式的關系數據庫進行搜索,搜索可以根據名字、郵件地址、所 在地和電話號碼來進行,每次搜索需要花費10~15秒的時間。在花了一周時間學習Redis的基礎知識之后,我使用Redis重寫了一個新的搜索引擎,然 后又花費了數周時間來仔細測試這個新系統,使它達到生產級別,最終這個新的搜索系統不僅可以根據名字、郵件地址、所在地和電話號碼等信息來過濾和排序客戶 聯系方式,并且每次操作都可以在50毫秒之內完成,這比原來的搜索系統足足快了 200 倍。閱讀本書可以讓你學到很多小技巧、小竅門以及使用Redis解決某些常見問題的方法。
本章將介紹Redis的適用范圍,以及在不同環境中使用Redis的方法(比如怎樣跟不同的組件和編程語言進行通信等);而之后的章節則會展示各式各樣的問題,以及使用Redis來解決這些問題的方法。
現在你已經知道我是怎樣開始使用Redis的了,也知道了這本書大概要講些什么內容了,是時候更詳細地介紹一下Redis,并說明為什么應該使用Redis了。
安裝Redis和Python 附錄A介紹了快速安裝Redis和Python的方法。
在其他編程語言里面使用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),可以將存儲在內存的鍵值對數據持久化到硬 盤,可以使用復制特性來擴展讀性能,還可以使用客戶端分片 1 來擴展寫性能,接下來的幾節將分別介紹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)和第三方擴展 |
SELECT、INSERT、UPDATE、DELETE、函數、存儲過程 |
支持ACID性質(需要使用InnoDB),主從復制和主主復制 (master/master replication) |
PostgreSQL |
關系數據庫 |
每個數據庫可以包含多個表,每個表可以包含多個行;可以處理多個表的視圖;支持空間和第三方擴展;支持可定制類型 |
SELECT、INSERT、UPDATE、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的LIST和SET允許用戶直接添加或者刪除元素。
使用Redis而不是memcached來解決問題,不僅可以讓代碼變得更簡短、更易懂、更易維護,而且還可以使代碼的運行速度更快(因為用戶不需要通過讀取數據庫來更新數據)。除此之外,在其他許多情況下,Redis的效率和易用性也比關系數據庫要好得多。
數據庫的一個常見用法是存儲長期的報告數據,并將這些報告數據用作固定時間范圍內的聚合數據(aggregates)。收集聚合數據的常見做法是:先將各 個行插入一個報告表里面,之后再通過掃描這些行來收集聚合數據,并根據收集到的聚合數據來更新聚合表中已有的那些行。之所以使用插入行的方式來存儲,是因 為對于大部分數據庫來說,插入行操作的執行速度非常快(插入行只會在硬盤文件末尾進行寫入)。不過,對表里面的行進行更新卻是一個速度相當慢的操作,因為 這種更新除了會引起一次隨機讀(random read)之外,還可能會引起一次隨機寫(random write)。而在Redis里面,用戶可以直接使用原子的(atomic)INCR命令及其變種來計算聚合數據,并且因為Redis將數據存儲在內存里面 2 ,而且發送給Redis的命令請求并不需要經過典型的查詢分析器(parser)或者查詢優化器(optimizer)進行處理,所以對Redis存儲的數據執行隨機寫的速度總是非常迅速的。
使用 Redis 而不是關系數據庫或者其他硬盤存儲數據庫,可以避免寫入不必要的臨時數據,也免去了對臨時數據進行掃描或者刪除的麻煩,并最終改善程序的性能。雖然上面列舉的都是一些簡單的例子,但它們很好地證明了“工具會極大地改變人們解決問題的方式”這一點。
除了第6章提到的任務隊列(task queue)之外,本書的大部分內容都致力于實時地解決問題。本書通過展示各種技術并提供可工作的代碼來幫助讀者消滅瓶頸、簡化代碼、收集數據、分發 (distribute)數據、構建實用程序(utility),并最終幫助讀者更輕松地完成構建軟件的任務。只要正確地使用書中介紹的技術,讀者的軟件 就可以擴展至令那些所謂的“Web擴展技術(web-sacle technology)”相形見絀的地步。
在了解了Redis是什么、它能做什么以及我們為什么要使用它之后,是時候來實際地使用一下它了。接下來的一節將對Redis提供的數據結構進行介紹,說明這些數據結構的作用,并展示操作這些數據結構的其中一部分命令。
1.2 Redis數據結構簡介
正如之前的表1-1所示,Redis可以存儲鍵與5種不同數據結構類型之間的映射,這5種數據結構類型分別為STRING(字符串)、LIST(列表)、SET(集合)、HASH(散列)和ZSET(有序集合)。有一部分Redis命令對于這5種結構都是通用的,如DEL、TYPE、RENAME等;但也有一部分Redis命令只能對特定的一種或者兩種結構使用,第3章將對Redis提供的命令進行更深入的介紹。
大部分程序員應該都不會對Redis的STRING、LIST、HASH這3種結構感到陌生,因為它們和很多編程語言內建的字符串、列表和散列等結構在實現和語義(semantics)方面都非常相似。有些編程語言還有集合數據結構,在實現和語義上類似于Redis的SET。ZSET在某種程度上是一種Redis特有的結構,但是當你熟悉了它之后,就會發現它也是一種非常有用的結構。表1-2對比了Redis提供的5種結構,說明了這些結構存儲的值,并簡單介紹了它們的語義。
表1-2 Redis提供的5種結構
結構類型 |
結構存儲的值 |
結構的讀寫能力 |
---|---|---|
STRING |
可以是字符串、整數或者浮點數 |
對整個字符串或者字符串的其中一部分執行操作;對整數和浮點數執行自增(increment)或者自減(decrement)操作 |
LIST |
一個鏈表,鏈表上的每個節點都包含了一個字符串 |
從鏈表的兩端推入或者彈出元素;根據偏移量對鏈表進行修剪(trim);讀取單個或者多個元素;根據值查找或者移除元素 |
SET |
包含字符串的無序收集器(unordered collection),并且被包含的每個字符串都是獨一無二、各不相同的 |
添加、獲取、移除單個元素;檢查一個元素是否存在于集合中;計算交集、并集、差集;從集合里面隨機獲取元素 |
HASH |
包含鍵值對的無序散列表 |
添加、獲取、移除單個鍵值對;獲取所有鍵值對 |
ZSET(有序集合) |
字符串成員(member)與浮點數分值(score)之間的有序映射,元素的排列順序由分值的大小決定 |
添加、獲取、刪除單個元素;根據分值范圍(range)或者成員來獲取元素 |
命令列表 本節在介紹每個數據類型的時候,都會在一個表格里面展示一小部分處理這些數據結構的命令,之后的第 3 章會展示一個更詳細(但仍不完整)的命令列表,完整的 Redis 命令列表可以在 http://redis.io/commands 找到。
這一節將介紹如何表示Redis的這5種結構,并且還會介紹Redis命令的使用方法,從而為本書的后續內容打好基礎。本書展示的所有示例代碼都 是用Python寫的,如果讀者已經按照附錄A里面描述的方法安裝好了Redis,那么應該也已經安裝好了Python,以及在Python里面使用 Redis所需的客戶端庫。只要讀者在電腦里面安裝了Redis、Python和redis-py庫,就可以在閱讀本書的同時,嘗試執行書中展示的示例代 碼了。
請安裝Redis和Python 在閱讀后續內容之前,請讀者先按照附錄A中介紹的方法安裝Redis和Python。如果讀者覺得附錄A描述的安裝方法過于復雜,那么這里有一個更簡單的方法,但這個方法只能用于Debian系統(或者該系統的衍生系統):從 http://redis.io/download 下載Redis的壓縮包,解壓壓縮包,執行make && sudo make install,之后再執行sudo python -m easy_install redis hiredis(hiredis是可選的,它是一個使用C語言編寫的高性能Redis客戶端)。
如果讀者熟悉過程式編程語言或者面向對象編程語言,那么即使沒有使用過Python,應該也可以看懂Python代碼。另一方面,如果讀者決定使用其他編程語言來操作Redis,那么就需要自己來將本書的Python代碼翻譯成正在使用的語言的代碼。
使用其他語言編寫的示例代碼 盡管沒有包含在書中,但本書展示的Python示例代碼已經被翻譯成了Ruby代碼、Java代碼和JavaScript代碼,這些翻譯代碼可以在 https://github.com/josiahcarlson/redis-in-action 下載到。跟Python編寫的示例代碼一樣,這些翻譯代碼也包含相應的注釋,方便讀者參考。
為了讓示例代碼盡可能地簡單,本書會盡量避免使用Python的高級特性,并使用函數而不是類或者其他東西來執行Redis操作,以此來將焦點放在使用 Redis解決問題上面,而不必過多地關注Python的語法。本節將使用redis-cli控制臺與Redis進行互動。首先,讓我們來了解一下 Redis中最簡單的結構:STRING。
1.2.1 Redis中的字符串
Redis的STRING和其他編程語言或者其他鍵值存儲提供的字符串非常相似。本書在使用圖片表示鍵和值的時候,通常會將鍵名(key name)和值的類型放在方框的頂部,并將值放在方框的里面。圖1-1以鍵為hello、值為world的STRING為例,分別標記了方框的各個部分。
圖1-1 一個STRING示例,鍵為hello,值為world
STRING擁有一些和其他鍵值存儲相似的命令,比如GET(獲取值)、SET(設置值)和DEL(刪除值)。如果讀者已經按照附錄A中給出的方法安裝了Redis,那么可以根據代碼清單1-1展示的例子,嘗試使用redis-cli執行SET、GET和DEL,表1-3描述了這3個命令的基本用法。
表1-3 字符串命令
命令 |
行為 |
---|---|
GET |
獲取存儲在給定鍵中的值 |
SET |
設置存儲在給定鍵中的值 |
DEL |
刪除存儲在給定鍵中的值(這個命令可以用于所有類型) |
代碼清單1-1SET、GET和DEL的使用示例
使用 redis-cli 為了讓讀者在一開始就能便捷地與 Redis 進行交互,本章將使用redis-cli這個交互式客戶端來介紹Redis命令。
除了能夠GET、SET和DEL字符串值之外,Redis還提供了一些可以對字符串的其中一部分內容進行讀取和寫入的命令,以及一些能對字符串存儲的數值執行自增或者自減操作的命令。第3章將對這些命令進行介紹,但是在此之前,我們還有許多基礎知識需要了解,下面來看一下Redis的列表及其功能。
1.2.2 Redis中的列表
Redis對鏈表(linked-list)結構的支持使得它在鍵值存儲的世界中獨樹一幟。一個列表結構可以有序地存儲多個字符串,和表示字符串時使用的方法一樣,本節使用帶有標簽的方框來表示列表,并將列表包含的元素放在方框里面。圖1-2展示了一個這樣的示例。
圖1-2list-key是一個包含3個元素的列表鍵,注意列表里面的元素是可以重復的
Redis列表可執行的操作和很多編程語言里面的列表操作非常相似:LPUSH命令和RPUSH命令分別用于將元素推入列表的左端(left end)和右端(right end);LPOP命令和RPOP命令分別用于從列表的左端和右端彈出元素;LINDEX命令用于獲取列表在給定位置上的一個元素;LRANGE命令用于獲取列表在給定范圍上的所有元素。代碼清單1-2展示了一些列表命令的使用示例,表1-4簡單介紹了示例中用到的各個命令。
表1-4 列表命令
命令 |
行為 |
---|---|
RPUSH |
將給定值推入列表的右端 |
LRANGE |
獲取列表在給定范圍上的所有值 |
LINDEX |
獲取列表在給定位置上的單個元素 |
LPOP |
從列表的左端彈出一個值,并返回被彈出的值 |
代碼清單1-2RPUSH、LRANGE、LINDEX和LPOP的使用示例
即使Redis的列表只支持以上提到的幾個命令,它也已經可以用來解決很多問題了,但Redis并沒有就此止步——除了上面提到的命令之 外,Redis列表還擁有從列表里面移除元素的命令、將元素插入列表中間的命令、將列表修剪至指定長度(相當于從列表的其中一端或者兩端移除元素)的命 令,以及其他一些命令。第3章將介紹許多列表命令,但是在此之前,讓我們先來了解一下Redis的集合。
1.2.3 Redis的集合
Redis 的集合和列表都可以存儲多個字符串,它們之間的不同在于,列表可以存儲多個相同的字符串,而集合則通過使用散列表來保證自己存儲的每個字符串都是各不相同 的(這些散列表只有鍵,但沒有與鍵相關聯的值)。本書表示集合的方法和表示列表的方法基本相同,圖1-3展示了一個包含3個元素的示例集合。
圖1-3set-key是一個包含3個元素的集合鍵
因為Redis的集合使用無序(unordered)方式存儲元素,所以用戶不能像使用列表那樣,將元素推入集合的某一端,或者從集合的某一端彈出元素。不過用戶可以使用SADD命令將元素添加到集合,或者使用SRAM命令從集合里面移除元素。另外還可以通過SISMEMBER命令快速地檢查一個元素是否已經存在于集合中,或者使用SMEMBERS命令獲取集合包含的所有元素(如果集合包含的元素非常多,那么SMEMBERS命令的執行速度可能會很慢,所以請謹慎地使用這個命令)。代碼清單1-3展示了一些集合命令的使用示例,表1-5簡單介紹了代碼清單里面用到的各個命令。
代碼清單1-3SADD、SMEMBERS、SISMEMBER和SREM的使用示例
表1-5 集合命令
命令 |
行為 |
---|---|
SADD |
將給定元素添加到集合 |
SMEMBERS |
返回集合包含的所有元素 |
SISMEMBER |
檢查給定元素是否存在于集合中 |
SREM |
如果給定的元素存在于集合中,那么移除這個元素 |
跟字符串和列表一樣,集合除了基本的添加操作和移除操作之外,還支持很多其他操作,比如SINTER、SUNION、SDIFF``這3個命令就可以分別執行常見的交集計算、并集計算和差集計算。第3章將對集合的相關命令進行更詳細的介紹,另外第7章還會展示如何使用集合來解決多個問題。不過別心急,因為在Redis提供的5種數據結構中,還有兩種我們尚未了解,讓我們先來看看Redis的散列。
1.2.4 Redis的散列
Redis的散列可以存儲多個鍵值對之間的映射。和字符串一樣,散列存儲的值既可以是字符串又可以是數字值,并且用戶同樣可以對散列存儲的數字值執行自增操作或者自減操作。圖1-4展示了一個包含兩個鍵值對的散列。
圖1-4hash-key是一個包含兩個鍵值對的散列鍵
散列在很多方面就像是一個微縮版的Redis,不少字符串命令都有相應的散列版本。代碼清單1-4展示了怎樣對散列執行插入元素、獲取元素和移除元素等操作,表1-6簡單介紹了代碼清單里面用到的各個命令。
代碼清單1-4HSET、HGET、HGETALL和HDEL的使用示例
表1-6 散列命令
命令 |
行為 |
---|---|
HSET |
在散列里面關聯起給定的鍵值對 |
HGET |
獲取指定散列鍵的值 |
HGETALL |
獲取散列包含的所有鍵值對 |
HDEL |
如果給定鍵存在于散列里面,那么移除這個鍵 |
熟悉文檔數據庫的讀者可以將Redis的散列看作是文檔數據庫里面的 文檔 ,而熟悉關系數據庫的讀者則可以將Redis的散列看作是關系數據庫里面的行,因為散列、文檔和行這三者都允許用戶同時訪問或者修改一個或多個域(field)。最后,讓我們來了解一下Redis的5種數據結構中的最后一種:有序集合。
1.2.5 Redis的有序集合
有序集合和散列一樣,都用于存儲鍵值對:有序集合的鍵被稱為成員(member),每個成員都是獨一無二的;而有序集合的值則被稱為 分值 (score),分值必須為浮點數。有序集合是Redis里面唯一一個既可以根據成員訪問元素(這一點和散列一樣),又可以根據分值以及分值的排列順序來訪問元素的結構。圖1-5展示了一個包含兩個元素的有序集合示例。
圖1-5zset-key是一個包含兩個元素的有序集合鍵
和Redis的其他結構一樣,用戶可以對有序集合執行添加、移除和獲取等操作,代碼清單1-5展示了這些操作的執行示例,表1-7簡單介紹了代碼清單里面用到的各個命令。
代碼清單1-5ZADD、ZRANGE、ZRANGEBYSCORE和ZREM的使用示例
表1-7 有序集合命令
命令 |
行為 |
---|---|
ZADD |
將一個帶有給定分值的成員添加到有序集合里面 |
ZRANGE |
根據元素在有序排列中所處的位置,從有序集合里面獲取多個元素 |
ZRANGEBYSCORE |
獲取有序集合在給定分值范圍內的所有元素 |
ZREM |
如果給定成員存在于有序集合,那么移除這個成員 |
現在讀者應該已經知道有序集合是什么和它能干什么了,到目前為止,我們基本了解了Redis提供的5種結構。接下來的一節將展示如何通過結合散列的數據存儲能力和有序集合內建的排序能力來解決一個常見的問題。
1.3 你好Redis
在對Redis提供的5種結構有了基本的了解之后,現在是時候來學習一下怎樣使用這些結構來解決實際問題了。最近幾年,越來越多的網站開始提供對 網頁鏈接、文章或者問題進行投票的功能,其中包括圖1-6展示的reddit以及圖1-7展示的StackOverflow。這些網站會根據文章的發布時 間和文章獲得的投票數量計算出一個評分,然后按照這個評分來決定如何排序和展示文章。本節將展示如何使用Redis來構建一個簡單的文章投票網站的后端。
圖1-6 Reddit是一個可以對文章進行投票的網站
圖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展示了一個使用散列來存儲文章信息的例子。
圖1-8 一個使用散列存儲文章信息的例子
使用冒號作為分隔符 本書使用冒號(:)來分隔名字的不同部分:比如圖 1-8 里面的鍵名article:92617就使用了冒號來分隔單詞article和文章的ID號92617,以此來構建命名空間(namespace)。使用:作為分隔符只是我的個人喜好,不過大部分Redis用戶也都是這么做的,另外還有一些常見的分隔符,如句號(.)、斜線(/),有些人甚至還會使用管道符號(|)。無論使用哪個符號來做分隔符,都要保持分隔符的一致性。同時,請讀者注意觀察和學習本書使用冒號創建嵌套命名空間的方法。
我們的文章投票網站將使用兩個有序集合來有序地存儲文章:第一個有序集合的成員為文章 ID,分值為文章的發布時間;第二個有序集合的成員同樣為文章 ID,而分值則為文章的評分。通過這兩個有序集合,網站既可以根據文章發布的先后順序來展示文章,又可以根據文章評分的高低來展示文章,圖1-9展示了這 兩個有序集合的一個示例。
圖1-9 兩個有序集合分別記錄了根據發布時間排序的文章和根據評分排序的文章
為了防止用戶對同一篇文章進行多次投票,網站需要為每篇文章記錄一個已投票用戶名單。為此,程序將為每篇文章創建一個集合,并使用這個集合來存儲所有已投票用戶的ID,圖1-10展示了一個這樣的集合示例。
圖1-10 為100408號文章投過票的一部分用戶
為了盡量節約內存,我們規定當一篇文章發布期滿一周之后,用戶將不能再對它進行投票,文章的評分將被固定下來,而記錄文章已投票用戶名單的集合也會被刪除。
在實現投票功能之前,讓我們來看看圖 1-11:這幅圖展示了當115423號用戶給100408號文章投票的時候,數據結構發生的變化。
圖1-11 當115423號用戶給100408號文章投票的時候,數據結構發生的變化
既然我們已經知道了網站計算文章評分的方法,也知道了網站存儲數據所需的數據結構,那么現在是時候實際地實現這個投票功能了!當用戶嘗試對一篇文章進行投票時,程序需要使用ZSCORE命令檢查記錄文章發布時間的有序集合,判斷文章的發布時間是否未超過一周。如果文章仍然處于可以投票的時間范圍之內,那么程序將使用SADD命令,嘗試將用戶添加到記錄文章已投票用戶名單的集合里面。如果添加操作執行成功的話,那么說明用戶是第一次對這篇文章進行投票,程序將使用ZINCRBY命令為文章的評分增加432分(ZINCRBY``命令用于對有序集合成員的分值執行自增操作),并使用HINCRBY命令對散列記錄的文章投票數量進行更新(HINCRBY``命令用于對散列存儲的值執行自增操作),代碼清單1-6展示了投票功能的實現代碼。
代碼清單1-6article_vote()函數
Redis事務 從技術上來講,要正確地實現投票功能,我們需要將代碼清單1-6里面的SADD、ZINCRBY和HINCRBY這3個命令放到一個事務里面執行,不過因為本書要等到第4章才介紹Redis事務,所以我們暫時忽略這個問題。
這個投票功能還是很不錯的,對吧?那么發布文章的功能要怎么實現呢?
1.3.2 發布并獲取文章
發布一篇新文章首先需要創建一個新的文章ID,這項工作可以通過對一個計數器(counter)執行INCR命令來完成。接著程序需要使用SADD將文章發布者的ID添加到記錄文章已投票用戶名單的集合里面,并使用EXPIRE命令為這個集合設置一個過期時間,讓Redis在文章發布期滿一周之后自動刪除這個集合。之后,程序會使用HMSET命令來存儲文章的相關信息,并執行兩個ZADD命令,將文章的初始評分(initial score)和發布時間分別添加到兩個相應的有序集合里面。代碼清單1-7展示了發布新文章功能的實現代碼。
代碼清單1-7post_article()函數
好了,我們已經陸續實現了文章投票功能和文章發布功能,接下來要考慮的就是如何取出評分最高的文章以及如何取出最新發布的文章了。為了實現這兩個功能,程序需要先使用ZREVRANGE命令取出多個文章ID,然后再對每個文章ID執行一次HGETALL命令來取出文章的詳細信息,這個方法既可以用于取出評分最高的文章,又可以用于取出最新發布的文章。這里特別要注意的一點是,因為有序集合會根據成員的分值從小到大地排列元素,所以使用ZREVRANGE命令,以“分值從大到小”的排列順序取出文章ID才是正確的做法,代碼清單1-8展示了文章獲取功能的實現函數。
代碼清單1-8get_articles()函數
Python的默認值參數和關鍵字參數 代碼清單1-8中的get_articles()函數為order參數設置了默認值score:。Python語言的初學者可能會對“默認值參數”以及“根據名字(而不是位置)來傳入參數”的一些細節感到陌生。如果讀者在理解函數定義或者參數傳遞方 面有困難,可以考慮去看看《Python語言教程》,教程里面對這兩個方面進行了很好的介紹,點擊以下短鏈接就可以直接訪問教程的相關章節: http://mng.bz/KM5x 。
雖然我們構建的網站現在已經可以展示最新發布的文章和評分最高的文章了,但它還不具備目前很多投票網站都支持的群組(group)功能:這個功能 可以讓用戶只看見與特定話題有關的文章,比如與“可愛的動物”有關的文章、與“政治”有關的文章、與“Java編程”有關的文章或者介紹“Redis用 法”的文章等等。接下來的一節將向我們展示為文章投票網站添加群組功能的方法。
1.3.3 對文章進行分組
群組功能由兩個部分組成,一個部分負責記錄文章屬于哪個群組,另一個部分負責取出群組里面的文章。為了記錄各個群組都保存了哪些文章,網站需要為 每個群組創建一個集合,并將所有同屬一個群組的文章ID都記錄到那個集合里面。代碼清單1-9展示了將文章添加到群組里面的方法,以及從群組里面移除文章 的方法。
代碼清單1-9add_remove_groups()函數
初看上去,可能會有讀者覺得使用集合來記錄群組文章并沒有多大用處。到目前為止,讀者只看到了集合結構檢查某個元素是否存在的能力,但實際上Redis不僅可以對多個集合執行操作,甚至在一些情況下,還可以在集合和有序集合之間執行操作。
為了能夠根據評分對群組文章進行排序和分頁(paging),網站需要將同一個群組里面的所有文章都按照評分有序地存儲到一個有序集合里面。Redis的ZINTERSTORE命令可以接受多個集合和多個有序集合作為輸入,找出所有同時存在于集合和有序集合的成員,并以幾種不同的方式來組合(combine)這些成員的分值(所有集合成員的分值都會被視為是1)。對于我們的文章投票網站來說,程序需要使用ZINTERSTORE命令選出相同成員中最大的那個分值來作為交集成員的分值:取決于所使用的排序選項,這些分值既可以是文章的評分,也可以是文章的發布時間。
圖 1-12 展示了對一個包含少量文章的群組集合和一個包含大量文章及評分的有序集合執行ZINTERSTORE命令的過程,注意觀察那些同時出現在集合和有序集合里面的文章是怎樣被添加到結果有序集合里面的。
圖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-10get_group_articles()函數
有些網站只允許用戶將文章放在一個或者兩個群組里面(其中一個是“所有文章”群組,另一個是最適合文章的群組)。在這種情況下,最好直接將文章所在的群組記錄到存儲文章信息的散列里面,并在article_vote()函數的末尾增加一個ZINCRBY命令調用,用于更新文章在群組中的評分。但是在這個示例里面,我們構建的文章投票網站允許一篇文章同時屬于多個群組(比如一篇文章可以同時屬于“編程”和 “算法”兩個群組),所以對于一篇同時屬于多個群組的文章來說,更新文章的評分意味著程序需要對文章所屬的全部群組執行自增操作。在這種情況下,如果一篇 文章同時屬于很多個群組,那么更新文章評分這一操作可能會變得相當耗時,因此,我們在get_group_articles()函數里面對ZINTERSTORE命令的執行結果進行了緩存處理,以此來盡量減少ZINTERSTORE命令的執行次數。開發者在靈活性或限制條件之間的取舍將改變程序存儲和更新數據的方式,這一點對于任何數據庫都是適用的,Redis也不例外。
練習:實現投反對票的功能
我們的示例目前只實現了投支持票的功能,但是在很多實際的網站里面,反對票也能給用戶提供有用的反饋信息。因此,請讀者能想辦法在article_vote()函數和post_article()函數里面添加投反對票的功能。除此之外,讀者還可以嘗試為用戶提供對調投票的功能:比如將支持票轉換成反對票,或者將反對票轉換成支持票。提示:如果讀者在實現對調投票功能時出現了困難,可以參考一下第3章介紹的SMOVE命令。
好的,現在我們已經成功地構建起了一個展示最受歡迎文章的網站后端,這個網站可以獲取文章、發布文章、對文章進行投票甚至還可以對文章進行分組。 如果你覺得前面展示的內容不好理解,或者弄不懂這些示例,又或者沒辦法運行本書提供的源代碼,那么請閱讀下一節來了解如何獲取幫助。
1.4 尋求幫助
當你遇到與Redis有關的問題時,不要害怕求助于別人,因為其他人可能也遇到過類似的問題。首先,你可以根據錯誤信息在搜索引擎里面進行查找,看是否有所發現。
如果搜索一無所獲,又或者你遇到的問題與本書的示例代碼有關,那么你可以到Manning出版社提供的論壇里面發問( http://www.manning-sandbox.com/forum.jspa?forumID=809 ),我和其他熟悉本書的人將為你提供幫助。
如果你遇到的問題與Redis本身有關,又或者你正在解決的問題在這本書里面沒有出現過,那么你可以到Redis的郵件列表里面發問( https://groups.google.com/d/forum/redis-db/) ,同樣地,我和其他熟悉Redis的人將為你提供幫助。
最后,如果你遇到的問題與某個函數庫或者某種編程語言有關,那么比起在Redis郵件列表里面發帖提問,更好的方法是直接到你正在使用的那個函數庫或者那種編程語言的郵件列表或論壇里面尋求幫助。
1.5 小結
本章對Redis進行了初步的介紹,說明了Redis與其他數據庫的相同之處和不同之處,以及一些讀者可能會使用Redis的理由。在閱讀本書的 后續章節之前,請記住本書的目標并不是構建一個完整的應用或者工具,而是展示各式各樣的問題,并給出使用Redis來解決這些問題的辦法。
本章希望向讀者傳達這樣一個概念:Redis是一個可以用來解決問題的工具,它既擁有其他數據庫不具備的數據結構,又擁有內存存儲(這使得 Redis的速度非常快)、遠程(這使得Redis可以與多個客戶端和服務器進行連接)、持久化(這使得服務器可以在重啟之后仍然保持重啟之前的數據)和 可擴展(通過主從復制和分片)等多個特性,這使得用戶可以以熟悉的方式為各種不同的問題構建解決方案。
在閱讀本書的后續章節時,請讀者注意自己解決問題的方式發生了什么變化:你也許會驚訝地發現,自己思考數據問題的方式已經從原來的“怎樣將我的想法塞進數據庫的表和行里面”,變成了“使用哪種Redis數據結構來解決這個問題比較好呢?”。
本文摘自即將上架的《Redis實戰》
推薦閱讀: 《Redis入門指南(第2版)》
1 分片是一種將數據劃分為多個部分的方法,對數據的劃分可以基于鍵包含的ID、基于鍵的散列值,或者基于以上兩者的某種組合。通過對數據進行分片,用戶可以將數據存儲到多臺機器里面,也可以從多臺機器里面獲取數據,這種方法在解決某些問題時可以獲得線性級別的性能提升。
2 客觀來講,memcached也能用在這個簡單的場景里,但使用Redis存儲聚合數據有以下3個好處:首先,使用Redis可以將彼此相關的聚合數據放 在同一個結構里面,這樣訪問聚合數據就會變得更為容易;其次,使用Redis可以將聚合數據放到有序集合里面,構建出一個實時的排行榜;最后,Redis 的聚合數據可以是整數或者浮點數,而memcached的聚合數據只能是整數。