1. 程式人生 > 其它 >Redis核心技術與實戰

Redis核心技術與實戰

《Redis核心技術與實戰》

高效能主線,包括執行緒模型、資料結構、持久化、網路框架;

高可靠主線,包括主從複製、哨兵機制;

高可擴充套件主線,包括資料分片、負載均衡。

資料結構

02 Redis底層資料結構

Redis 鍵值對中值的資料型別,也就是資料的儲存形式:

String(字串)、List(列表)、Hash(雜湊)、Set(集合)和 Sorted Set(有序集合)

底層資料結構一共有 6 種,分別是簡單動態字串、雙向連結串列、壓縮列表、雜湊表、跳錶和整數陣列

String 型別的底層實現只有一種資料結構,也就是簡單動態字串。而 List、Hash、Set 和 Sorted Set 這四種資料型別,都有兩種底層實現結構。通常情況下,我們會把這四種類型稱為集合型別,它們的特點是一個鍵對應了一個集合的資料

鍵和值用什麼結構組織?

Redis 使用了一個雜湊表來儲存所有鍵值對

一個雜湊表,其實就是一個數組,陣列的每個元素稱為一個雜湊桶。

雜湊桶中的元素儲存的並不是值本身,而是指向具體值的指標。

因為這個雜湊表儲存了所有的鍵值對,所以,我也把它稱為全域性雜湊表

雜湊表的最大好處很明顯,就是讓我們可以用 O(1) 的時間複雜度來快速查詢到鍵值對——我們只需要計算鍵的雜湊值,就可以知道它所對應的雜湊桶位置,然後就可以訪問相應的 entry 元素。

為什麼雜湊表操作變慢了?

答:雜湊表的衝突問題和 rehash 可能帶來的操作阻塞

這裡的雜湊衝突,也就是指,兩個 key 的雜湊值和雜湊桶計算對應關係時,正好落在了同一個雜湊桶中。畢竟,雜湊桶的個數通常要少於 key 的數量

Redis 解決雜湊衝突的方式,就是鏈式雜湊。鏈式雜湊就是指同一個雜湊桶中的多個元素用一個連結串列來儲存,它們之間依次用指標連線。

這裡依然存在一個問題,雜湊衝突鏈上的元素只能通過指標逐一查詢再操作。如果雜湊表裡寫入的資料越來越多,雜湊衝突可能也會越來越多,這就會導致某些雜湊衝突鏈過長,進而導致這個鏈上的元素查詢耗時長,效率降低。所以,Redis 會對雜湊表做 rehash 操作。

rehash 也就是增加現有的雜湊桶數量,讓逐漸增多的 entry 元素能在更多的桶之間分散儲存,減少單個桶中的元素數量,從而減少單個桶中的衝突。

為了使 rehash 操作更高效,Redis 預設使用了兩個全域性雜湊表:雜湊表 1 和雜湊表 2。一開始,當你剛插入資料時,預設使用雜湊表 1,此時的雜湊表 2 並沒有被分配空間。隨著資料逐步增多,Redis 開始執行 rehash,這個過程分為三步:

給雜湊表 2 分配更大的空間,例如是當前雜湊表 1 大小的兩倍;把雜湊表 1 中的資料重新對映並拷貝到雜湊表 2 中;釋放雜湊表 1 的空間。

但是第二步涉及大量的資料拷貝,如果一次性把雜湊表 1 中的資料都遷移完,會造成 Redis 執行緒阻塞

為了避免這個問題,Redis 採用了漸進式 rehash

簡單來說就是在第二步拷貝資料時,Redis 仍然正常處理客戶端請求,每處理一個請求時,從雜湊表 1 中的第一個索引位置開始,順帶著將這個索引位置上的所有 entries 拷貝到雜湊表 2 中;等處理下一個請求時,再順帶拷貝雜湊表 1 中的下一個索引位置的 entries。這樣就巧妙地把一次性大量拷貝的開銷,分攤到了多次處理請求的過程中,避免了耗時操作,保證了資料的快速訪問。

對於 String 型別來說,找到雜湊桶就能直接增刪改查了,所以,雜湊表的 O(1) 操作複雜度也就是它的複雜度了。

集合資料操作效率

集合型別的底層資料結構和操作複雜度:

集合型別的底層資料結構主要有 5 種:整數陣列、雙向連結串列、雜湊表、壓縮列表和跳錶

整數陣列和雙向連結串列很常見,它們的操作特徵都是順序讀寫,也就是通過陣列下標或者連結串列的指標逐個元素訪問,操作複雜度基本是 O(N),操作效率比較低;

壓縮列表實際上類似於一個數組,陣列中的每一個元素都對應儲存一個數據。和陣列不同的是,壓縮列表在表頭有三個欄位 zlbytes、zltail 和 zllen,分別表示列表長度、列表尾的偏移量和列表中的 entry 個數;壓縮列表在表尾還有一個 zlend,表示列表結束。

在壓縮列表中,如果我們要查詢定位第一個元素和最後一個元素,可以通過表頭三個欄位的長度直接定位,複雜度是 O(1)。而查詢其他元素時,就沒有這麼高效了,只能逐個查詢,此時的複雜度就是 O(N) 了。

跳錶在連結串列的基礎上,增加了多級索引,通過索引位置的幾個跳轉,實現資料的快速定位

這個查詢過程就是在多級索引上跳來跳去,最後定位到元素。這也正好符合“跳”表的叫法。當資料量很大時,跳錶的查詢複雜度就是 O(logN)

集合型別的不同操作的複雜度

單元素操作是基礎;範圍操作非常耗時;統計操作通常高效;例外情況只有幾個。

(Redis 從 2.8 版本開始提供了 SCAN 系列操作(包括 HSCAN,SSCAN 和 ZSCAN),這類操作實現了漸進式遍歷,每次只返回有限數量的資料。這樣一來,相比於 HGETALL、SMEMBERS 這類操作來說,就避免了一次性返回所有元素而導致的 Redis 阻塞)

11 儲存單值鍵值對不一定String最好用

String 型別並不是適用於所有場合的,它有一個明顯的短板,就是它儲存資料時所消耗的記憶體空間較多

String 型別還需要額外的記憶體空間記錄資料長度、空間使用等資訊,這些資訊也叫作元資料。當實際儲存的資料長度較小時,元資料的空間開銷就顯得比較大了,有點“喧賓奪主”的意思。

如何用集合型別儲存單值的鍵值對?

在儲存單值的鍵值對時,可以採用基於 Hash 型別的二級編碼方法。這裡說的二級編碼,就是把一個單值的資料拆分成兩部分,前一部分作為 Hash 集合的 key,後一部分作為 Hash 集合的 value

以圖片 ID 1101000060 和圖片儲存物件 ID 3302000080 為例,我們可以把圖片 ID 的前 7 位(1101000)作為 Hash 型別的鍵,把圖片 ID 的最後 3 位(060)和圖片儲存物件 ID 分別作為 Hash 型別值中的 key 和 value。

其實,二級編碼方法中採用的 ID 長度是有講究的:

Redis Hash 型別的兩種底層實現結構,分別是壓縮列表和雜湊表。Hash 型別設定了用壓縮列表儲存資料時的兩個閾值,一旦超過了閾值,Hash 型別就會用雜湊表來儲存資料了。

hash-max-ziplist-entries:表示用壓縮列表儲存時雜湊集合中的最大元素個數。

hash-max-ziplist-value:表示用壓縮列表儲存時雜湊集合中單個元素的最大長度。

為了能充分使用壓縮列表的精簡記憶體佈局,我們一般要控制儲存在 Hash 集合中的元素個數。所以,在剛才的二級編碼中,我們只用圖片 ID 最後 3 位作為 Hash 集合的 key,也就保證了 Hash 集合的元素個數不超過 1000,同時,我們把 hash-max-ziplist-entries 設定為 1000,這樣一來,Hash 集合就可以一直使用壓縮列表來節省記憶體空間了。

Redis容量預估工具

http://www.redis.cn/redis_memory/

12 集合型別常見的四種統計模式

集合型別常見的四種統計模式,包括聚合統計、排序統計、二值狀態統計和基數統計。

聚合統計

,就是指統計多個集合元素的聚合結果,包括:統計多個集合的共有元素(交集統計);把兩個集合相比,統計其中一個集合獨有的元素(差集統計);統計多個集合的所有元素(並集統計)

比如統計手機 App 每天的新增使用者數和第二天的留存使用者數?正好對應了聚合統計:

要完成這個統計任務,我們可以用一個集合記錄所有登入過 App 的使用者 ID,同時,用另一個集合記錄每一天登入過 App 的使用者 ID。然後,再對這兩個集合做聚合統計。

記錄所有登入過 App 的使用者 ID 還是比較簡單的,我們可以直接使用 Set 型別,把 key 設定為 user:id,表示記錄的是使用者 ID,value 就是一個 Set 集合,裡面是所有登入過 App 的使用者 ID,我們可以把這個 Set 叫作累計使用者 Set

我們還需要把每一天登入的使用者 ID,記錄到一個新集合中,我們把這個集合叫作每日使用者 Set,它有兩個特點:key 是 user:id 以及當天日期,例如 user:id:20200803;value 是 Set 集合,記錄當天登入的使用者 ID。

例如:

假設我們的手機 App 在 2020 年 8 月 3 日上線,當天登入的使用者 ID 會被記錄到 key 為 user:id:20200803 的 Set 中,我們計算累計使用者 Set 和 user:id:20200803 Set 的並集結果,結果儲存在 user:id 這個累計使用者 Set 中:

SUNIONSTORE  user:id  user:id  user:id:20200803 

等到 8 月 4 日再統計時,我們把 8 月 4 日登入的使用者 ID 記錄到 user:id:20200804 的 Set 中。我們執行 SDIFFSTORE 命令計算累計使用者 Set 和 user:id:20200804 Set 的差集,結果儲存在 key 為 user:new 的 Set 中:

SDIFFSTORE  user:new  user:id:20200804 user:id  

user:new 這個 Set 中記錄的就是 8 月 4 日的新增使用者

當要計算 8 月 4 日的留存使用者時,我們只需要再計算 user:id:20200803 和 user:id:20200804 兩個 Set 的交集:

SINTERSTORE user:id:rem user:id:20200803 user:id:20200804

注意;這3個命令都會在Redis中生成一個新key,而從庫預設是readonly不可寫的,所以這些命令只能在主庫使用。想在從庫上操作,可以使用SUNION、SDIFF、SINTER,這些命令可以計算出結果,但不會生成新key。

排序統計

