redis 系列:總結篇

hjavn發表於2021-09-08

工具與資源中心

幫助開發者更加高效的工作,提供圍繞開發者全生命週期的工具與資源
developer.aliyun.com/tool?spm=a1z3...

Redis 是 key-value 型的 memory 快取中介軟體,相信大部分程式設計師都在專案中使用過它。我們也可以利用 memory 來實現快取,只是使用 redis 的話,可以將快取功能統一到一個元件裡,方便後續重用擴充。

在底層上, redis 使用了 IO 多路複用技術,像 select、epoll 等。能較好的保障吞吐量。而且 redis 採用了單執行緒處理請求,避免了執行緒切換和鎖競爭鎖帶來的額外消耗。

加上 redis 本身也對一些資料結構進行了優化設計,所以 redis 的效能非常好,官方給出的測試報告是單機可以支援約 10w/s 的 QPS。

redis 是基於 tcp 長連線的 C/S 架構,採用的是文字序列化協議,並且和 http 一樣,也是一個請求一個響應,客戶端接到響應後再繼續請求。

當然,也可以將多次請求傳送過去,然後一次響應回所有執行結果,這就是所謂的管道 pipeline 技術。

redis 的文字序列化協議比較簡單,通過一些規範格式去解析文字,大概如下:

  • \r\n 表示解析結束
  • 簡單字串,以“+”開頭
  • 錯誤 Errors,以“-”開頭
  • 整數型別,以“:”開頭
  • 大字串型別,以“$”開頭
  • 陣列型別,以“*”開頭

例如,客戶端向伺服器傳送命令:

SET key value

將被解析為:

*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

上面的命令可以看成:

*<引數數量> CR LF

$<引數 1 的位元組數量> CR LF
<引數 1 的資料> CR LF
...
$<引數 N 的位元組數量> CR LF
<引數 N 的資料> CR LF

而伺服器的回覆則有很多型別,一般由響應資料的第一個位元組決定:

狀態回覆(status reply)的第一個位元組是 "+"

錯誤回覆(error reply)的第一個位元組是 "-"

整數回覆(integer reply)的第一個位元組是 ":"

批量回復(bulk reply)的第一個位元組是 "$"

多條批量回復(multi bulk reply)的第一個位元組是 "*"

例如,響應回來的狀態回覆如下:

+OK

為了讓開發者能更好的使用快取,redis 支援了 5 種資料型別。底層是由 6 種資料結構組成的。

5 種資料型別

字串:字串型別是 redis 裡最基礎的資料型別,像 set name "hello" 操作後,在 get name 時返回的就是字串,而且還支援了對位的操作。一般一個鍵能儲存 512MB 的值。

hash:雜湊型別主要是用來儲存物件的,一般我們如果有一整個物件要儲存,裡面包含了多個欄位,則可以使用 hash 來儲存,因為 redis 提供了對這些欄位的提取和設定,減少了開發者對它的二次處理,比如序列化反序列化操作。

list:一個簡單的字串列表,它允許我們從兩端進行 push,pop 操作,還支援一定範圍的列表元素。可以看成是雙向列表。

set:集合是一個不重複值的組合,為我們提供了交集、並集、差集等操作,像找出共同好友這種需求就可以使用集合操作了。

sorted set:有序集合,在上面集合的基礎上提供了排序功能,通過一個 score 屬性來進行排序。

6 種底層資料結構

上面的資料型別實際上在 redis 底層是有對應的資料結構來實現的,都是 redis 經過精心設計的,能很好的提高處理效率。

簡單動態字串:redis 是使用 C 語言寫的,而 C 語言裡的字串型別比較原始,比如使用 \0 作為字元結束符。所以 redis 實現了屬於自己的字串型別,比如字串長度,預先分配記憶體,動態擴充等特點,也保證了處理安全性。

連結串列:一個雙端連結串列,有 prev,next 指標去獲取前後節點,帶有 len 屬性,能儲存多種型別的值。

字典:通過雜湊演算法來實現 key-value 的對映操作,採用鏈地址法解決了 hash 衝突,一般時間複雜度能達到 O(1)。

跳躍表:一個多層有序連結串列,每一層都是對下面一層的有序提取,能降低搜尋次數,有點像有序二叉樹的搜尋一樣。

跳躍表

整數集合:一個有序的整數集合,不會有重複元素。

