Redis實戰學習


目錄

1. 為什么要用Redis?

  1. 開源
  2. 高性能
  3. 基於鍵值對的緩存與存儲系統
  4. 提供多種鍵值數據類型來適應不同場景下的緩存與存儲需求

1.1 Redis簡介

1.1.1 存儲結構

1.REmote DIctionary Server(遠程字典服務器)的縮寫,以字典結構存儲數據,允許通過TCP協議讀寫字典中的內容。

2.Redis支持的鍵值數據類型如下:

  • 字符串類型(STRING)
  • 列表類型(LIST)
  • 散列類型(HASH)
  • 集合類型(SET)
  • 有序集合類型(ZSET)

3.傳統MySQL關系數據庫是二維表形式的存儲結構,對復雜的場景還原十分復雜,不是很直觀。Redis中數據的存儲形式和其在程序中的存儲方式非常相近。

1.1.2 內存存儲於持久化

Redis數據庫中的所有數據都存儲在內存中。不過Redis提供了對持久化的支持,可以將內存中的數據異步寫入到硬盤中,同時不影響繼續提供服務。

Redis擁有兩種不同形式的持久化方法:①時間點轉儲(point-in-timedump)②將所有修改了數據庫的命令都寫入一個只追加(append-only)文件里面

1.1.3 功能豐富

Redis可以用作緩存、隊列系統等。它可以設置鍵的生存時間和限定數據占用的最大內存空間,在鍵到期后或數據達到空間限制后,可以淘汰不用的鍵。

1.2 准備

1.2.1 Redis命令

  • redis-cli -h 127.0.0.1 -p 6379 連接redis服務器
  • redis-cli PING 測試客戶端與Redis的連接是否正常
  • redis-cli 進入交互模式,可以自由輸入命令

1.2.2 多數據庫

1.理解:一個Redis實例提供了多個用來存儲數據的字典,客戶端可以指定將數據存儲在哪個字典中。可以將每個字典理解成一個獨立的數據庫。

2.每個數據庫對外都是以0開始的遞增數字命名,默認支持16個數據庫。客戶端與Redis建立連接自動選擇0號數據庫,可以通過SELECT 1命令切換數據庫

3.Redis不支持更改數據庫的名字,開發者需要自己記錄哪些數據庫存儲了哪些數據

4.Redis不支持為每個數據庫設置不同的訪問密碼,一個客戶端要么可以訪問全部,要么都不能訪問

5.多個數據庫之間不是完全隔離的,使用FLUSHALL命令可以清空Redis實例中所有數據庫的數據

6.不同應用應該使用不同的Redis實例存儲數據,Redis非常輕量級,一個空Redis實例占用內存只有1MB左右

1.3 Redis存儲結構

1.3.1 Redis中的字符串

Redis的字符串和其他編程語言或者其他鍵值存儲提供的字符串非常相似。

字符串命令:

  • GET 獲取存儲在給定鍵中的值
  • SET 設置存儲在給定鍵中的值
  • DEL 刪除存儲在給定鍵中的值(這個命令可以用於所有類型)

Redis中的鍵會區分大小寫,命令不會區分大小寫。

1.3.2 Redis中的列表

Redis對鏈表(linked-list)結構支持,一個列表結構可以有序地存儲多個字符串。

Redis列表可執行的操作和很多編程語言里面的列表操作非常相似:

  • LPUSHRPUSH分別用於將元素推入列表的左端(left end)和右端(right end);
  • LPOPRPOP命令分別用於從列表的左端和右端彈出元素;
  • LINDEX命令用於獲取列表在給定位置上的一個元素;
  • LRANGE命令用於獲取列表在給定范圍上的所有元素。

1.3.3 Redis的集合

Redis的集合和列表都可以存儲多個字符串,它們之間的不同在於,列表可以存儲多個相同的字符串,而集合則通過使用散列來保證自己存儲的每個字符串都是各不相同的(這些散列表只有鍵,但沒有與鍵相關的值)。

Redis的集合使用無序方式存儲元素,所以用戶不能像使用列表那樣,將元素推入集合的某一端,或者從集合的某一端彈出元素。

  • SADD 將元素添加到集合
  • SREM從集合里面移除元素
  • SISMEMBER快速地檢查一個元素是否已經存在於集合中
  • SMEMBERS獲取集合包含的所有元素(如果集合包含的元素非常多,那么SMEMBERS命令的執行速度可能會很慢,所以謹慎使用這個命令)

1.3.4 Redis的散列

Redis的散列可以存儲多個鍵值對之間的映射。散列在很多方面就像是一個微縮版的Redis。

  • HSET 在散列里面關聯起給定的鍵值對
  • HGET 獲取指定散列鍵的值
  • HGETALL 獲取散列包含的所有鍵值對
  • HDEL 如果給定鍵存在於散列里面,那么移除這個鍵

1.3.5 Redis的有序集合

有序集合和散列一樣,都用於存儲鍵值對:有序集合的鍵被稱為成員(member),每個成員都是各不相同的;而有序集合的值則被稱為分值(score),分值必須為浮點數

有序集合是Redis里面唯一一個既可以根據成員訪問元素,又可以根據分值以及分值的排列順序來訪問元素的結構。

  • ZADD 將一個帶有給定分值的成員添加到有序集合里面
  • ZRANGE 根據元素在有序集合排列中所處的位置,從有序集合里面獲取多個元素
  • ZRANGEBYSCORE 獲取有序集合在給定分值范圍內的所有元素
  • ZREM 如果給定成員存在於有序集合,那么移除這個成員

1.4 Redis使用實例

1.4.1 對文章進行投票

背景:

現在要構建一個文章投票網站,如果一篇文章獲得了至少200張支持票,那么網站認為這篇文章是一篇有趣的文章;假如這個網站每天發布1000篇文章,而其中的50篇符合網站對有趣文章的要求,那么網站要做的就是把這50篇文章放到文章列表前100位至少一天;該網站暫不支持投反對票的功能。

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