在 Redis 常用的 4 個集合型別中(List、Hash、Set、Sorted Set),List 和 Sorted Set 就屬於有序集合。

List 是按照元素進入 List 的順序進行排序的,而 Sorted Set 可以根據元素的權重來排序

在電商網站上提供最新評論列表的場景為例,進行講解:

先說說用 List 的情況。每個商品對應一個 List,這個 List 包含了對這個商品的所有評論,而且會按照評論時間儲存這些評論,每來一個新評論,就用 LPUSH 命令把它插入 List 的隊頭

在實際應用中,網站一般會分頁顯示最新的評論列表,一旦涉及到分頁操作,List 就可能會出現問題了。

在展示第一頁的 3 個評論時,我們可以用下面的命令,得到最新的三條評論 A、B、C:

LRANGE product1 0 2

但是,如果在展示第二頁前,又產生了一個新評論 G,評論 G 就會被 LPUSH 命令插入到評論 List 的隊頭,評論 List 就變成了{G, A, B, C, D, E, F}。此時,再用剛才的命令獲取第二頁評論時,就會發現,評論 C 又被展示出來了,也就是 C、D、E。所以,對比新元素插入前後,List 相同位置上的元素就會發生變化,用 LRANGE 讀取時,就會讀到舊元素。

Sorted Set 就不存在這個問題: Sorted Set 的 ZRANGEBYSCORE 命令就可以按權重排序後返回元素。

假設越新的評論權重越大,目前最新評論的權重是 N,我們執行下面的命令時,就可以獲得最新的 10 條評論:

ZRANGEBYSCORE comments N-9 N

所以,在面對需要展示最新列表、排行榜等場景時,如果資料更新頻繁或者需要分頁顯示,建議你優先考慮使用 Sorted Set。

二值狀態統計

這裡的二值狀態就是指集合元素的取值就只有 0 和 1 兩種。在簽到打卡的場景中,我們只用記錄簽到(1)或未簽到(0),所以它就是非常典型的二值狀態

這個時候,我們就可以選擇 Bitmap。這是 Redis 提供的擴充套件資料型別。

Bitmap 本身是用 String 型別作為底層資料結構實現的一種統計二值狀態的資料型別。

Bitmap 提供了 GETBIT/SETBIT 操作,使用一個偏移值 offset 對 bit 陣列的某一個 bit 位進行讀和寫。不過,需要注意的是,Bitmap 的偏移量是從 0 開始算的,也就是說 offset 的最小值是 0。

假設我們要統計 ID 3000 的使用者在 2020 年 8 月份的簽到情況,就可以按照下面的步驟進行操作。

第一步,執行下面的命令,記錄該使用者 8 月 3 號已簽到。

SETBIT uid:sign:3000:202008 2 1

第二步,檢查該使用者 8 月 3 日是否簽到。

GETBIT uid:sign:3000:202008 2

第三步,統計該使用者在 8 月份的簽到次數

BITCOUNT uid:sign:3000:202008

如果記錄了 1 億個使用者 10 天的簽到情況,你有辦法統計出這 10 天連續簽到的使用者總數嗎?

你可以把每天的日期作為 key,每個 key 對應一個 1 億位的 Bitmap,每一個 bit 對應一個使用者當天的簽到情況。我們對 10 個 Bitmap 做“與”操作,得到的結果也是一個 Bitmap。在這個 Bitmap 中,只有 10 天都簽到的使用者對應的 bit 位上的值才會是 1。最後,我們可以用 BITCOUNT 統計下 Bitmap 中的 1 的個數,這就是連續簽到 10 天的使用者總數了。在記錄海量資料時,Bitmap 能夠有效地節省記憶體空間。

基數統計

基數統計就是指統計一個集合中不重複的元素個數。比如統計網頁的 UV

網頁 UV 的統計有個獨特的地方,就是需要去重,一個使用者一天內的多次訪問只能算作一次。在 Redis 的集合型別中,Set 型別預設支援去重,所以看到有去重需求時,我們可能第一時間就會想到用 Set 型別。

有一個使用者 user1 訪問 page1 時,你把這個資訊加到 Set 中:

SADD page1:uv user1

對於一個搞大促的電商網站而言,這樣的頁面可能有成千上萬個,如果每個頁面都用這樣的一個 Set,就會消耗很大的記憶體空間

這時候,就要用到 Redis 提供的 HyperLogLog 了。

HyperLogLog 是一種用於統計基數的資料集合型別,它的最大優勢就在於,當集合元素數量非常多時,它計算基數所需的空間總是固定的,而且還很小。

在統計 UV 時,你可以用 PFADD 命令(用於向 HyperLogLog 中新增新元素)把訪問頁面的每個使用者都新增到 HyperLogLog 中。

PFADD page1:uv user1 user2 user3 user4 user5

接下來,就可以用 PFCOUNT 命令直接獲得 page1 的 UV 值了,這個命令的作用就是返回 HyperLogLog 的統計結果。

PFCOUNT page1:uv

HyperLogLog 的統計規則是基於概率完成的,所以它給出的統計結果是有一定誤差的,標準誤算率是 0.81%。這也就意味著,你使用 HyperLogLog 統計的 UV 是 100 萬,但實際的 UV 可能是 101 萬。雖然誤差率不算大,但是,如果你需要精確統計結果的話,最好還是繼續用 Set 或 Hash 型別。

13 GEO 位置資訊處理

GEO

Redis 的 5 大基本資料型別:String、List、Hash、Set 和 Sorted Set,它們可以滿足大多數的資料儲存需求,但是在面對海量資料統計時,它們的記憶體開銷很大,而且對於一些特殊的場景,它們是無法支援的。所以,Redis 還提供了 3 種擴充套件資料型別,分別是 Bitmap、HyperLogLog 和 GEO。

在日常生活中,我們越來越依賴搜尋“附近的餐館”、在打車軟體上叫車,這些都離不開基於位置資訊服務(Location-Based Service,LBS)的應用。LBS 應用訪問的資料是和人或物關聯的一組經緯度資訊,而且要能查詢相鄰的經緯度範圍,GEO 就非常適合應用在 LBS 服務的場景中

GEO 型別是把經緯度所在的區間編碼作為 Sorted Set 中元素的權重分數,把和經緯度相關的車輛 ID 作為 Sorted Set 中元素本身的值儲存下來,這樣相鄰經緯度的查詢就可以通過編碼值的大小範圍查詢來實現了。

如何操作 GEO 型別

GEOADD 命令:用於把一組經緯度資訊和相對應的一個 ID 記錄到 GEO 型別集合中;

GEORADIUS 命令:會根據輸入的經緯度位置,查詢以這個經緯度為中心的一定範圍內的其他元素。

以叫車應用的車輛匹配場景為例,

假設車輛 ID 是 33,經緯度位置是(116.034579,39.030452),我們可以用一個 GEO 集合儲存所有車輛的經緯度,集合 key 是 cars:locations。

GEOADD cars:locations 116.034579 39.030452 33

LBS 應用執行下面的命令時,Redis 會根據輸入的使用者的經緯度資訊(116.054579,39.030452 ),查詢以這個經緯度為中心的 5 公里內的車輛資訊,並返回給 LBS 應用。當然, 你可以修改“5”這個引數,來返回更大或更小範圍內的車輛資訊。

GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10

使用 ASC 選項,讓返回的車輛資訊按照距離這個中心位置從近到遠的方式來排序,以方便選擇最近的車輛;還可以使用 COUNT 選項,指定返回的車輛資訊的數量。畢竟,5 公里範圍內的車輛可能有很多,如果返回全部資訊,會佔用比較多的資料頻寬,這個選項可以幫助控制返回的資料量,節省頻寬。

14 如何在Redis中儲存時間序列資料?

時間序列資料的讀寫特點

這種資料的寫入特點很簡單,就是插入資料快,這就要求我們選擇的資料型別,在進行資料插入時,複雜度要低,儘量不要阻塞。

在查詢時間序列資料時,支援單點查詢、範圍查詢和聚合計算

基於 Hash 和 Sorted Set 儲存時間序列資料

用 Hash 型別來實現單鍵的查詢很簡單。但是,Hash 型別有個短板:它並不支援對資料進行範圍查詢。

為了能同時支援按時間戳範圍的查詢,可以用 Sorted Set 來儲存時間序列資料,因為它能夠根據元素的權重分數來排序。我們可以把時間戳作為 Sorted Set 集合的元素分數,把時間點上記錄的資料作為元素本身。

使用 Sorted Set 儲存資料後,我們就可以使用 ZRANGEBYSCORE 命令,按照輸入的最大時間戳和最小時間戳來查詢這個時間範圍內的溫度值了。如下所示,我們來查詢一下在 2020 年 8 月 3 日 9 點 7 分到 9 點 10 分間的所有溫度值:

ZRANGEBYSCORE device:temperature 202008030907 202008030910

第二個問題:如何保證寫入 Hash 和 Sorted Set 是一個原子性的操作呢

Redis 用來實現簡單的事務的 MULTI 和 EXEC 命令。

MULTI 命令:表示一系列原子性操作的開始。收到這個命令後,Redis 就知道,接下來再收到的命令需要放到一個內部佇列中,後續一起執行,保證原子性。

EXEC 命令:表示一系列原子性操作的結束。一旦 Redis 收到了這個命令,就表示所有要保證原子性的命令操作都已經發送完成了。此時,Redis 開始執行剛才放到內部佇列中的所有命令操作。

以儲存裝置狀態資訊的需求為例,我們執行下面的程式碼,把裝置在 2020 年 8 月 3 日 9 時 5 分的溫度,分別用 HSET 命令和 ZADD 命令寫入 Hash 集合和 Sorted Set 集合

127.0.0.1:6379> MULTI
OK

127.0.0.1:6379> HSET device:temperature 202008030911 26.8
QUEUED

127.0.0.1:6379> ZADD device:temperature 202008030911 26.8
QUEUED

127.0.0.1:6379> EXEC
1) (integer) 1
2) (integer) 1

事務中前一命令執行失敗,後一命令也會正常執行吧?redis事務只能保證不被事務外命令打斷,而不能保證要麼都成功,要麼都失敗吧: 是的

如果redis使用叢集部署,還能保證原子性嗎: 不能了,不支援分散式事務。

第三個問題:如何對時間序列資料進行聚合計算

Sorted Set 只支援範圍查詢,無法直接進行聚合計算,所以,我們只能先把時間範圍內的資料取回到客戶端,然後在客戶端自行完成聚合計算。

但是,如果我們需要進行大量的聚合計算,同時網路頻寬條件不是太好時,Hash 和 Sorted Set 的組合就不太適合了。此時,使用 RedisTimeSeries 就更加合適一些。

