Redis 大 key 多 key 拆分方案

業務場景中經常會有各種大 key 多 key 的情況, 比如:

1:單個簡單的 key 存儲的 value 很大

2:hash, set,zset,list 中存儲過多的元素(以萬爲單位)

3:一個集羣存儲了上億的 key,Key 本身過多也帶來了更多的空間佔用

(如無意外,文章中所提及的 hash,set 等數據結構均指 redis 中的數據結構   )

由於 redis 是單線程運行的,如果一次操作的 value 很大會對整個 redis 的響應時間造成負面影響,所以,業務上能拆則拆,下面舉幾個典型的分拆方案。

1:單個簡單的 key 存儲的 value 很大

 i:該對象需要每次都整存整取

可以嘗試將對象分拆成幾個 key-value, 使用 multiGet 獲取值,這樣分拆的意義在於分拆單次操作的壓力,將操作壓力平攤到多個 redis 實例中,降低對單個 redis 的 IO 影響;    

ii:該對象每次只需要存取部分數據

可以像第一種做法一樣,分拆成幾個 key-value,  也可以將這個存儲在一個 hash 中,每個 field 代表一個具體的屬性,

使用 hget,hmget 來獲取部分的 value,使用 hset,hmset 來更新部分屬性    

2:value 中存儲過多的元素

類似於場景一種的第一個做法,可以將這些元素分拆。

以 hash 爲例,原先的正常存取流程是  hget(hashKey, field) ; hset(hashKey, field, value)

現在,固定一個桶的數量,比如 10000, 每次存取的時候,先在本地計算 field 的 hash 值,模除 10000, 確定了該 field 落在哪個 key 上。

newHashKey  =  hashKey + ( set, zset, list 也可以類似上述做法

但有些不適合的場景,比如,要保證 lpop 的數據的確是最早 push 到 list 中去的,這個就需要一些附加的屬性,或者是在 key 的拼接上做一些工作(比如 list 按照時間來分拆)。

3:一個集羣存儲了上億的 key

如果 key 的個數過多會帶來更多的內存空間佔用,

      i:key 本身的佔用(每個 key 都會有一個 Category 前綴)

      ii:集羣模式中,服務端需要建立一些 slot2key 的映射關係,這其中的指針佔用在 key 多的情況下也是浪費巨大空間

      這兩個方面在 key 個數上億的時候消耗內存十分明顯(Redis 3.2 及以下版本均存在這個問題,4.0 有優化);

所以減少 key 的個數可以減少內存消耗,可以參考的方案是轉 Hash 結構存儲,即原先是直接使用 Redis String 的結構存儲,現在將多個 key 存儲在一個 Hash 結構中,具體場景參考如下:

       一:key 本身就有很強的相關性,比如多個 key 代表一個對象,每個 key 是對象的一個屬性,這種可直接按照特定對象的特徵來設置一個新 Key——Hash 結構, 原先的 key 則作爲這個新 Hash 的 field。

舉例說明:

原先存儲的三個 key 

user.zhangsan-id = 123;  

user.zhangsan-age = 18; 

user.zhangsan-country = china;     

這三個 key 本身就具有很強的相關特性,轉成 Hash 存儲就像這樣 key =  user.zhangsan

field:id = 123; 

field:age = 18; 

field:country = china;

即 redis 中存儲的是一個 key :user.zhangsan, 他有三個 field, 每個 field + key 就對應原先的一個 key。

      二:key 本身沒有相關性,預估一下總量,採取和上述第二種場景類似的方案,預分一個固定的桶數量

      比如現在預估 key 的總數爲 2 億,按照一個 hash 存儲 100 個 field 來算,需要 2 億 /  100  = 200W 個桶 (200W 個 key 佔用的空間很少,2 億可能有將近 20G)

原先比如有三個 key   :

user.123456789  

user.987654321

user.678912345

現在按照 200W 固定桶分就是先計算出桶的序號 hash(123456789)   % 200W , 這裏最好保證這個 hash 算法的值是個正數,否則需要調整下模除的規則;

這樣算出三個 key 的桶分別是     1 , 2, 2。   所以存儲的時候調用 API    hset(key,  field, value),讀取的時候使用  hget (key, field)   

圖片

注意兩個地方:1,hash 取模對負數的處理;  2,預分桶的時候, 一個 hash 中存儲的值最好不要超過 512 ,100 左右較爲合適

4:大 Bitmap 或布隆過濾器(Bloom )拆分

使用 bitmap 或布隆過濾器的場景,往往是數據量極大的情況,在這種情況下,Bitmap 和布隆過濾器使用空間也比較大,比如用於公司 userid 匹配的布隆過濾器,就需要 512MB 的大小,這對 redis 來說是絕對的大 value 了。

這種場景下,我們就需要對其進行拆分,拆分爲足夠小的 Bitmap,比如將 512MB 的大 Bitmap 拆分爲 1024 個 512KB 的 Bitmap。不過拆分的時候需要注意,要將每個 key 落在一個 Bitmap 上。有些業務只是把 Bitmap 拆開, 但還是當做一個整體的 bitmap 看, 所以一個 key 還是落在多個 Bitmap 上,這樣就有可能導致一個 key 請求需要查詢多個節點、多個 Bitmap。如下圖,被請求的值被 hash 到多個 Bitmap 上,也就是 redis 的多個 key 上,這些 key 還有可能在不同節點上,這樣拆分顯然大大降低了查詢的效率。

圖片

因此我們所要做的是把所有拆分後的 Bitmap 當作獨立的 bitmap,然後通過 hash 將不同的 key 分配給不同的 bitmap 上,而不是把所有的小 Bitmap 當作一個整體。這樣做後每次請求都只要取 redis 中一個 key 即可。

圖片

有同學可能會問,通過這樣拆分後,相當於 Bitmap 變小了,會不會增加布隆過濾器的誤判率?實際上是不會的,布隆過濾器的誤判率是哈希函數個數 k,集合元素個數 n,以及 Bitmap 大小 m 所決定的,其約等於圖片。因此如果我們在第一步,也就是在分配 key 給不同 Bitmap 時,能夠儘可能均勻的拆分,那麼 n/m 的值幾乎是一樣的,誤判率也就不會改變。具體的誤判率推導可以參考 wiki:Bloom_filter

同時,客戶端也提供便利的 api (>=2.3.4 版本), setBits/ getBits 用於一次操作同一個 key 的多個 bit 值 。

建議 :k 取 13 個, 單個 bloomfilter 控制在 512KB 以下

以上方案僅供參考,歡迎大家提供其他的優秀方案。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/UeJ4xo2cRQmXRopgGUbe7Q