常量是432,這個常量是通過將一天的秒數(86400)除以文章展示一天所需的支持票數量(200)得出的:文章每獲得一張支持票,程序就需要將文章的評分增加432分。

設計基本的存儲結構:

1.文章:我們需要存儲文章的標題、指向文章的網址、發布文章的用戶、文章的發布時間、文章得到的投票數量等信息。最好的存儲方式就是使用散列,一篇文章有這些基本屬性,每個屬性有對應的值;那么我們理應使用散列,一篇文章的標識作為散列的鍵,具體的屬性及值作為散列里面的鍵值對。

2.為了提供文章排序功能,我們使用發布時間和文章評分作為排序標准;文章的唯一標識和文章的發布時間組成鍵值對,文章的唯一標識和文章評分組成鍵值對;既然如此,我們理應使用有序集合來存儲,因為它自動幫我們進行了排序。

3.一篇文章被投票,應該是一對多關系;但是一個用戶和一篇文章之間投票應該是一對一的關系;我們理應使用集合來存儲文章投票的用戶。

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

編碼思路:

當用戶嘗試對一篇文章進行投票時,程序要使用ZSCORE命令檢查記錄文章發布時間的有序集合,判斷文章的發布時間是否超過一周。如果文章仍然處於可以投票的時間范圍之內,那么程序將使用SADD命令,嘗試將用戶添加到記錄文章已投票用戶名單的集合里面。如果添加操作執行成功的話,那么說明用戶是第一次對這篇文章進行投票,程序將使用ZINCRBY命令為文章的評分增加432分(ZINCRBY命令用於對有序集合成員的分值執行自增操作),並使用HINCRBY命令對散列記錄的文章投票數量進行更新(HINCRBY命令用於對散列存儲的值執行自增操作)。

1.4.2 發布文章

發布一篇新文章首先需要創建一個新的文章ID,這項工作可以通過對一個計數器執行INCR命令完成。接着程序使用SADD將文章發布者的ID添加到記錄文章已投票用戶名單的集合里面,並使用EXPIRE命令為這個集合設置一個過期時間,讓Redis在文章發布期滿一周之后自動刪除這個集合。之后,程序會使用HMSET命令來存儲文章的相關信息,並執行兩個ZADD命令,將文章的初始評分和發布時間分別添加到兩個相應的有序集合里面。

1.4.3 獲取文章

如何取出評分最高的文章以及如何取出最新發布的文章?程序先使用ZREVRANGE命令取出多個文章ID,然后對每個文章ID執行一次HGETALL命令來取出文章的詳細信息,這個方法既可以用於取出評分最高的文章,又可以用於取出發布最新的文章。注意一點:因為有序集合會根據成員的分值從小到大地排列元素,所以使用ZREVRANGE命令,以"分值從大到小"的排列順序取出文章ID才是正確的做法。

1.4.4 對文章進行分組

文章現在可以展示最新發布的和評分最高的,但是不具備分類功能。

我們可以使用一個集合來存儲一類文章,利用Redis可以在集合和有序集合間執行操作的特性,就可以得到評分高的這類文章了。

ZINTERSTORE命令可以接受多個集合和多個有序集合作為輸入,找出所有同事存在於集合和有序集合的成員,並以幾種不同的方式合並這些成員的分值。

2. 使用Redis構建Web應用

從高層次的角度來看,Web應用就是通過HTTP協議對網頁瀏覽器發送的請求進行響應的服務器或者服務。一個Web服務器對請求進行響應的典型步驟如下:

  1. 服務器對客戶端發來的請求(request)進行解析
  2. 請求被轉發給一個預定義的處理器(handler)
  3. 處理器可能會從數據庫中取出數據
  4. 處理器根據取出的數據對模板(template)進行渲染(render)
  5. 處理器向客戶端返回渲染后的內容作為對請求的響應(response)

2.1 登錄和cookie緩存

每當登錄互聯網服務的時候,這些服務都會使用cookie來記錄我們的身份。對於用來登錄的cookie,有兩種常見的方法可以將登錄信息存儲在cookie里面:一種事簽名(signed)cookie,另一種是令牌(token)cookie。

簽名cookie:通常會存儲用戶名,可能還有用戶ID、用戶最后一次成功登錄的時間,以及網站覺得有用的其他任何信息。除了用戶的相關信息之外,簽名cookie還包含一個簽名,服務器可以使用這個簽名來驗證瀏覽器發送的信息是否未經改動(比如將cookie中登錄的用戶名改為另一個用戶)。

令牌cookie:會在cookie里面存儲一串隨機字節作為令牌,服務器可以根據令牌在數據庫中查找令牌的擁有者。隨着時間推移,舊令牌會被新令牌取代。

2.1.1 存儲登錄cookie令牌

使用散列存儲登錄cookie令牌與已登錄用戶之間的映射。要檢查一個用戶是否已經登錄,需要根據給定的令牌來查找與之對應的用戶。

  1. 檢查用戶是否已經登錄,需要根據給定的令牌來查找與之對應的用戶,並在用戶已經登錄的情況下,返回該用戶的ID
  2. 用戶每次瀏覽頁面的時候,程序都會對用戶存儲在登錄散列里面的信息進行更新,並將用戶的令牌和當前時間戳添加到記錄最近登錄用戶的有序集合里面;如果用戶正在瀏覽的是一個商品頁面,那么程序還會將這個商品添加到記錄這個用戶最近瀏覽過的商品的有序集合里面,並在被記錄商品的數量超過25個時,對這個有序集合進行修剪。
  3. 因為存儲會話數據所需的內存會隨着時間的推移而不斷增加,所以我們需要定期清理舊的會話數據。為了限制會話數據的數量,我們決定只保留最新的1000萬個會話。清理舊會話的程序由一個循環構成,這個循環每次執行的時候,都會檢查存儲最近登錄令牌的有序集合的大小,如果有序集合的大小超過了限制,那么程序就會從有序集合里面移除最多100個最舊的令牌,並從記錄用戶登錄信息的散列里面,移除被刪除令牌對應的用戶的信息,並對存儲了這些用戶最近瀏覽商品記錄的有序集合進行清理。與此相反,如果令牌的數量未超過限制,那么程序會先休眠1秒,之后再重新進行檢查。