基於 RedisTimeSeries 模組儲存時間序列資料

RedisTimeSeries 是 Redis 的一個擴充套件模組。它專門面向時間序列資料提供了資料型別和訪問介面,並且支援在 Redis 例項上直接對資料進行按時間範圍的聚合計算。

因為 RedisTimeSeries 不屬於 Redis 的內建功能模組,在使用時,我們需要先把它的原始碼單獨編譯成動態連結庫 redistimeseries.so,再使用 loadmodule 命令進行載入,如下所示:

loadmodule redistimeseries.so

當用於時間序列資料存取時,RedisTimeSeries 的操作主要有 5 個:

用 TS.CREATE 命令建立時間序列資料集合;用 TS.ADD 命令插入資料;用 TS.GET 命令讀取最新資料;用 TS.MGET 命令按標籤過濾查詢資料集合;用 TS.RANGE 支援聚合計算的範圍查詢。

如果你是Redis的開發維護者,你會把聚合計算也設計為Sorted Set的內在功能嗎?

不會。因為聚合計算是CPU密集型任務,Redis在處理請求時是單執行緒的,也就是它在做聚合計算時無法利用到多核CPU來提升計算速度,如果計算量太大,這也會導致Redis的響應延遲變長,影響Redis的效能。Redis的定位就是高效能的記憶體資料庫,要求訪問速度極快。所以對於時序資料的儲存和聚合計算,我覺得更好的方式是交給時序資料庫去做,時序資料庫會針對這些儲存和計算的場景做針對性優化。

快取策略與常用架構

23 Redis作為旁路快取

Redis 作為旁路快取,就意味著需要在應用程式中新增快取邏輯處理的程式碼

Redis 做快取時,還有兩種模式,分別是只讀快取和讀寫快取

只讀快取

假設業務應用要修改資料 A,此時,資料 A 在 Redis 中也快取了,那麼,應用會先直接在資料庫裡修改 A,並把 Redis 中的 A 刪除。等到應用需要讀取資料 A 時,會發生快取缺失,此時,應用從資料庫中讀取 A,並寫入 Redis,以便後續請求從快取中直接讀取

讀寫快取

讀寫快取還提供了同步直寫和非同步寫回這兩種模式,同步直寫模式側重於保證資料可靠性,而非同步寫回模式則側重於提供低延遲訪問

24 快取資料淘汰策略

8種策略:

我們使用 EXPIRE 命令對一批鍵值對設定了過期時間後,無論是這些鍵值對的過期時間是快到了,還是 Redis 的記憶體使用量達到了 maxmemory 閾值,Redis 都會進一步按照 volatile-ttl、volatile-random、volatile-lru、volatile-lfu 這四種策略的具體篩選規則進行淘汰:

volatile-ttl 在篩選時,針對設定了過期時間的鍵值對,根據過期時間先後進行刪除,越早過期越先被刪除;

volatile-random 就像它的名稱一樣,在設定了過期時間的鍵值對中,進行隨機刪除;

volatile-lru 會使用 LRU 演算法篩選設定了過期時間的鍵值對;

volatile-lfu 會使用 LFU 演算法選擇設定了過期時間的鍵值對;

allkeys-lru、allkeys-random、allkeys-lfu 這三種淘汰策略的備選淘汰資料範圍,就擴大到了所有鍵值對,無論這些鍵值對是否設定了過期時間:

allkeys-random 策略,從所有鍵值對中隨機選擇並刪除資料;

allkeys-lru 策略,使用 LRU 演算法在所有資料中進行篩選;

allkeys-lfu 策略,使用 LFU 演算法在所有資料中進行篩選;

LRU 演算法的全稱是 Least Recently Used,從名字上就可以看出,這是按照最近最少使用的原則來篩選資料,最不常用的資料會被篩選出來,而最近頻繁使用的資料會留在快取中

LFU在LRU 演算法的基礎上,同時考慮了資料的訪問時效性和資料的訪問次數,可以看作是對淘汰策略的優化

建議

優先使用 allkeys-lru 策略。這樣,可以充分利用 LRU 這一經典快取演算法的優勢,把最近最常訪問的資料留在快取中,提升應用的訪問效能。如果你的業務資料中有明顯的冷熱資料區分,我建議你使用 allkeys-lru 策略。

如果業務應用中的資料訪問頻率相差不大,沒有明顯的冷熱資料區分,建議使用 allkeys-random 策略,隨機選擇淘汰的資料就行。

如果你的業務中有置頂的需求,比如置頂新聞、置頂視訊,那麼,可以使用 volatile-lru 策略,同時不給這些置頂資料設定過期時間。這樣一來,這些需要置頂的資料一直不會被刪除,而其他資料會在過期時根據 LRU 規則進行篩選。

25 快取資料和資料庫不一致

刪除快取值或更新資料庫失敗而導致資料不一致,你可以使用重試機制確保刪除或更新操作成功;

在刪除快取值、更新資料庫的這兩步操作中,有其他執行緒的併發讀操作,導致其他執行緒讀取到舊值,應對方案是延遲雙刪

26 快取雪崩、快取擊穿和快取穿透

快取雪崩

快取雪崩是指大量的應用請求無法在 Redis 快取中進行處理,緊接著,應用將大量請求傳送到資料庫層,導致資料庫層的壓力激增

原因:

  1. 快取中有大量資料同時過期,導致大量請求無法得到處理;

處理:

微調過期時間;

服務降級,當業務應用訪問的是非核心資料(例如電商商品屬性)時,暫時停止從快取中查詢這些資料,而是直接返回預定義資訊、空值或是錯誤資訊,當業務應用訪問的是核心資料(例如電商商品庫存)時,仍然允許查詢快取,如果快取缺失,也可以繼續通過資料庫讀取

  1. Redis 快取例項發生故障宕機了,無法處理請求;

處理:

在業務系統中實現服務熔斷機制:就是業務應用呼叫快取介面時,快取客戶端並不把請求發給 Redis 快取例項,而是直接返回,等到 Redis 快取例項重新恢復服務後,再允許應用請求傳送到快取系統;

請求限流機制:在業務系統的請求入口前端控制每秒進入系統的請求數,避免過多的請求被髮送到資料庫;

構建 Redis 快取叢集;

快取擊穿

快取擊穿是指,針對某個訪問非常頻繁的熱點資料的請求,無法在快取中進行處理,緊接著,訪問該資料的大量請求,一下子都發送到了後端資料庫,導致了資料庫壓力激增,會影響資料庫處理其他請求。

解決:

解決方法也比較直接,對於訪問特別頻繁的熱點資料,我們就不設定過期時間了

快取穿透

快取穿透是指要訪問的資料既不在 Redis 快取中,也不在資料庫中。如果應用持續有大量請求訪問資料,就會同時給快取和資料庫帶來巨大壓力

原因:

  1. 業務層誤操作:快取中的資料和資料庫中的資料被誤刪除了,所以快取和資料庫中都沒有資料;
  2. 惡意攻擊:專門訪問資料庫中沒有的資料

解決:

第一種方案是,快取空值或預設值;

第二種方案是,使用布隆過濾器快速判斷資料是否存在,避免從資料庫中查詢資料是否存在,減輕資料庫壓力;

布隆過濾器由一個初值都為 0 的 bit 陣列和 N 個雜湊函式組成,可以用來快速判斷某個資料是否存在。N個雜湊函式分別計算某個資料雜湊值,N個值對bit陣列長度取模,對應位置bit位設為1,查詢時N個位置都為1才說明資料存在。

最後一種方案是,在請求入口的前端進行請求檢測,把惡意的請求過濾掉;

27 快取汙染

快取汙染問題指的是留存在快取中的資料,實際不會被再次訪問了,但是又佔據了快取空間

在實際業務應用中,LRU 和 LFU 兩個策略都有應用。LRU 和 LFU 兩個策略關注的資料訪問特徵各有側重,LRU 策略更加關注資料的時效性,而 LFU 策略更加關注資料的訪問頻次。通常情況下,實際應用的負載具有較好的時間區域性性,所以 LRU 策略的應用會更加廣泛。但是,在掃描式查詢的應用場景中,LFU 策略就可以很好地應對快取汙染問題了,建議你優先使用。

33 腦裂:兩個主節點

所謂的腦裂,就是指在主從叢集中,同時有兩個主節點,它們都能接收寫請求。而腦裂最直接的影響,就是客戶端不知道應該往哪個主節點寫入資料,結果就是不同的客戶端會往不同的主節點上寫入資料。而且,嚴重的話,腦裂會進一步導致資料丟失。

是不是資料同步出現了問題

在主從叢集中發生資料丟失,最常見的原因就是主庫的資料還沒有同步到從庫,結果主庫發生了故障,等從庫升級為主庫後,未同步的資料就丟失了。

腦裂發生的原因

和主庫部署在同一臺伺服器上的其他程式臨時佔用了大量資源(例如 CPU 資源),導致主庫資源使用受限,短時間內無法響應心跳。其它程式不再使用資源時,主庫又恢復正常。

主庫自身遇到了阻塞的情況,例如,處理 bigkey 或是發生記憶體 swap,短時間內無法響應心跳,等主庫阻塞解除後,又恢復正常的請求處理了。

為什麼腦裂會導致資料丟失

在主從切換的過程中,如果原主庫只是“假故障”,它會觸發哨兵啟動主從切換,一旦等它從假故障中恢復後,又開始處理請求,這樣一來,就會和新主庫同時存在,形成腦裂。等到哨兵讓原主庫和新主庫做全量同步後,原主庫在切換期間儲存的資料就丟失了。

避免腦裂帶來資料丟失

假設從庫有 K 個,可以將 min-slaves-to-write 設定為 K/2+1(如果 K 等於 1,就設為 1),將 min-slaves-max-lag 設定為十幾秒(例如 10~20s),在這個配置下,如果有一半以上的從庫和主庫進行的 ACK 訊息延遲超過十幾秒,我們就禁止主庫接收客戶端寫請求。

這樣一來,我們可以避免腦裂帶來資料丟失的情況,而且,也不會因為只有少數幾個從庫因為網路阻塞連不上主庫,就禁止主庫接收請求,增加了系統的魯棒性。

訊息佇列

15 Redis 提供的訊息佇列方案

訊息佇列在存取訊息時,必須要滿足三個需求,分別是訊息保序、處理重複的訊息和保證訊息可靠性。

Redis 的 List 和 Streams 兩種資料型別,就可以滿足訊息佇列的這三個需求

基於 List 的訊息佇列解決方案

保序