壓縮列表(ziplist):經過特殊編碼的一塊連續記憶體,能有效的節省記憶體。

快速列表:將 ziplist 組織為了一個雙向連結串列,由於 ziplist 的內部連續性,能降低連結串列的記憶體碎片問題,提高記憶體利用率。

redis 的淘汰策略主要是 LRU 淘汰、TTL 淘汰和隨機淘汰這三種機制。

  • LRU 淘汰:最近最少使用的淘汰掉
  • TTL 淘汰:越早過期的越先淘汰掉。
  • 隨機淘汰:採用隨機演算法淘汰掉。

由於 redis 可以對鍵設定過期時間,也可以不設定,所以淘汰策略還得再細分:

  • volatile-lru:針對設定了過期時間的 key 執行 LRU 淘汰策略,沒有設定過期時間的不會被淘汰。
  • volatile-ttl:只針對設定了過期時間的 key 執行 TTL 淘汰。
  • volatile-random:只針對設定了過期時間的 key 執行隨機淘汰。
  • allkeys-lru:針對所有鍵進行 LRU 淘汰策略
  • allkeys-random:針對所有鍵進行隨機淘汰策略
  • no-enviction:不執行淘汰策略,如果有寫入操作,則報錯;讀請求可以繼續進行。

在 Redis 的配置檔案 redis.conf 裡我們可以進行淘汰策略的設定:

# 資料達到多大後執行淘汰策略
maxmemory 300mb

# 淘汰策略的設定
maxmemory-policy volatile-lru

Redis 的使用場景有很多,最常用的莫過於資料快取了。但由於它提供了多種資料型別,因此我們還可以進行其他場景的開發,比如:

  • 排行榜:前面提到過有序集合(sorted set),由於每次寫入都會進行排序,而且不含重複值,所以我們可以將使用者的唯一標識,比如 userId 作為 key,分數作為 score,然後就可以進行 ZADD 操作,以得到排行榜。
  • 簽到:簽到往往只有 2 種狀態,已簽到和未簽到。這就跟 0 和 1 一樣,所以 redis 的 setbitgetbit 這種對位的操作就適合簽到場景。
  • 計數:redis 是單執行緒操作,這種計數功能,比如點贊數、粉絲數的操作可以交給 redis 以避免併發競爭問題。當然,也得考慮持久化問題。

有的時候我們可能會使用 redis 作為分散式鎖的輔助使用,通過對 redis 操作響應以判斷當前是否可以獲取到鎖。

不過這樣的解決方案會有單節點的瓶頸,如果 redis 當機了,就會導致鎖的不可用。

有的朋友可能會說 redis 也有它的高可用方案。但實際上 redis 的高可用方案還是不適合分散式鎖的應用,會有多節點同時獲取到鎖的風險。

如果真的需要比較嚴謹的分散式鎖,還是得使用 zookeeperetcd等分散式協調方案,能保證強一致性。

快取雪崩和穿透

Redis 通過快取冗餘的資料,為我們的程式提供了高效能的保障。但需要注意的是一旦快取失效,那麼就會有大量的請求過來,壓垮系統,這就是快取雪崩。

除了快取雪崩,還有快取穿透的可能。比如每次訪問不一樣的資料,則請求還是會落到後方。

為了防止快取雪崩,我們可以對請求做控制,比如加入到訊息佇列,慢慢消化它;又或者直接開啟限流功能,將流量控制在合理的範圍內。

而針對快取穿透,我們可以建立黑白名單,將一些惡意請求拎出來,然後直接拒絕掉。如果是正常的請求,那可以將篩選出來的結果也暫時快取起來,即使得到的值是 NULL 值。

資料併發問題

由於 Redis 是以元件形式存在,所以實際上我們的程式通訊可以認為是分散式的了,也就是會有快取和後端資料一致性的問題。

常見的做法是在有新資料到來時,將快取 key 刪除掉,等待下次的查詢重新填補上快取。

之所以在更新資料時不讓 Redis 也做更新動作,是為了防止多個更新動作一起發生,可能由於網路原因,導致後更新的比前面更新的先一步達到 Redis, 這樣就會跟原來的流程不一樣了。所以只採取了刪除動作,不做其他。

不過,就算是刪除 key 這種方案也有一定概率跟上面的情況一樣,真的要嚴謹的話,一般會設定定時過期時間,讓資料最多在這段時間不一致。

本文轉自:developer.aliyun.com/article/78993...

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章