2.1.2 使用Redis實現購物車

使用cookie實現購物車——也就是將整個購物車都存儲到cookie里面的做法非常常見,這種做法的一大優點是無須對數據庫進行寫入就可以實現購物車功能,而缺點則是程序需要重新解析和驗證(validate)cookie,確保cookie的格式正確,並且包含的商品都是真正可購買的商品。cookie購物車還有一個缺點:因為瀏覽器每次發送請求都會連cookie一起發送,所以如果購物車cookie的體積比較大,那么請求發送和處理的速度可能會有所降低。

購物車的定義非常簡單:每個用戶的購物車都是一個散列,這個散列存儲了商品ID與商品訂購數量之間的映射。對商品數量進行驗證的工作有Web應用程序負責,我們要做的則是在商品的訂購數量出現變化時,對購物車進行更新:如果用戶訂購某件商品的數量大於0,那么程序會將這件商品的ID以及用戶訂購該商品的數量添加到散列里面,如果用戶購買的商品已經存在於散列里面,那么新的訂購數量會覆蓋已有的訂購數量;相反地,如果用戶訂購某件商品的數量不大於0,那么程序將從散列里面移除該條目。

2.1.3 網頁緩存

網站所處理的90%頁面每天最多只會改變一次,這些頁面的內容實際上並不需要動態地生成,而我們的工作就是想辦法不再生成這些頁面。減少網站在動態生成內容上面所花的時間,可以降低網站處理相同負載所需的服務器數量,並讓網站的速度變得更快。

我們使用Redis緩存函數:對於一個不能被緩存的請求,函數將直接生成並返回頁面;而對於可以被緩存的請求,函數首先會嘗試從緩存里面取出並返回被緩存的頁面,如果緩存頁面不存在,那么函數會生成頁面並將其緩存在Redis里面5分鍾,最后再將頁面返回給函數調用者。

這個緩存函數對於減少頁面載入時間和降低數據庫負載的作用會更加顯著。

2.1.4 數據行緩存

商品頁面通常只會從數據庫里面載入一兩行數據,包括已登錄用戶的用戶信息和商品本身的信息。即使是那些無法被緩存起來的頁面——比如用戶賬號頁面、記錄用戶以往購買商品的頁面等等,程序也可以通過緩存頁面載入時所需的數據庫行來減少載入頁面所需要的時間。

假設網站為了清空舊庫存和吸引客戶消費,決定開始新一輪的促銷活動:這個活動每天都會推出一些特價商品供用戶搶購,所有特價商品的數量都是限定的,賣完即止。

  1. 在這種情況下,網站是不能對整個促銷頁面進行緩存的,因為這可能導致用戶看到錯誤的特價商品剩余數量
  2. 但是每次載入頁面都從數據庫里面取出特價商品的剩余數量的話,又會給數據庫帶來巨大的壓力,並導致我們需要花費額外的成本來擴展數據庫
  3. 為了應對促銷活動帶來的大量負載,我們需要對數據行進行緩存
  4. 編寫一個持續運行的守護進程函數,讓這個函數將指定的數據行緩存到Redis里面,並不定期地對這些緩存進行更新。
  5. 為了讓緩存函數定期地緩存數據行,程序首先需要將行ID和給定延遲值添加到延遲有序集合里面,然后再將行ID和當前時間的時間戳添加到調度有序集合里面。
  6. 實際執行緩存操作的函數需要用到數據行的延遲值,如果某個數據行的延遲值不存在,那么程序將取消對這個數據行的調度。如果我們想要移除某個數據行已有的緩存,並且讓緩存函數不再緩存那個數據行,那么只需要把那個數據行的延遲值設置為小於或等於0就可以了。
  7. 負責緩存數據行的函數會嘗試讀取調度有序集合的第一個元素以及該元素的分值,如果調度有序集合沒有包含任何元素,或者分值存儲的時間戳所指定的時間尚未來臨,那么函數會先休眠50毫秒,然后再重新進行檢查。
  8. 當緩存程序發現一個需要立即進行更新的數據行時,緩存函數會檢查這個數據行的延遲值:如果數據行的延遲值小於或等於0,那么緩存函數會從延遲有序集合和調度有序集合里面移除這個數據行的ID,並從緩存里面刪除這個數據行已有的緩存,然后再重新進行檢查;對於延遲值大於0的數據行來說,緩存函數會從數據庫里面取出這些行,將他們編碼為JSON格式並存儲到Redis里面,然后更新這些行的調度時間。

通過組合使用調度函數和持續運行緩存函數,我們實現了一種重復進行調度的自動緩存機制,並且可以隨心所欲地控制數據行緩存的更新頻率。如果數據記錄的是特價促銷商品的剩余數量,並且參與促銷活動的用戶非常多的話,那么我們最好每隔幾秒更新一次數據行緩存;另一方面,如果數據並不經常改變,或者商品缺貨是可以接受的,那么我們可以每分鍾更新一次緩存。

2.1.5 網頁分析

網站總共包含100,000件商品,而冒然地緩存所有商品頁面將耗盡整個網站的全部內存,所以我們決定只對其中10,000件商品的頁面進行緩存。新建一個viewed的有序集合,其中放用戶瀏覽的商品及其被訪問次數。conn.zincrby('viewd:',item,-1)表示對商品每訪問一次,就將它的分值-1,以達到訪問最多的排在有序集合最先的位置。除了緩存最常被瀏覽的商品之外,程序還需要發現那些變得越來越流行的新商品,並在合適的時候緩存它們。