生產者可以使用 LPUSH 命令把要傳送的訊息依次寫入 List,而消費者則可以使用 RPOP 命令,從 List 的另一端按照訊息的寫入順序,依次讀取訊息並進行處理。

但是,即使沒有新訊息寫入 List,消費者也要不停地呼叫 RPOP 命令,這就會導致消費者程式的 CPU 一直消耗在執行 RPOP 命令上,帶來不必要的效能損失。

Redis 提供了 BRPOP 命令。BRPOP 命令也稱為阻塞式讀取,客戶端在沒有讀到佇列資料時,自動阻塞,直到有新的資料寫入佇列,再開始讀取新資料。和消費者程式自己不停地呼叫 RPOP 命令相比,這種方式能節省 CPU 開銷。

處理重複訊息

消費者程式本身能對重複訊息進行判斷,訊息的全域性唯一 ID 號就需要生產者程式在傳送訊息前自行生成。生成之後,我們在用 LPUSH 命令把訊息插入 List 時,需要在訊息中包含這個全域性唯一 ID:

LPUSH mq "101030001:stock:5"
(integer) 1

可靠性

為了留存訊息,List 型別提供了 BRPOPLPUSH 命令,這個命令的作用是讓消費者程式從一個 List 中讀取訊息,同時,Redis 會把這個訊息再插入到另一個 List(可以叫作備份 List)留存。這樣一來,如果消費者程式讀了訊息但沒能正常處理,等它重啟後,就可以從備份 List 中重新讀取訊息並進行處理了

基於 Streams 的訊息佇列解決方案

Streams 是 Redis 5.0 專門為訊息佇列設計的資料型別,它提供了豐富的訊息佇列操作命令。

XADD:插入訊息,保證有序,可以自動生成全域性唯一 ID;

XREAD:用於讀取訊息,可以按 ID 讀取資料;

XREADGROUP:按消費組形式讀取訊息;

XPENDING 和 XACK:XPENDING 命令可以用來查詢每個消費組內所有消費者已讀取但尚未確認的訊息,而 XACK 命令用於向訊息佇列確認訊息處理已完成。

比如說,我們執行下面的命令,就可以往名稱為 mqstream 的訊息佇列中插入一條訊息,訊息的鍵是 repo,值是 5。其中,訊息佇列名稱後面的*,表示讓 Redis 為插入的資料自動生成一個全域性唯一的 ID,例如“1599203861727-0”。

XADD mqstream * repo 5
"1599203861727-0"

我們可以執行下面的命令,從 ID 號為 1599203861727-0 的訊息開始,讀取後續的所有訊息(示例中一共 3 條)

XREAD BLOCK 100 STREAMS  mqstream 1599203861727-0
1) 1) "mqstream"
   2) 1) 1) "1599274912765-0"
         2) 1) "repo"
            2) "3"
      2) 1) "1599274925823-0"
         2) 1) "repo"
            2) "2"
      3) 1) "1599274927910-0"
         2) 1) "repo"
            2) "1"

設定了 block 100 的配置項,10000 的單位是毫秒,表明 XREAD 在讀取最新訊息時,如果沒有訊息到來,XREAD 將阻塞 100 毫秒(即 10 秒),然後再返回。

Streams 本身可以使用 XGROUP 建立消費組,建立消費組之後,Streams 可以使用 XREADGROUP 命令讓消費組內的消費者讀取訊息,

例如,我們執行下面的命令,建立一個名為 group1 的消費組,這個消費組消費的訊息佇列是 mqstream。

XGROUP create mqstream group1 0
OK

我們再執行一段命令,讓 group1 消費組裡的消費者 consumer1 從 mqstream 中讀取所有訊息,其中,命令最後的引數“>”,表示從第一條尚未被消費的訊息開始讀取。

XREADGROUP group group1 consumer1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599203861727-0"
         2) 1) "repo"
            2) "5"
      2) 1) "1599274912765-0"
         2) 1) "repo"
            2) "3"
      3) 1) "1599274925823-0"
         2) 1) "repo"
            2) "2"
      4) 1) "1599274927910-0"
         2) 1) "repo"
            2) "1"

使用消費組的目的是讓組內的多個消費者共同分擔讀取訊息,所以,我們通常會讓每個消費者讀取部分訊息,從而實現訊息讀取負載在多個消費者間是均衡分佈的。例如,我們執行下列命令,讓 group2 中的 consumer1、2、3 各自讀取一條訊息。

XREADGROUP group group2 consumer1 count 1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599203861727-0"
         2) 1) "repo"
            2) "5"

XREADGROUP group group2 consumer2 count 1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599274912765-0"
         2) 1) "repo"
            2) "3"

XREADGROUP group group2 consumer3 count 1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599274925823-0"
         2) 1) "repo"
            2) "2"

相比 Redis 來說,Kafka 和 RabbitMQ 一般被認為是重量級的訊息佇列。

如果一個生產者傳送給訊息佇列的訊息,需要被多個消費者進行讀取和處理,你會使用Redis的什麼資料型別來解決這個問題?

這種情況下,只能使用Streams資料型別來解決。使用Streams資料型別,建立多個消費者組,就可以實現同時消費生產者的資料。每個消費者組內可以再掛多個消費者分擔讀取訊息進行消費,消費完成後,各自向Redis傳送XACK,標記自己的消費組已經消費到了哪個位置,而且消費組之間互不影響。

高效能

03 高效能IO模型

我們通常說,Redis 是單執行緒,主要是指 Redis 的網路 IO 和鍵值對讀寫是由一個執行緒來完成的,這也是 Redis 對外提供鍵值儲存服務的主要流程。但 Redis 的其他功能,比如持久化、非同步刪除、叢集資料同步等,其實是由額外的執行緒執行的。

Redis 為什麼用單執行緒?

多執行緒會引入複雜的併發控制問題

對於一個多執行緒的系統來說,可以增加系統中處理請求操作的資源實體,進而提升系統能夠同時處理的請求數,即吞吐率。

但是,多執行緒程式設計模式面臨的共享資源的併發訪問控制問題。

併發訪問控制一直是多執行緒開發中的一個難點問題,如果沒有精細的設計,比如說,只是簡單地採用一個粗粒度互斥鎖,就會出現不理想的結果:即使增加了執行緒,大部分執行緒也在等待獲取訪問共享資源的互斥鎖,並行變序列,系統吞吐率並沒有隨著執行緒的增加而增加。

單執行緒 Redis 為什麼那麼快?

通常來說,單執行緒的處理能力要比多執行緒差很多,但是 Redis 卻能使用單執行緒模型達到每秒數十萬級別的處理能力,這是為什麼呢?

一方面,Redis 的大部分操作在記憶體上完成,再加上它採用了高效的資料結構,例如雜湊表和跳錶,這是它實現高效能的一個重要原因。

另一方面,就是 Redis 採用了多路複用機制,使其在網路 IO 操作中能併發處理大量的客戶端請求,實現高吞吐率。

Linux 中的 IO 多路複用機制是指一個執行緒處理多個 IO 流,就是我們經常聽到的 select/epoll 機制。簡單來說,在 Redis 只執行單執行緒的情況下,該機制允許核心中,同時存在多個監聽套接字和已連線套接字。核心會一直監聽這些套接字上的連線請求或資料請求。一旦有請求到達,就會交給 Redis 執行緒處理,這就實現了一個 Redis 執行緒處理多個 IO 流的效果。

16 非同步機制:避免單執行緒模型的阻塞

Redis 例項5個阻塞點

與客戶端互動

網路 IO

不是:因為使用了 IO 多路複用機制

鍵值對增刪改查操作

是:1.集合全量查詢和聚合操作;2.bigkey 刪除操作

資料庫操作

是:3.清空資料庫

與磁碟互動

生成 RDB 快照

不是:用的子程序

記錄 AOF 日誌

是:4.AOF 日誌同步寫

AOF 日誌重寫

不是:用的子程序

主從節點互動

主庫生成 RDB 檔案,並傳輸給從庫

不是:用的子程序

 

從庫接收 RDB 檔案、清空資料庫、載入 RDB 檔案

是:5.從庫載入 RDB 檔案到記憶體

切片叢集例項互動

向其他例項傳輸雜湊槽資訊

不是:雜湊槽的資訊量不大

 

資料遷移

不是:資料遷移是漸進式執行的

在這 5 大阻塞點中,bigkey 刪除、清空資料庫、AOF 日誌同步寫不屬於關鍵路徑操作,可以使用非同步子執行緒機制來完成。

Redis 在執行時會建立三個子執行緒,主執行緒會通過一個任務佇列和三個子執行緒進行互動。子執行緒會根據任務的具體型別,來執行相應的非同步操作。

鍵值對刪除:當你的集合型別中有大量元素(例如有百萬級別或千萬級別元素)需要刪除時,我建議你使用 UNLINK 命令。

清空資料庫:可以在 FLUSHDB 和 FLUSHALL 命令後加上 ASYNC 選項,這樣就可以讓後臺子執行緒非同步地清空資料庫

集合全量查詢和聚合操作、從庫載入 RDB 檔案是在關鍵路徑上,無法使用非同步操作來完成。對於這兩個阻塞點

集合全量查詢和聚合操作:可以使用 SCAN 命令,分批讀取資料,再在客戶端進行聚合計算;
從庫載入 RDB 檔案:把主庫的資料量大小控制在 2~4GB 左右,以保證 RDB 檔案能以較快的速度載入。

17 CPU綁核優化 Redis 效能

在 CPU 多核的場景下,用 taskset 命令把 Redis 例項和一個核繫結,可以減少 Redis 例項在不同核上被來回排程執行的開銷,避免較高的尾延遲;在多 CPU 的 NUMA 架構下,如果你對網路中斷程式做了綁核操作,建議你同時把 Redis 例項和網路中斷程式綁在同一個 CPU Socket 的不同核上,這樣可以避免 Redis 跨 Socket 訪問記憶體中的網路資料的時間開銷。

18 19 如何應對變慢的Redis?

如何確定Redis變慢

檢視 Redis 的響應延遲;

redis-cli 命令提供了–intrinsic-latency 選項,可以用來監測和統計測試期間內的最大延遲:

./redis-cli --intrinsic-latency 120
Max latency so far: 17 microseconds.
Max latency so far: 44 microseconds.
Max latency so far: 94 microseconds.
Max latency so far: 110 microseconds.
Max latency so far: 119 microseconds.

36481658 total runs (avg latency: 3.2893 microseconds / 3289.32 nanoseconds per run).
Worst run took 36x longer than the average latency.