為了讓商品瀏覽次數排行榜能保持最新,我們需要定期修剪有序集合的長度並調整已有元素的分值,從而使得新流行的商品也可以在排行榜中占據一席之地。

然后根據排行榜進行判斷頁面是否需要被緩存。

3. Redis命令

3.1 字符串

Redis的字符串就是一個由字節組成的序列,字符串可以存儲以下3中類型的值:

  • 字節串(byte string)
  • 整數
  • 浮點數

Redis字符串執行自增和自減操作的命令:

當用戶將一個值存儲到Redis字符串里面的時候,如果這個值可以被解釋為十進制整數或者浮點數,那么Redis會察覺到這一點,並允許用戶對這個字符串執行各種INCR*和DECR*操作;如果用戶對一個不存在的鍵或者一個保存了空串的鍵執行自增或者自減操作,那么Redis在執行操作時會將這個鍵的值當做是0來處理。如果用戶嘗試對一個值無法被解釋為整數或者浮點數的字符串鍵執行自增或者自減操作,那么Redis將向用戶返回一個錯誤。

Redis還擁有對字節串的其中一部分內容進行讀取或者寫入的操作,下表展示了用來處理字符串子串和二進制位的命令。

3.2 列表

Redis的列表允許用戶從序列的兩端推入或彈出元素,獲取列表元素,以及執行各種常見的列表操作。除此之外,列表還可以用來存儲任務信息、最近瀏覽過的文章或者常用聯系人信息。

有幾個列表命令可以將元素從一個列表移動到另一個列表,或者阻塞(block)執行命令的客戶端直到有其他客戶端給列表添加元素為止。

3.3 集合

Redis的集合以無序的方式來存儲多個各不相同的元素,用戶可以快速地對集合執行添加元素操作、移除元素操作以及檢查一個元素是否存在於集合里。

下面對最常用的集合命令進行介紹,包括插入命令、移除命令、將元素從一個集合移動到另一個集合的命令,以及對多個集合執行交集運算、並集運算和差集運算的命令。

集合真正厲害的地方在於組合和關聯多個集合,其命令如圖:

3.4 散列

Redis的散列可以讓用戶將多個鍵值對存儲到一個Redis鍵里面。從功能上來說,Redis為散列值提供了一些與字符串值相同的特性,使得散列非常適用於將一些相關的數據存儲在一起。我們可以把這種數據聚集看作是關系數據庫中的行。

散列的常用命令:

散列的其他幾個批處理操作命令,以及一些和字符串操作類似的散列命令。

盡管有HGETALL存在,但是HKEYSHVALUES也是非常有用的:如果散列中包含的值非常大,那么用戶可以先使用HKEYS取出散列中包含的所有鍵,然后再使用HGET一個接一個地取出鍵的值,從而避免因為一次獲取多個大體積的值而導致服務器阻塞。

3.5 有序集合

和散列存儲着鍵與值之間的映射類似,有序集合也儲存着成員與分值之間的映射,並且提供了分值處理命令,以及根據分值大小有序地獲取(fetch)或掃描(scan)成員和分值的命令。

下圖展示了一部分常用的有序集合命令:

一些有用的有序集合命令:

3.6 發布與訂閱

一般來說,發布與訂閱(又稱pub/sub)的特點是訂閱者(listener)負責訂閱頻道(channel),發送者(publisher)負責向頻道發送二進制字符串消息(binary string message)。每當有消息被發送至給定頻道時,頻道的所有訂閱者都會收到消息。

下圖是Redis提供的5個發布與訂閱命令。

雖然Redis的發布和訂閱模式非常有用,但是它的使用頻率很低;

因為

  1. 和Redis系統的穩定性有關,對於舊版Redis來說,如果一個客戶端訂閱了某個或某些頻道,但他讀取消息的速度卻不夠快的話,那么不斷積壓的消息就會使得Redis輸出緩沖區的體積變得越來越大,這可能導致Rdis的速度變慢,甚至直接崩潰。也可能導致Redis被操作系統強制殺死,甚至導致操作系統本身不可用。新版的Redis不會出現這種問題,因為它會自動斷開不符合client-output-buffer-limit pubsub配置選項要求的訂閱客戶端。
  2. 和數據傳輸的可靠性有關。任何網絡系統在執行操作時都可能會遇上斷線情況,而斷線產生的連接錯誤通常會使得網絡連接兩端中的其中一端進行重新連接。但是,如果客戶端在執行訂閱操作的過程中斷線,那么客戶端將丟失在斷線期間發送的所有消息,因此依靠頻道來接收消息的用戶可能會對Redis提供的PUBLISH命令和SUBSCRIBE命令的語義感到失望。

3.7 其他命令

下面將要介紹的命令可以用於處理多種類型的數據:首先要介紹的是可以同時處理字符串、集合、列表和散列的SORT命令;之后介紹用於實現基本事務特性的MULTI命令和EXEC命令,這兩個命令可以讓用戶將多個命令當做一個命令來執行;最后要介紹的是幾個不同的自動過期命令,它們可以自動刪除無用數據。

3.7.1 排序

負責執行排序操作的SORT命令可以根據字符串、列表、集合、有序集合、散列這5種鍵里面存儲着的數據,對列表、集合以及有序集合進行排序。

3.7.2 基本的Redis事務

有時候為了同時處理多個結構,我們需要向Redis發送多個命令。盡管Redis有幾個可以在兩個鍵之間復制或者移動元素的命令,但卻沒有那種可以在兩個不同類型之間移動元素的命令。(除了ZUNIONSTORE命令將元素從一個集合復制到一個有序集合)。為了對相同或者不同類型的多個鍵值執行操作,Redis有5個命令可以讓用戶在不被打斷的情況下對多個鍵執行操作,它們是:WATCH MULTI EXEC UNWATCH DISCARD

Redis的基本事務需要用到MULTI命令和EXEC命令,這種事務可以讓一個客戶端在不被其他客戶端打斷的情況下執行多個命令。和關系數據庫那種可以在執行過程中進行回滾的事務不同,在Reids里面,被MULTI命令和EXEC命令包圍的所有命令會一個接一個地執行,直到所有命令都執行完畢為止。當一個事務執行完畢之后,Redis才會處理其他客戶端的命令。

要在Redis里面執行事務,我們首先要執行MULTI命令,然后輸入那些我們想要在事務里面執行的命令,最后再執行EXEC命令。當Redis從一個客戶端那里接收到MULTI命令時,Redis會將這個客戶端之后發送的所有命令都放入到一個隊列里面,直到這個客戶端發送EXEC命令為止,然后Redis就會在不被打斷的情況下,一個接一個地執行存儲在隊列里面的命令。

3.7.3 鍵的過期時間

在使用Redis存儲數據的時候,有些數據可能在某個時間點之后就不再有用了,用戶使用DEL命令顯示地刪除這些無用數據,也可以通過Redis的過期時間特性來讓一個鍵在給定的時限之后自動被刪除。

雖然過期時間特性對於清理緩存數據非常有用,但是對於列表、集合、散列和有序集合這樣的容器來說,鍵過期命令只能為整個鍵設置過期時間,而沒辦法為鍵里面的單個元素設置過期時間。

下面列出了為鍵設置過期時間的命令,以及查看鍵的過期時間的命令。

4 數據安全與性能保障

本章將展示維護數據安全以及應對系統故障的方法。首先介紹Redis的各個持久化選項,這些選項可以讓用戶把自己的數據存儲到硬盤上面。然后通過Redis的復制特性,把不斷更新的數據副本存儲到附加的機器上面,從而提升系統的性能和數據的可靠性。

主要弄懂更多的Redis運作原理,從而學會如何在首先保證數據正確的前提下,加快數據操作的執行速度。

4.1 持久化選項

Redis提供了兩種持久化數據的方式。一種叫做快照(snapshotting),它可以將存在於某一時刻的所有數據都寫入硬盤里面。另一種叫只追加文件(append-only file,AOF),它會在執行命令時,將被執行的寫命令復制到硬盤里面。這兩種持久化方法既可以同時使用,又可以單獨使用,在某些情況下甚至可以兩種方法都不使用,具體選擇哪種持久化方法需要根據用戶的數據及應用來決定。

4.1.1 快照持久化

Redis可以通過創建快照來獲得存儲在內存里面的數據在某個時間點上的副本。根據配置,快照將被寫入dbfilename選項指定的文件里面,並存儲在dir選項指定的路徑上面。如果在新的快照文件創建完畢之前,Redis、系統或者硬件這三者中的任意一個崩潰了,那么Redis將丟失最近一次創建快照之后寫入的所有數據。

所以在只使用持久化來保存數據時,一定要記住:如果系統真的發生崩潰,用戶將丟失最近一次生成快照之后更改的所有數據。因此,快照持久化只適合那些即使丟失一部分數據也不會造成問題的應用程序,而不能接受這種數據損失的應用程序則考慮使用AOF持久化。

4.1.2 AOF持久化

簡單來說,AOF持久化會將被執行的命令寫到AOF文件末尾,以此來記錄數據發生的變化。因此,Redis只要從頭到尾重新執行一次AOF文件包含的所有寫命令,就可以恢復AOF文件所記錄的數據集。

appendfsync配置選項對AOF文件的同步頻率的影響:

雖然AOF持久化非常靈活地提供了多種不同的選項來滿足不同應用程序對數據安全的不同要求,但AOF持久化也有缺陷——那就是AOF文件的體積大小。

為了解決AOF文件體積不斷增大的問題,用戶可以向Redis發送BGREWRITEAOF命令,這個命令會通過移除AOF文件中的冗余命令來重寫AOF文件,使得AOF文件的體積變得盡可能小。BGREWRITEAOF的工作原理和BGSAVE創建快照的工作原理非常相似:Redis會創建一個子進程,然后由子進程負責對AOF文件進行重寫。因為AOF文件重寫也需要用到子進程,所以快照持久化因為創建子進程而導致的性能問題和內存占用問題,在AOF持久化中也同樣存在。更糟糕的是,如果不加以控制的話,AOF文件的體積可能會比快照文件的體積大好幾倍,在進行AOF重寫並刪除舊AOF文件的時候,刪除一個體積達到數十GB大的舊AOF文件可能會導致操作系統掛起數秒。

AOF持久化也可以設置auto-aof-rewrite-percentage選項和auto-aof-rewrite-min-size選項來自動執行BGREWRITEAOF

無論是使用AOF持久化還是快照持久化,將數據持久化到硬盤上都是非常有必要的,除了進行持久化之外,用戶還必須對持久化所得的文件進行備份,這樣才能盡量避免數據丟失事故發生。

4.2 復制

對於擴展平台以適應高負載經驗的工程師和管理員來說,復制(replication)是不可或缺的。復制可以讓其他服務器擁有一個不斷地更新的數據副本,從而使得擁有數據副本的服務器可以用於處理客戶端發送的讀請求。

在需要擴展讀請求的時候,或者在需要寫入臨時數據的時候,用戶可以通過設置額外的Redis從服務器來保存數據集的副本。在收到主服務器發送的數據初始副本之后,客戶端每次向主服務器進行寫入時,從服務器都會實時地得到更新。

4.2.1 Redis復制的啟動過程