一般來說,你要把執行時延遲和基線效能進行對比,如果你觀察到的 Redis 執行時延遲是其基線效能的 2 倍及以上,就可以認定 Redis 變慢了

Redis 自身的操作特性、檔案系統和作業系統,它們是影響 Redis 效能的三大要素

Redis 自身操作特性的影響

1. 慢查詢命令

當你發現 Redis 效能變慢時,可以通過 Redis 日誌,或者是 latency monitor 工具,查詢變慢的請求

如果的確有大量的慢查詢命令,有兩種處理方式:

1) 用其他高效命令代替。比如說,如果你需要返回一個 SET 中的所有成員時,不要使用 SMEMBERS 命令,而是要使用 SSCAN 多次迭代返回,避免一次返回大量資料,造成執行緒阻塞。

2) 當你需要執行排序、交集、並集操作時,可以在客戶端完成,而不要用 SORT、SUNION、SINTER 這些命令,以免拖慢 Redis 例項。

還有一個比較容易忽略的慢查詢命令,就是 KEYS。因為 KEYS 命令需要遍歷儲存的鍵值對,所以操作延時高。所以,KEYS 命令一般不被建議用於生產環境中

2. 過期 key 操作

刪除操作是阻塞的(Redis 4.0 後可以用非同步執行緒機制來減少阻塞影響)。所以,一旦該條件觸發,Redis 的執行緒就會一直執行刪除

如果一批 key 的確是同時過期,你還可以在 EXPIREAT 和 EXPIRE 的過期時間引數上,加上一個一定大小範圍內的隨機數,這樣,既保證了 key 在一個鄰近時間範圍內被刪除,又避免了同時過期造成的壓力。

檔案系統:AOF 模式

Redis AOF 日誌提供了三種日誌寫回策略:no、everysec、always。這三種寫回策略依賴檔案系統的兩個系統呼叫完成,也就是 write 和 fsync

write 只要把日誌記錄寫到核心緩衝區,就可以返回了,並不需要等待日誌實際寫回到磁碟;而 fsync 需要把日誌記錄寫回到磁碟後才能返回,時間較長。

Redis AOF 配置級別是什麼?業務層面是否的確需要這一可靠性級別?如果我們需要高效能,同時也允許資料丟失,可以將配置項 no-appendfsync-on-rewrite 設定為 yes,避免 AOF 重寫和 fsync 競爭磁碟 IO 資源,導致 Redis 延遲增加。當然, 如果既需要高效能又需要高可靠性,最好使用高速固態盤作為 AOF 日誌的寫入盤。

作業系統:swap

記憶體 swap 是作業系統裡將記憶體資料在記憶體和磁碟間來回換入和換出的機制,涉及到磁碟的讀寫,所以,一旦觸發 swap,無論是被換入資料的程序,還是被換出資料的程序,其效能都會受到慢速磁碟讀寫的影響

通常,觸發 swap 的原因主要是物理機器記憶體不足

解決思路:增加機器的記憶體或者使用 Redis 叢集

作業系統本身會在後臺記錄每個程序的 swap 使用情況,即有多少資料量發生了 swap。你可以先通過下面的命令檢視 Redis 的程序號,這裡是 5332。

$ redis-cli info | grep process_id
process_id: 5332

然後,進入 Redis 所在機器的 /proc 目錄下的該程序目錄中:

$ cd /proc/5332

最後,執行下面的命令,檢視該 Redis 程序的使用情況。在這兒,我只截取了部分結果:

$cat smaps | egrep '^(Swap|Size)'
Size: 584 kB
Swap: 0 kB
Size: 4 kB
Swap: 4 kB
Size: 4 kB
Swap: 0 kB
Size: 462044 kB
Swap: 462008 kB
Size: 21392 kB
Swap: 0 kB

每一行 Size 表示的是 Redis 例項所用的一塊記憶體大小,而 Size 下方的 Swap 和它相對應,表示這塊 Size 大小的記憶體區域有多少已經被換出到磁碟上了。如果這兩個值相等,就表示這塊記憶體區域已經完全被換出到磁碟了。

作業系統:記憶體大頁

客戶端的寫請求可能會修改正在進行持久化的資料。在這一過程中,Redis 就會採用寫時複製機制

如果採用了記憶體大頁,那麼,即使客戶端請求只修改 100B 的資料,Redis 也需要拷貝 2MB 的大頁。相反,如果是常規記憶體頁機制,只用拷貝 4KB。

在實際生產環境中部署時,我建議你不要使用記憶體大頁機制,操作也很簡單,只需要執行下面的命令就可以了:

echo never /sys/kernel/mm/transparent_hugepage/enabled

20 記憶體碎片清理

記憶體碎片是如何形成的

內因:記憶體分配器一般是按固定大小來分配記憶體,而不是完全按照應用程式申請的記憶體空間大小給程式分配

外因:鍵值對大小不一樣和刪改操作

如何判斷是否有記憶體碎片

Redis 自身提供了 INFO 命令

INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
…
mem_fragmentation_ratio:1.86

這裡有一個 mem_fragmentation_ratio 的指標,它表示的就是 Redis 當前的記憶體碎片率

例如,Redis 申請使用了 100 位元組(used_memory),作業系統實際分配了 128 位元組(used_memory_rss),此時,mem_fragmentation_ratio 就是 1.28

mem_fragmentation_ratio 大於 1.5 。這表明記憶體碎片率已經超過了 50%。一般情況下,這個時候,我們就需要採取一些措施來降低記憶體碎片率了

如何清理記憶體碎片

從 4.0-RC3 版本以後,Redis 自身提供了一種記憶體碎片自動清理的方法

Redis 需要啟用自動記憶體碎片清理,可以把 activedefrag 配置項設定為 yes

具體什麼時候清理,會受到下面這兩個引數的控制。這兩個引數分別設定了觸發記憶體清理的一個條件,如果同時滿足這兩個條件,就開始清理

active-defrag-ignore-bytes 100mb:表示記憶體碎片的位元組數達到 100MB 時,開始清理;

active-defrag-threshold-lower 10:表示記憶體碎片空間佔作業系統分配給 Redis 的總空間比例達到 10% 時,開始清理。

還設定了兩個引數,分別用於控制清理操作佔用的 CPU 時間比例的上、下限,既保證清理工作能正常進行

active-defrag-cycle-min 25: 表示自動清理過程所用 CPU 時間的比例不低於 25%,保證清理能正常開展;

active-defrag-cycle-max 75:表示自動清理過程所用 CPU 時間的比例不高於 75%,一旦超過,就停止清理,從而避免在清理時,大量的記憶體拷貝阻塞 Redis,導致響應延遲升高。

21 緩衝區

緩衝區的功能是用一塊記憶體空間來暫時存放命令資料,以免出現因為資料和命令的處理速度慢於傳送速度而導致的資料丟失和效能問題。

Redis 是典型的 client-server 架構,所有的操作命令都需要通過客戶端傳送給伺服器端。所以,緩衝區在 Redis 中的一個主要應用場景,就是在客戶端和伺服器端之間進行通訊時,用來暫存客戶端傳送的命令資料,或者是伺服器端返回給客戶端的資料結果。此外,緩衝區的另一個主要應用場景,是在主從節點間進行資料同步時,用來暫存主節點接收的寫命令和資料。

客戶端輸入和輸出緩衝區

輸入緩衝區就是用來暫存客戶端傳送的請求命令的,所以可能導致溢位的情況主要是下面兩種:

寫入了 bigkey,比如一下子寫入了多個百萬級別的集合型別資料;

伺服器端處理請求的速度過慢;

要檢視和伺服器端相連的每個客戶端對輸入緩衝區的使用情況,我們可以使用 CLIENT LIST 命令:

CLIENT LIST
id=5 addr=127.0.0.1:50487 fd=9 name= age=4 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client

qbuf,表示輸入緩衝區已經使用的大小; qbuf-free,表示輸入緩衝區尚未使用的大小

客戶端輸入緩衝區溢位,Redis 的處理辦法就是把客戶端連線關閉,結果就是業務程式無法進行資料存取了

輸入緩衝區的大小預設是固定的最多1G,我們無法通過配置來修改它

針對命令資料處理較慢的問題,解決方案就是減少 Redis 主執行緒上的阻塞操作,例如使用非同步的刪除操作

如何應對輸出緩衝區溢位:

避免 bigkey 操作返回大量資料結果;

避免在線上環境中持續使用 MONITOR 命令;

使用 client-output-buffer-limit 設定合理的緩衝區大小上限,或是緩衝區連續寫入時間和寫入量上限;

主從叢集中的緩衝區

複製緩衝區的溢位問題:

按通常的使用經驗,我們會把主節點的資料量控制在 2~4GB,這樣可以讓全量同步執行得更快些,避免複製緩衝區累積過多命令

複製積壓緩衝區的溢位問題:

複製積壓緩衝區是一個大小有限的環形緩衝區。當主節點把複製積壓緩衝區寫滿後,會覆蓋緩衝區中的舊命令資料;可以調整複製積壓緩衝區的大小,也就是設定 repl_backlog_size 這個引數的值

持久化與主從模式

04 AOF日誌

Redis為什麼要持久化

一旦伺服器宕機,記憶體中的資料將全部丟失。我們很容易想到的一個解決方案是,從後端資料庫恢復這些資料,但這種方式存在兩個問題:一是,需要頻繁訪問資料庫,會給資料庫帶來巨大的壓力;二是,這些資料是從慢速資料庫中讀取出來的,效能肯定比不上從 Redis 中讀取,導致使用這些資料的應用程式響應變慢。所以,對 Redis 來說,實現資料的持久化,避免從後端資料庫中進行恢復,是至關重要的。

Redis 的持久化主要有兩大機制,即 AOF(Append Only File)日誌和 RDB 快照。

AOF,是寫後日志,

好處是,可以避免出現記錄錯誤命令的情況;它是在命令執行後才記錄日誌,所以不會阻塞當前的寫操作。

風險是,如果剛執行完一個命令,還沒有來得及記日誌就宕機了,那麼這個命令和相應的資料就有丟失的風險;AOF 雖然避免了對當前命令的阻塞,但可能會給下一個操作帶來阻塞風險,AOF 日誌也是在主執行緒中執行的

三種寫回策略

AOF 機制給我們提供了三個選擇,也就是 AOF 配置項 appendfsync 的三個可選值:

Always,同步寫回:每個寫命令執行完,立馬同步地將日誌寫回磁碟;

Everysec,每秒寫回:每個寫命令執行完,只是先把日誌寫到 AOF 檔案的記憶體緩衝區,每隔一秒把緩衝區中的內容寫入磁碟;