下面是當從服務器連接主服務器時,主從服務器執行的所有操作。

Redis在復制進行期間也會盡可能地處理接收到的命令請求,但是,如果主從服務器之間的網絡帶寬不足,或者主服務器沒有足夠的內存來創建子進程和創建記錄寫命令的緩沖區,那么Redis處理命令請求的效率就會受到影響。因此,盡管這並不是必須的,但在實際中最好還是讓主服務器只使用50%-60%的內存,留下30%-45%的內存用於執行BGSAVE命令和創建記錄寫命令的緩沖區。

設置從服務器的步驟非常簡單,用戶既可以通過配置選項SLAVEOF host port來將一個Redis服務器設置為從服務器,又可以通過向運行中的Redis服務器發送SLAVEOF命令來將其設置為從服務器。

如果用戶使用的是SLAVEOF配置選項,那么Redis在啟動時首先會載入當前可用的任何快照文件或者AOF文件,然后連接主服務器並執行表4-2中的復制過程。如果用戶使用的是SLAVEOF命令,那么Redis會立即嘗試連接主服務器,並在連接成功之后,開始表4-2中的復制過程。

從服務器在進行同步時,會清空自己的所有數據:從服務器在與主服務器進行初始連接時,數據庫中原有的所有數據都將丟失,並被替換成主服務器發來的數據。

Redis不支持主主復制

在大部分情況下,Redis都會盡可能地減少復制所需要的工作,然而,如果從服務器連接主服務器的時間並不湊巧,那么主服務器就需要多做一些額外的工作。另一方面,當多個從服務器同時連接主服務器的時候,同步多個從服務器所占用的帶寬可能使得其他命令請求難以傳遞給主服務器,與主服務器位於同一網絡中的其他硬件的網速可能也會因此而降低。

4.2.2 主從鏈

Redis的主服務器和從服務器並沒有特別不同的地方,所以從服務器也可以擁有自己的從服務器,並由此形成主從鏈。(master/slave chaining)

從服務器對從服務器進行復制在操作上和從服務器對主服務器進行復制的唯一區別在於,如果從服務器x擁有從服務器y,那么當服務器x在執行表4-2中步驟4時,它將斷開與從服務器y的連接,導致從服務器y需要重新連接並重新同步。

4.2.3 檢驗硬盤寫入

判斷數據是否已經被保存到硬盤里,用戶可以檢查INFO命令的輸出結果中aof_pending_bio_fsync屬性的值是否為0,如果是的話,那么久表示服務器已經將已知的所有數據都保存到硬盤里了。

4.3 處理系統故障

如果我們決定要將Redis用作應用程序唯一的數據存儲手段,那么就必須確保Redis不會丟失任何數據。跟提供了ACID保證的傳統關系型數據庫不同,在使用Redis為后端構建應用程序的時候,用戶需要多做一些工作才能保證數據的一致性。

Redis是一個軟件,它運行在硬件之上,即使軟件和硬件都設計得完美無瑕,也有可能會出現停電、發電機因為燃料耗盡而無法發電或者備用電池電量消盡等情況。

4.3.1 驗證快照文件和AOF文件

無論是快照持久化還是AOF持久化,都提供了在遇到系統故障時進行數據恢復的工具。Redis提供了兩個命令行程序redis-check-aofredis-check-dump,他們可以在系統故障發生之后,檢查AOF文件和快照文件的狀態,並在有需要的情況下對文件進行修復。

程序修復AOF文件的方法非常簡單:它會掃描給定的AOF文件,尋找不正確或者不完整的命令,當發現第一個出錯命令的時候,程序會刪除出錯的命令以及位於出錯命令之后的所有命令,只保留那些出錯命令之前的正確命令。在大多數情況下,被刪除的都是AOF文件末尾的不完整的寫命令。

遺憾的是,目前並沒有辦法可以修復出錯的快照文件。盡管發現快照文件首個出現錯誤的地方是有可能的,但是因為快照文件本身經過了壓縮,而出現在快照文件中間的錯誤有可能會導致快照文件的剩余部分無法被讀取。因此,用戶最好為重要的快照文件保留多個備份,並在進行數據恢復時,通過計算快照文件的SHA1散列值和SHA256散列值來對內容進行驗證。

4.3.2 更換故障主服務器

在擁有一個主服務器和一個從服務器的情況下,更換主服務器的具體步驟。

假設A、B兩台機器都運行着Redis,其中機器A的Redis為主服務器,而機器B的Redis為從服務器。不巧的是,機器A剛剛因為某個暫時無法修復的故障而斷開了網絡連接,因此用戶決定將同樣安裝了Redis的機器C作為新的主服務器。更換服務器的計划非常簡單:

方案1.首先向機器B發送一個SAVE命令,讓他創建一個新的快照文件,接着將這個快照文件發送給機器C,並在機器C上面啟動Redis。最后,讓機器B成為機器C的從服務器。

下面是替換主節點的命令:

其中注意在機器B上運行的SAVE命令和將機器B設置為機器C的從服務器的SLAVEOF命令。

方案2.將從服務器升級為主服務器,並為升級后的主服務器創建從服務器。

4.4 Redis事務

為了保證數據的正確性,我們必須認識到這一點:在多個客戶端同時處理相同的數據時,不謹慎的操作很容易會導致數據出錯。

Redis的事務和傳統關系數據庫的事務並不相同。在關系數據庫中,用戶首先向數據庫服務發送BEGIN,然后執行各個相互一致(consistent)的寫操作和讀操作,最后,用戶可以選擇發送COMMIT來確認之前所做的修改,或者發送ROLLBACK來放棄那些修改。

在Redis里面也有簡單的方法可以處理一連串相互一致的讀操作和寫操作。Redis的事務以特殊命令MULTI為開始,之后跟着用戶傳入的多個命令,最后以EXEC為結束。但是由於這種簡單的事務在EXEC命令被調用之前不會執行任何實際操作,所以用戶沒辦法根據讀取到的數據來做決定。這個問題看上去似乎無足輕重,但實際上以一致的形式讀取數據將導致某一類型的問題變得難以解決,除此之外,因為在多個事務同時處理同一個對象時通常需要用到二階提交,所以如果事務不能一致的形式讀取數據,那么二階提交將無法實現,從而導致一些原本可以成功執行的事務淪落至執行失敗的地步。

4.4.1 模擬設計實現商品買賣市場

設計用戶和背包的數據結構如下:

商品買賣市場的需求非常簡單:一個用戶(賣家)可以將自己的商品按照給定的價格放到市場上進行銷售,當另一個用戶(買家)購買這個商品時,賣家就會收到錢。

為了將被銷售商品的全部信息都存儲到市場里面,我們將商品的ID和賣家的ID拼接起來,並將拼接的結果用作成員存儲到市場有序集合里面,而商品的售價則用作成員的分值。

通過將所有數據都包含在一起,我們極大地簡化了實現商品買賣市場所需的數據結構,並且因為市場里面的所有商品都按照價格排序,所以針對商品的分頁功能和查找功能都可以很容易地實現。

4.4.2 將商品放到市場上銷售

為了將商品放到市場上進行銷售,程序除了使用MULTI命令和EXEC命令之外,還需要配合使用WATCH命令,有時候甚至還會用到UNWATCHDISCARD命令。在用戶使用WATCH命令對鍵進行監視之后,直到用戶執行EXEC命令的這段時間里面,如果有其他客戶端搶先對任何被監視的鍵進行了替換、更新或刪除等操作,那么當用戶嘗試執行EXEC命令的時候,事務將失敗並返回一個錯誤。通過使用WATCHMULTI/EXECUNWATCH/DISCARD等命令,程序可以在執行某些重要操作的時候,通過確保自己正在使用的數據沒有發生變化來避免數據出錯。

思路:在將一件商品放到市場上進行銷售的時候,程序需要將被銷售的商品添加到記錄市場正在銷售商品的有序集合里面,並且在添加操作執行的過程中,監視賣家的包裹以確保被銷售的商品的確存在於賣家的包裹當中。

因為沒有一個Redis命令可以在移除集合元素的同時,將被移除的元素改名並添加到有序集合里面,所以這里使用了ZADDSREM兩個命令來實現這一操作。

4.4.3 購買商品

購買一件商品的具體方法:程序首先使用WATCH對市場以及買家的個人信息進行監視,然后獲取買家擁有的錢數以及商品的售價,並檢查買家是否有足夠的錢來購買該商品。如果買家沒有足夠的錢,那么程序會取消事務;相反地,如果買家的錢足夠,那么程序首先會將買家支付的錢轉移給賣家,然后將售出的商品移動至買家的包裹,並將該商品從市場中移除。當買家的個人信息或者商品買賣市場出現變化而導致WatchError異常出現時,程序將進行重試,其中最大重試時間為10秒。

在執行商品購買操作的時候,程序除了需要花費大量時間來准備相關數據之外,還需要對商品買賣市場以及買家的個人信息進行監視:監視商品買賣市場是為了確保買家想要購買的商品仍然有售(或者在商品已經被其他人買走時進行提示),而監視買家的個人信息則是為了驗證買家是否有足夠的錢來購買自己想要的商品。當程序確認商品仍然存在並且買家有足夠錢的時候,程序會將被購買的商品移動到買家的包裹里面,並將買家支付的錢轉移給賣家。

當有多個客戶端同時對相同的數據進行操作時,正確地使用事務可以有效防止數據錯誤發生。

4.5 非事務型流水線

MULTIEXEC包裹的命令在執行時不會被其他客戶端打擾。而使用事務的其中一個好處就是底層的客戶端會通過使用流水線來提高事務執行時的性能。下面介紹如何在不使用事務的情況下,通過使用流水線來提升命令的執行性能。

使用非事務型流水線可以獲得相似批處理命令的性能提升,並且可以讓用戶同時執行多個不同的命令。

在需要執行大量命令的情況下,即使命令實際上並不需要放在事務里面執行,但是為了通過一次發送所有命令來減少通信次數並降低延遲值,用戶也可能會將命令包裹在MULTIEXEC里面執行。遺憾的是,MULTIEXEC並不是免費的——他們也消耗資源,並且可能會導致其他重要的命令被延遲執行。不過好消息是,我們實際上可以在不使用MULTIEXEC的情況下,獲得流水線帶來的所有好處。

通過將標准的Redis連接替換成流水線連接,程序可以將通信往返的次數減少至原來的1/2到1/5。

高延遲網絡使用流水線時的速度要比不使用流水線時的速度快5倍,低延遲網絡使用流水線也可以帶來接近4倍的速度提升,而本地網絡的測試結果實際上已經達到了單核環境下使用Redis協議發送和接收命令序列的性能極限。

4.6 關於性能方面的注意事項

對於已經存在的應用程序,我們應該如何判斷這個程序能否被優化?又該如何對它進行優化?

要對Redis的性能進行優化,用戶首先需要弄清除各種類型的Redis命令到底能跑多快,可以通過調用Redis附帶的性能測試程序redis-benchmark來得知。redis-benchmark的運行結果展示了一些常用Redis命令在1秒內可以執行的次數。

下面是我的虛擬機中docker容器安裝的redis鏡像,運行redis-benchmark命令。

redis-benchmark的運行結果展示了一些常用Redis命令在1秒內可以執行的次數。如果用戶在不給定任何參數的情況下運行redis-benchmark,那么redis-benchmark將使用50個客戶端來進行測試。