No,作業系統控制的寫回:每個寫命令執行完,只是先把日誌寫到 AOF 檔案的記憶體緩衝區,由作業系統決定何時將緩衝區內容寫回磁碟。

AOF日誌檔案太大了怎麼辦?

AOF 重寫機制就是在重寫時,Redis 根據資料庫的現狀建立一個新的 AOF 檔案,也就是說,讀取資料庫中的所有鍵值對,然後對每一個鍵值對用一條命令記錄它的寫入,(當我們對一個列表先後做了 6 次修改操作後,列表的最後狀態是[“D”, “C”, “N”],此時,只用 LPUSH u:list “N”, “C”, "D"這一條命令就能實現該資料的恢復,這就節省了五條命令的空間。)

和 AOF 日誌由主執行緒寫回不同,重寫過程是由後臺子程序 bgrewriteaof 來完成的,這也是為了避免阻塞主執行緒,導致資料庫效能下降。

05 記憶體快照RDB

用 AOF 方法進行故障恢復的時候,需要逐一把操作日誌都執行一遍。如果操作日誌非常多,Redis 就會恢復得很緩慢,影響到正常使用

另一種持久化方法:記憶體快照, RDB 就是 Redis DataBase 的縮寫。

和 AOF 相比,RDB 記錄的是某一時刻的資料,並不是操作,所以,在做資料恢復時,我們可以直接把 RDB 檔案讀入記憶體,很快地完成恢復。聽起來好像很不錯,但記憶體快照也並不是最優選項。為什麼這麼說呢?

我們還要考慮兩個關鍵問題:

給哪些記憶體資料做快照?

Redis 的資料都在記憶體中,為了提供所有資料的可靠性保證,它執行的是全量快照

Redis 提供了兩個命令來生成 RDB 檔案,分別是 save 和 bgsave。

save:在主執行緒中執行,會導致阻塞;bgsave:建立一個子程序,專門用於寫入 RDB 檔案,避免了主執行緒的阻塞,這也是 Redis RDB 檔案生成的預設配置。好了,這個時候,我們就可以通過 bgsave 命令來執行全量快照,這既提供了資料的可靠性保證,也避免了對 Redis 的效能影響。

在對記憶體資料做快照時,這些資料還能被修改嗎?

為了快照而暫停寫操作,肯定是不能接受的。所以這個時候,Redis 就會藉助作業系統提供的寫時複製技術(Copy-On-Write, COW),在執行快照的同時,正常處理寫操作。

簡單來說,bgsave 子程序是由主執行緒 fork 生成的,可以共享主執行緒的所有記憶體資料。bgsave 子程序執行後,開始讀取主執行緒的記憶體資料,並把它們寫入 RDB 檔案。此時,如果主執行緒對這些資料也都是讀操作(例如圖中的鍵值對 A),那麼,主執行緒和 bgsave 子程序相互不影響。但是,如果主執行緒要修改一塊資料(例如圖中的鍵值對 C),那麼,這塊資料就會被複制一份,生成該資料的副本(鍵值對 C’)。然後,主執行緒在這個資料副本上進行修改。同時,bgsave 子程序可以繼續把原來的資料(鍵值對 C)寫入 RDB 檔案。

雖然跟 AOF 相比,快照的恢復速度快,但是,快照的頻率不好把握,如果頻率太低,兩次快照間一旦宕機,就可能有比較多的資料丟失。如果頻率太高,又會產生額外開銷

Redis 4.0 中提出了一個混合使用 AOF 日誌和記憶體快照的方法

簡單來說,記憶體快照以一定的頻率執行,在兩次快照之間,使用 AOF 日誌記錄這期間的所有命令操作。

T1 和 T2 時刻的修改,用 AOF 日誌記錄,等到第二次做全量快照時,就可以清空 AOF 日誌,因為此時的修改都已經記錄到快照中了,恢復時就不再用日誌了。

06 主從庫資料同步

Redis 具有高可靠性,其實,這裡有兩層含義:一是資料儘量少丟失,二是服務儘量少中斷

AOF 和 RDB 保證了前者,而對於後者,Redis 的做法就是增加副本冗餘量,將一份資料同時儲存在多個例項上。

Redis 提供了主從庫模式,以保證資料副本的一致,主從庫之間採用的是讀寫分離的方式。

讀操作:主庫、從庫都可以接收;

寫操作:首先到主庫執行,然後,主庫將寫操作同步給從庫。

主從庫間如何進行第一次同步?

例如,現在有例項 1(ip:172.16.19.3)和例項 2(ip:172.16.19.5),我們在例項 2 上執行以下這個命令後,例項 2 就變成了例項 1 的從庫,並從例項 1 上覆制資料:

replicaof 172.16.19.3 6379

主從庫間資料第一次同步的三個階段:

我們可以通過“主 - 從 - 從”模式將主庫生成 RDB 和傳輸 RDB 的壓力,以級聯的方式分散到從庫上。

主從庫間網路斷了怎麼辦?

在 Redis 2.8 之前,如果主從庫在命令傳播時出現了網路閃斷,那麼,從庫就會和主庫重新進行一次全量複製,開銷非常大。從 Redis 2.8 開始,網路斷了之後,主從庫會採用增量複製的方式繼續同步。

增量複製時,主從庫之間具體是怎麼保持同步的呢?這裡的奧妙就在於 repl_backlog_buffer 這個緩衝區。

repl_backlog_buffer 是一個環形緩衝區,主庫會記錄自己寫到的位置,從庫則會記錄自己已經讀到的位置。

如果從庫的讀取速度比較慢,就有可能導致從庫還未讀取的操作被主庫新寫的操作覆蓋了,這會導致主從庫間的資料不一致。因此,我們要想辦法避免這一情況,一般而言,我們可以調整 repl_backlog_size 這個引數。

如果併發請求量非常大,連兩倍的緩衝空間都存不下新操作請求的話,此時,主從庫資料仍然可能不一致。

針對這種情況,一方面,你可以根據 Redis 所在伺服器的記憶體資源再適當增加 repl_backlog_size 值,比如說設定成緩衝空間大小的 4 倍,另一方面,你可以考慮使用切片叢集來分擔單個主庫的請求壓力

練習題:

使用一個 2 核 CPU、4GB 記憶體、500GB 磁碟的雲主機執行 Redis,Redis 資料庫的資料量大小差不多是 2GB。當時 Redis 主要以修改操作為主,寫讀比例差不多在 8:2 左右,也就是說,如果有 100 個請求,80 個請求執行的是修改操作。在這個場景下,用 RDB 做持久化有什麼風險嗎?

記憶體不足的風險:Redis fork 一個 bgsave 子程序進行 RDB 寫入,如果主執行緒再接收到寫操作,就會採用寫時複製。寫時複製需要給寫操作的資料分配新的記憶體空間。本問題中寫的比例為 80%,那麼,在持久化過程中,為了儲存 80% 寫操作涉及的資料,寫時複製機制會在例項記憶體中,為這些資料再分配新記憶體空間,分配的記憶體量相當於整個例項資料量的 80%,大約是 1.6GB,這樣一來,整個系統記憶體的使用量就接近飽和了。此時,如果例項還有大量的新 key 寫入或 key 修改,雲主機記憶體很快就會被吃光。如果雲主機開啟了 Swap 機制,就會有一部分資料被換到磁碟上,當訪問磁碟上的這部分資料時,效能會急劇下降。如果雲主機沒有開啟 Swap,會直接觸發 OOM,整個 Redis 例項會面臨被系統 kill 掉的風險。

主執行緒和子程序競爭使用 CPU 的風險:生成 RDB 的子程序需要 CPU 核執行,主執行緒本身也需要 CPU 核執行,而且,如果 Redis 還啟用了後臺執行緒,此時,主執行緒、子程序和後臺執行緒都會競爭 CPU 資源。由於雲主機只有 2 核 CPU,這就會影響到主執行緒處理請求的速度。

32 主從同步與故障切換有哪些坑

Redis 同時使用了兩種策略來刪除過期的資料,分別是惰性刪除策略和定期刪除策略

惰性刪除策略。當一個數據的過期時間到了以後,並不會立即刪除資料,而是等到再有請求來讀寫這個資料時,對資料進行檢查,如果發現數據已經過期了,再刪除這個資料。

定期刪除策略是指,Redis 每隔一段時間(預設 100ms),就會隨機選出一定數量的資料,檢查它們是否過期,並把其中過期的資料刪除。

我們在應用主從叢集時,要注意將 protected-mode 配置項設定為 no,並且將 bind 配置項設定為其它哨兵例項的 IP 地址。這樣一來,只有在 bind 中設定了 IP 地址的哨兵,才可以訪問當前例項,既保證了例項間能夠通訊進行主從切換,也保證了哨兵的安全性。

哨兵機制

07 哨兵機制

在 Redis 主從叢集中,哨兵機制是實現主從庫自動切換的關鍵機制,它有效地解決了主從複製模式下故障轉移

哨兵機制的基本流程

哨兵主要負責的就是三個任務:監控、選主和通知。

主觀下線和客觀下線

哨兵程序會使用 PING 命令檢測它自己和主、從庫的網路連線情況,用來判斷例項的狀態。如果哨兵發現主庫或從庫對 PING 命令的響應超時了,那麼,哨兵就會先把它標記為“主觀下線”

如果檢測的是主庫,那麼,哨兵還不能簡單地把它標記為“主觀下線”,開啟主從切換。因為很有可能存在這麼一個情況:那就是哨兵誤判了,其實主庫並沒有故障。

誤判一般會發生在叢集網路壓力較大、網路擁塞,或者是主庫本身壓力較大的情況下。

那怎麼減少誤判呢?通常會採用多例項組成的叢集模式進行部署,這也被稱為哨兵叢集。引入多個哨兵例項一起來判斷,就可以避免單個哨兵因為自身網路狀況不好,而誤判主庫下線的情況。只有到了quorum 配置項值或者大多數的哨兵例項,都判斷主庫已經“主觀下線”了,主庫才會被標記為“客觀下線”,這個叫法也是表明主庫下線成為一個客觀事實了。這個判斷原則就是:少數服從多數。

如何選定新主庫?

把哨兵選擇新主庫的過程稱為“篩選 + 打分”

我們在多個從庫中,先按照一定的篩選條件,把不符合條件的從庫去掉。然後,我們再按照一定的規則,給剩下的從庫逐個打分,將得分最高的從庫選為新主庫

篩選,按照線上狀態、網路狀態,篩選過濾掉一部分不符合要求的從庫

打分,依次從庫優先順序(可以配置)、從庫複製進度(和舊主庫同步程度最接近)以及從庫 ID 號(最小的例項ID號)打分

問題 1:在主從切換過程中,客戶端能否正常地進行請求操作呢?

主從叢集一般是採用讀寫分離模式,當主庫故障後,客戶端仍然可以把讀請求傳送給從庫,讓從庫服務。但是,對於寫請求操作,客戶端就無法執行了。

問題 2:如果想要應用程式不感知服務的中斷,還需要哨兵或客戶端再做些什麼嗎?

一方面,客戶端需要能快取應用傳送的寫請求。只要不是同步寫操作(Redis 應用場景一般也沒有同步寫),寫請求通常不會在應用程式的關鍵路徑上,所以,客戶端快取寫請求後,給應用程式返回一個確認就行。

另一方面,主從切換完成後,客戶端要能和新主庫重新建立連線,哨兵需要提供訂閱頻道,讓客戶端能夠訂閱到新主庫的資訊。同時,客戶端也需要能主動和哨兵通訊,詢問新主庫的資訊。

08 哨兵叢集

一旦多個例項組成了哨兵叢集,即使有哨兵例項出現故障掛掉了,其他哨兵還能繼續協作完成主從庫切換的工作

哨兵之間互通機制:基於pub/sub機制,在主庫中有一個"__sentinel__:hello"的頻道,哨兵之間互相發現通訊

哨兵與主從庫互通機制:哨兵向主庫傳送INFO指令,可以獲取所有從庫的資訊,實現對主庫,從庫的監控

客戶端和哨兵之間的事件通知:基於哨兵自身的 pub/sub 功能,哨兵是一個特殊的redis例項,所以客戶端可以訂閱哨兵的指定頻道獲得redis主從庫的資訊

哨兵叢集執行主從切換機制:誰發現,誰就發起投票流程,誰獲得多數票,誰就是哨兵Leader,由Leader負責主從庫切換

任何一個例項只要自身判斷主庫“主觀下線”後,就會給其他例項傳送 is-master-down-by-addr 命令。接著,其他例項會根據自己和主庫的連線情況,做出 Y 或 N 的響應,Y 相當於贊成票,N 相當於反對票。一個哨兵獲得了仲裁所需的贊成票數後,1.就可以標記主庫為“客觀下線”

此時,2.這個哨兵就可以再給其他哨兵傳送命令,表明希望由自己來執行主從切換,並讓所有其他哨兵進行投票。這個投票過程稱為“Leader 選舉”。

在投票過程中,任何一個想成為 Leader 的哨兵,要滿足兩個條件:第一,拿到半數以上的贊成票;第二,拿到的票數同時還需要大於等於哨兵配置檔案中的 quorum 值。

如果哨兵叢集只有 2 個例項,此時,一個哨兵要想成為 Leader,必須獲得 2 票,而不是 1 票。所以,如果有個哨兵掛掉了,那麼,此時的叢集是無法進行主從庫切換的。因此,通常我們至少會配置 3 個哨兵例項。

練習 1:5 個哨兵例項的叢集,quorum 值設為 2。在執行過程中,如果有 3 個哨兵例項都發生故障了,此時,Redis 主庫如果有故障,還能正確地判斷主庫“客觀下線”嗎?如果可以的話,還能進行主從庫自動切換嗎?

因為判定主庫“客觀下線”的依據是,認為主庫“主觀下線”的哨兵個數要大於等於 quorum 值,現在還剩 2 個哨兵例項,個數正好等於 quorum 值,所以還能正常判斷主庫是否處於“客觀下線”狀態。如果一個哨兵想要執行主從切換,就要獲到半數以上的哨兵投票贊成,也就是至少需要 3 個哨兵投票贊成。但是,現在只有 2 個哨兵了,所以就無法進行主從切換了。

練習2:哨兵例項是不是越多越好呢?如果同時調大 down-after-milliseconds 值,對減少誤判是不是也有好處?

哨兵例項越多,誤判率會越低,但是在判定主庫下線和選舉 Leader 時,例項需要拿到的贊成票數也越多,等待所有哨兵投完票的時間可能也會相應增加,主從庫切換的時間也會變長,客戶端容易堆積較多的請求操作,可能會導致客戶端請求溢位,從而造成請求丟失。如果業務層對 Redis 的操作有響應時間要求,就可能會因為新主庫一直沒有選定,新操作無法執行而發生超時報警。

調大 down-after-milliseconds 後,可能會導致這樣的情況:主庫實際已經發生故障了,但是哨兵過了很長時間才判斷出來,這就會影響到 Redis 對業務的可用性。

分散式/切片叢集

09 切片叢集

切片叢集,也叫分片叢集,就是指啟動多個 Redis 例項組成一個叢集,然後按照一定的規則,把收到的資料劃分成多份,每一份用一個例項來儲存。

如何儲存更多資料?

Redis 應對資料量增多的兩種方案:縱向擴充套件(scale up)和橫向擴充套件(scale out)

縱向擴充套件:升級單個 Redis 例項的資源配置,包括增加記憶體容量、增加磁碟容量、使用更高配置的 CPU。

橫向擴充套件:橫向增加當前 Redis 例項的個數

縱向擴充套件好處是,實施起來簡單、直接;第一個問題是,當使用 RDB 對資料進行持久化時,如果資料量增加,需要的記憶體也會增加,主執行緒 fork 子程序時就可能會阻塞;第二個問題:縱向擴充套件會受到硬體和成本的上限限制

在面向百萬、千萬級別的使用者規模時,橫向擴充套件的 Redis 切片叢集會是一個非常好的選擇。

切片叢集多個例項的分散式管理問題

從 3.0 開始,官方提供了一個名為 Redis Cluster 的方案,用於實現切片叢集。Redis Cluster 方案中就規定了資料和例項的對應規則。

Redis Cluster 方案採用雜湊槽(Hash Slot,接下來我會直接稱之為 Slot),來處理資料和例項之間的對映關係。在 Redis Cluster 方案中,一個切片叢集共有 16384 個雜湊槽,這些雜湊槽類似於資料分割槽,每個鍵值對都會根據它的 key,被對映到一個雜湊槽中。例如,如果叢集中有 N 個例項,那麼,每個例項上的槽個數為 16384/N 個。

客戶端如何定位資料?

客戶端和叢集例項建立連線後,例項就會把雜湊槽的分配資訊發給客戶端。客戶端收到雜湊槽資訊後,會把雜湊槽資訊快取在本地。當客戶端請求鍵值對時,會先計算鍵所對應的雜湊槽,然後就可以給相應的例項傳送請求了。

另外,叢集的例項增減,或者是為了實現負載均衡而進行的資料重新分佈,會導致雜湊槽和例項的對映關係發生變化,客戶端傳送請求時,會收到命令執行報錯資訊。瞭解了 MOVED 和 ASK 命令,你就不會為這類報錯而頭疼了。

問題:為什麼 Redis 不直接用一個表,把鍵值對和例項的對應關係記錄下來?

如果使用表記錄鍵值對和例項的對應關係,一旦鍵值對和例項的對應關係發生了變化(例如例項有增減或者資料重新分佈),就要修改表。如果是單執行緒操作表,那麼所有操作都要序列執行,效能慢;如果是多執行緒操作表,就涉及到加鎖開銷。此外,如果資料量非常大,使用表記錄鍵值對和例項的對應關係,需要的額外儲存空間也會增加。基於雜湊槽計算時,雖然也要記錄雜湊槽和例項的對應關係,但是雜湊槽的個數要比鍵值對的個數少很多,無論是修改雜湊槽和例項的對應關係,還是使用額外空間儲存雜湊槽和例項的對應關係,都比直接記錄鍵值對和例項的關係的開銷小得多。

29 併發訪問控制:原子操作

併發訪問中需要對什麼進行控制

併發訪問控制,是指對多個客戶端訪問操作同一份資料的過程進行控制,以保證任何一個客戶端傳送的操作在 Redis 例項上執行時具有互斥性。例如,客戶端 A 的訪問操作在執行時,客戶端 B 的操作不能執行,需要等到 A 的操作結束後,才能執行。

加鎖會導致系統併發效能降低,和加鎖類似,原子操作也能實現併發控制,但是原子操作對系統併發效能的影響較小

如果我們執行的 RMW 操作是對資料進行增減值的話,Redis 提供的原子操作 INCR 和 DECR 可以直接幫助我們進行併發控制;

如果我們有多個操作要執行,但是又無法用 INCR/DECR 這種命令操作來實現,可以把這些要執行的操作編寫到一個 Lua 指令碼中。然後,我們可以使用 Redis 的 EVAL 命令來執行指令碼

30 使用Redis實現分散式鎖

Redis 屬於分散式系統,當有多個客戶端需要爭搶鎖時,我們必須要保證,這把鎖不能是某個客戶端本地的鎖。否則的話,其它客戶端是無法訪問這把鎖的,當然也就不能獲取這把鎖了,此時,鎖是儲存在一個共享儲存系統中的,可以被多個客戶端共享訪問和獲取。

基於單個 Redis 節點實現分散式鎖

SETNX 命令,在執行時會判斷鍵值對是否存在,如果不存在,就設定鍵值對的值,如果存在,就不做任何設定。

我們就可以用 SETNX 和 DEL 命令組合來實現加鎖和釋放鎖操作,同時給鎖變數設定一個過期時間防止沒主動釋放;

// 加鎖, unique_value作為客戶端唯一性的標識
SET lock_key unique_value NX PX 10000

基於多個 Redis 節點實現高可靠的分散式鎖

為了避免 Redis 例項故障而導致的鎖無法工作的問題,Redis 的開發者 Antirez 提出了分散式鎖演算法 Redlock

Redlock 演算法的基本思路,是讓客戶端和多個獨立的 Redis 例項依次請求加鎖,如果客戶端能夠和半數以上的例項成功地完成加鎖操作。這裡的依次加鎖操作和在單例項上執行的加鎖操作一樣,使用 SET 命令,帶上 NX,EX/PX 選項,以及帶上客戶端的唯一標識。

練習

我們是可以用下面的方式來實現加鎖操作呢

// 加鎖
SETNX lock_key unique_value
EXPIRE lock_key 10S
// 業務邏輯
DO THINGS

不可以這麼使用。使用 2 個命令無法保證操作的原子性,在異常情況下,加鎖結果會不符合預期。

31 Redis事務機制能實現ACID屬性嗎

事務是資料庫的一個重要功能。所謂的事務,就是指對資料進行讀寫的一系列操作。事務在執行時,會提供專門的屬性保證,包括原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和永續性(Durability),也就是 ACID 屬性。

原子性。原子性的要求很明確,就是一個事務中的多個操作必須都完成,或者都不完成。

一致性。就是指資料庫中的資料從一個正確的狀態到另一個正確的狀態,沒有約束條件被破壞

隔離性。它要求資料庫在執行一個事務時,其它操作無法存取到正在執行事務訪問的資料。

永續性。資料庫執行事務後,資料的修改要被持久化儲存下來。

Redis 如何實現事務

事務的執行過程包含三個步驟,Redis 提供了 MULTI、EXEC 兩個命令來完成這三個步驟。

第一步,客戶端要使用一個命令顯式地表示一個事務的開啟。在 Redis 中,這個命令就是 MULTI。

第二步,客戶端把事務中本身要執行的具體操作(例如增刪改資料)傳送給伺服器端。這些操作就是 Redis 本身提供的資料讀寫命令,例如 GET、SET 等。不過,這些命令雖然被客戶端傳送到了伺服器端,但 Redis 例項只是把這些命令暫存到一個命令佇列中,並不會立即執行。

第三步,客戶端向伺服器端傳送提交事務的命令,讓資料庫實際執行第二步中傳送的具體操作。Redis 提供的 EXEC 命令就是執行事務提交的。

#開啟事務
127.0.0.1:6379> MULTI
OK
#將a:stock減1,
127.0.0.1:6379> DECR a:stock
QUEUED
#將b:stock減1
127.0.0.1:6379> DECR b:stock
QUEUED
#實際執行事務
127.0.0.1:6379> EXEC
1) (integer) 4
2) (integer) 9

35 Codis 對比 Redis Cluster

Codis 和 Redis Cluster 的選型考慮:

  1. 從穩定性和成熟度來看,Codis 應用得比較早,在業界已經有了成熟的生產部署。雖然 Codis 引入了 proxy 和 Zookeeper,增加了叢集複雜度,但是,proxy 的無狀態設計和 Zookeeper 自身的穩定性,也給 Codis 的穩定使用提供了保證。而 Redis Cluster 的推出時間晚於 Codis,相對來說,成熟度要弱於 Codis,如果你想選擇一個成熟穩定的方案,Codis 更加合適些。
  2. 從業務應用客戶端相容性來看,連線單例項的客戶端可以直接連線 codis proxy,而原本連線單例項的客戶端要想連線 Redis Cluster 的話,就需要開發新功能。所以,如果你的業務應用中大量使用了單例項的客戶端,而現在想應用切片叢集的話,建議你選擇 Codis,這樣可以避免修改業務應用中的客戶端。
  3. 從使用 Redis 新命令和新特性來看,Codis server 是基於開源的 Redis 3.2.8 開發的,所以,Codis 並不支援 Redis 後續的開源版本中的新增命令和資料型別。另外,Codis 並沒有實現開源 Redis 版本的所有命令,比如 BITOP、BLPOP、BRPOP,以及和與事務相關的 MUTLI、EXEC 等命令。Codis 官網上列出了不被支援的命令列表,你在使用時記得去核查一下。所以,如果你想使用開源 Redis 版本的新特性,Redis Cluster 是一個合適的選擇。
  4. 從資料遷移效能維度來看,Codis 能支援非同步遷移,非同步遷移對叢集處理正常請求的效能影響要比使用同步遷移的小。所以,如果你在應用叢集時,資料遷移比較頻繁的話,Codis 是個更合適的選擇。

Codis 叢集包含 codis server、codis proxy、Zookeeper、codis dashboard 和 codis fe 這四大類元件

  • codis proxy 和 codis server 負責處理資料讀寫請求,其中,codis proxy 和客戶端連線,接收請求,並轉發請求給 codis server,而 codis server 負責具體處理請求。
  • codis dashboard 和 codis fe 負責叢集管理,其中,codis dashboard 執行管理操作,而 codis fe 提供 Web 管理介面。
  • Zookeeper 叢集負責儲存叢集的所有元資料資訊,包括路由表、proxy 例項資訊等。這裡,有個地方需要你注意,除了使用 Zookeeper,Codis 還可以使用 etcd 或本地檔案系統儲存元資料資訊。

37 資料傾斜

資料傾斜有兩類:

資料量傾斜:在某些情況下,例項上的資料分佈不均衡,某個例項上的資料特別多。

資料訪問傾斜:雖然每個叢集例項上的資料量相差不大,但是某個例項上的資料是熱點資料,被訪問得非常頻繁。

實用專案

28 Pika: 基於SSD實現大容量Redis

Redis 使用記憶體儲存資料,記憶體容量增加後,就會帶來兩方面的潛在問題,分別是,記憶體快照 RDB 生成和恢復效率低,以及主從節點全量同步時長增加、緩衝區易溢位。

跟 Redis 相比,Pika 的好處非常明顯:既支援 Redis 操作介面,又能支援儲存大容量的資料。如果你原來就在應用 Redis,現在想進行擴容,那麼,Pika 無疑是一個很好的選擇,無論是程式碼遷移還是運維管理,Pika 基本不需要額外的工作量。

不過,Pika 畢竟是把資料儲存到了 SSD 上,資料訪問要讀寫 SSD,所以,讀寫效能要弱於 Redis。針對這一點,我給你提供兩個降低讀寫 SSD 對 Pika 的效能影響的小建議:

利用 Pika 的多執行緒模型,增加執行緒數量,提升 Pika 的併發請求處理能力;

為 Pika 配置高配的 SSD,提升 SSD 自身的訪問效能。

36 Redis支撐秒殺場景的關鍵技術和實踐

秒殺場景的負載特徵對支撐系統的要求

第一個特徵是瞬時併發訪問量非常高。

第二個特徵是讀多寫少,而且讀操作是簡單的查詢操作。

Redis 可以在秒殺場景的哪些環節發揮作用

  • 秒殺活動前:

使用者會不斷重新整理商品詳情頁,應對方案,一般是儘量把商品詳情頁的頁面元素靜態化,然後使用 CDN 或是瀏覽器把這些靜態化的元素快取起來,不需要使用 Redis

  • 秒殺活動開始:

這個階段的操作就是三個:庫存查驗、庫存扣減和訂單處理。

查驗和扣減商品庫存,庫存查驗面臨大量的高併發請求,而庫存扣減又需要和庫存查驗一起執行,以保證原子性。這就是秒殺對 Redis 的需求。

訂單處理會涉及支付、商品出庫、物流等多個關聯操作,這些操作本身涉及資料庫中的多張資料表,要保證處理的事務性,需要在資料庫中完成。

  • 秒殺活動結束後

這個階段中的使用者請求量已經下降很多了,伺服器端一般都能支撐,我們就不重點討論了

基於原子操作支撐秒殺場景

在秒殺場景中,一個商品的庫存對應了兩個資訊,分別是總庫存量和已秒殺量

我們可以使用一個 Hash 型別的鍵值對來儲存庫存的這兩個資訊

key: itemID
value: {total: N, ordered: M}

其中,itemID 是商品的編號,total 是總庫存量,ordered 是已秒殺量。

因為庫存查驗和庫存扣減是兩個操作,無法用一條命令來完成,所以,我們就需要使用 Lua 指令碼原子性地執行這兩個操作。

Lua 指令碼寫的虛擬碼:

#獲取商品庫存資訊            
local counts = redis.call("HMGET", KEYS[1], "total", "ordered");
#將總庫存轉換為數值
local total = tonumber(counts[1])
#將已被秒殺的庫存轉換為數值
local ordered = tonumber(counts[2])  
#如果當前請求的庫存量加上已被秒殺的庫存量仍然小於總庫存量,就可以更新庫存         
if ordered + k <= total then
    #更新已秒殺的庫存量
    redis.call("HINCRBY",KEYS[1],"ordered",k)                              return k;  
end               
return 0

有了 Lua 指令碼後,我們就可以在 Redis 客戶端,使用 EVAL 命令來執行這個指令碼了。最後,客戶端會根據指令碼的返回值,來確定秒殺是成功還是失敗了。如果返回值是 k,就是成功了;如果是 0,就是失敗。

基於分散式鎖來支撐秒殺場景

具體做法是,先讓客戶端向 Redis 申請分散式鎖,只有拿到鎖的客戶端才能執行庫存查驗和庫存扣減。

虛擬碼:

//使用商品ID作為key
key = itemID
//使用客戶端唯一標識作為value
val = clientUniqueID
//申請分散式鎖,Timeout是超時時間
lock =acquireLock(key, val, Timeout)
//當拿到鎖後,才能進行庫存查驗和扣減
if(lock == True) {
   //庫存查驗和扣減
   availStock = DECR(key, k)
   //庫存已經扣減完了,釋放鎖,返回秒殺失敗
   if (availStock < 0) {
      releaseLock(key, val)
      return error
   }
   //庫存扣減成功,釋放鎖
   else{
     releaseLock(key, val)
     //訂單處理
   }
}
//沒有拿到鎖,直接返回
else
   return

在使用分散式鎖時,客戶端需要先向 Redis 請求鎖,只有請求到了鎖,才能進行庫存查驗等操作,這樣一來,客戶端在爭搶分散式鎖時,大部分秒殺請求本身就會因為搶不到鎖而被攔截。

41 Redis和Memcached對比

Memcached 有一個明顯的優勢,就是它的叢集規模可以很大.

使用一致性雜湊演算法把資料分散儲存到多個例項上,而一致性雜湊的優勢就是可以支援大規模的叢集。

Redis運維工具

最基本的監控命令:INFO 命令

INFO 命令在使用時,可以帶一個引數 section,這個引數的取值有好幾種

面向 Prometheus 的 Redis-exporter 監控

Prometheus是一套開源的系統監控報警框架。它的核心功能是從被監控系統中拉取監控資料,結合Grafana工具,進行視覺化展示。而且,監控資料可以儲存到時序資料庫中,以便運維人員進行歷史查詢。同時,Prometheus 會檢測系統的監控指標是否超過了預設的閾值,一旦超過閾值,Prometheus 就會觸發報警。

Prometheus 正好提供了外掛功能來實現對一個系統的監控,我們把外掛稱為 exporter,每一個 exporter 實際是一個採集監控資料的元件。exporter 採集的資料格式符合 Prometheus 的要求,Prometheus 獲取這些資料後,就可以進行展示和儲存了。

Redis-exporter就是用來監控 Redis 的,它將 INFO 命令監控到的執行狀態和各種統計資訊提供給 Prometheus,從而進行視覺化展示和報警設定。目前,Redis-exporter 可以支援 Redis 2.0 至 6.0 版本,適用範圍比較廣。