在考察redis-benchmark的輸出結果時,切記不要將輸出結果看作是應用程序的實際性能,這是因為redis-benchmark不會處理執行命令所獲得的的命令回復,所以它節約了大量用於對命令回復進行語法分析的時間。在一般情況下,對於只使用單個客戶端的redis-benchmark來說,根據被調用命令的復雜度,一個不使用流水線的Python客戶端的性能大概只有redis-benchmark所示性能的50%-60%.

上圖中是出現性能問題以及問題的解決方法。

大部分Redis客戶端庫都提供了某種級別的內置連接池(connection pool)。以Python的Redis客戶端為例,對於每個Redis服務器,用戶只需要創建一個redis.Redis()對象,該對象就會按需創建連接、重用已有的連接並關閉超時的連接,並且Python客戶端的連接池還可以安全地應用於多線程環境和多進程環境。

5 使用Redis構建支持程序

使用日志和計數器來收集系統當前的狀態信息、挖掘正在使用系統的顧客的相關信息、將Redis用作記錄配置信息的字典。

5.1 使用Redis來記錄日志

在構建應用程序和服務的過程中,對正在運行的系統的相關信息的挖掘能力將變得越來越重要:無論是通過挖掘信息來診斷系統問題,還是發現系統中潛在的問題,甚至是挖掘與用戶有關的信息——這些都需要用到日志。

在Linux和Unix中,有兩種常見的記錄日志的方法。

第一種:將日志記錄到文件里面,然后隨着時間流逝不斷地將一個又一個日志行添加到文件里面,並在一段時間之后創建新的日志文件。包括Redis在內的很多軟件都使用這種方法來記錄日志。但這種記錄日志的方式有時候可能會遇上麻煩:因為每個不同服務都會創建不同的日志,而這些服務輪換日志的機制也各不相同,並且也缺少一種能夠方便地聚合所有日志並對其進行處理的常用方法。

第二種:syslog服務是第二種常用的日志記錄方法,這個服務運行在幾乎所有Linux服務器和Unix服務器的514號TCP端口和UDP端口上面。syslog接受其他程序發來的日志消息,並將這些消息有(route)至存儲在硬盤上的各個日志文件里面,除此之外,syslog還負責舊日志的輪換和刪除工作。通過配置,syslog甚至可以將日志消息轉發給其他服務來做進一步的處理。因為對指定日志的輪換和刪除工作都可以交給syslog來完成,所以使用syslog服務比直接將日志寫入文件要方便得多。

5.1.1 最新日志

將最新出現的日志消息以列表的形式存儲到Redis里面:程序使用LPUSH命令將日志消息推入一個列表里面。之后,如果要查看已有日志消息,那么可以使用LRANGE命令來取出列表中的消息。

5.2 計數器和統計數據

通過在一段時間里面持續記錄網站點擊量和數據庫的讀寫次數,我們可以注意到流量的驟增和漸增情況,預測何時需要對服務器進行升級,從而防止系統因為負荷超載而下線。

那么如何使用Redis來實現時間序列計數器,以及如何使用這些計數器來記錄和監測應用程序的行為。

5.2.1 將計數器存儲到Redis里面

為實現網站點擊量的計數器,這個計數器會以不同的時間精度(如1秒、5秒)存儲最新的數據樣本。

實現計數器首先要考慮的就是如何存儲計數器信息。

1.為了對計數器進行更新,我們需要存儲實際的計數器信息。假如現在有一個計數器,它的精度有很多種,那么它的每一種精度存儲使用散列結構,散列的鍵是時間片的開始時間,鍵就是該時間片內的點擊量。每當有用戶點擊網頁,就會觸發該計數器的更新操作,它會更新每一個精度下的點擊量。更新計數器的函數如下:

2.為了能清理計數器包含的舊數據,那么我們需要知道有哪些計數器在進行計數。我們需要一個有序序列,這個序列不能包含任何一個重復元素,並且能夠讓我們一個接一個地遍歷序列中包含的所有元素。那么我們就使用有序集合來實現有序序列,將所有成員的分值設為0,Redis在嘗試按分值對有序集合進行排序的時候,就會改為按成員名進行排序。

3.清理舊計數器只需要遍歷有序集合並刪除其中的舊計數器就可以了。

5.2.2 使用Redis存儲統計數據

程序將值存儲在有序集合里面是為了對存儲着統計信息的有序集合和其他有序集合進行並集計算,並通過MINMAX這兩個聚合函數來篩選相交的元素。

5.3 查找IP所屬城市以及國家

使用Redis而不是傳統的關系數據庫來實現IP所屬地查找功能,是因為Redis實現的IP所屬地查找程序在運行速度上更具優勢。

5.3.1 載入位置表格

https://dev.maxmind.com/geoip/geoip2/geolite2/ 這個網址提供了可免費使用的IP所屬城市數據庫作為測試數據。

實現IP所屬地查找程序需要兩張表,第一張需要根據輸入的IP地址來查找IP所屬城市的ID,而第二張需要根據輸入城市的ID來查找ID對應城市的實際信息。

5.4 服務的發現與配置

對於一個Redis服務器、一個數據庫服務器以及一個Web服務器來說,存儲它們的配置信息不難。但是如果我們有好幾個從服務器的Redis主服務器,或者不同的應用程序設置了不同的Redis服務器,甚至為數據庫也設置了主服務器和從服務器的話,那么存儲這些服務器的配置信息就有難度了。

5.4.1 使用Redis存儲配置信息

通常情況下,使用一個標志來表示Web服務器是否正在維護;那么即使只更新配置中的一個標志,也會導致更新后的配置文件被強制推送到所有Web服務器,服務器可能需要重啟。

那么我們可以嘗試直接把配置寫入Redis里面,只要將配置信息存儲在Redis里面,並編寫應用程序來獲取這些信息,我們就不用再編寫工具來向服務器推送配置信息了,服務器和服務也不用再通過重新載入配置文件的方式來更新配置信息了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM