Redis大Key多Key拆分方案,你學(xué)會(huì)了嗎?
業(yè)務(wù)場景中經(jīng)常會(huì)有各種大key多key的情況, 比如:
1:單個(gè)簡單的key存儲(chǔ)的value很大。
2:hash, set,zset,list 中存儲(chǔ)過多的元素(以萬為單位)。
3:一個(gè)集群存儲(chǔ)了上億的key,Key 本身過多也帶來了更多的空間占用。
(如無意外,文章中所提及的hash,set等數(shù)據(jù)結(jié)構(gòu)均指redis中的數(shù)據(jù)結(jié)構(gòu) )。
由于redis是單線程運(yùn)行的,如果一次操作的value很大會(huì)對整個(gè)redis的響應(yīng)時(shí)間造成負(fù)面影響,所以,業(yè)務(wù)上能拆則拆,下面舉幾個(gè)典型的分拆方案。
1:單個(gè)簡單的key存儲(chǔ)的value很大
i:該對象需要每次都整存整取
可以嘗試將對象分拆成幾個(gè)key-value, 使用multiGet獲取值,這樣分拆的意義在于分拆單次操作的壓力,將操作壓力平攤到多個(gè)redis實(shí)例中,降低對單個(gè)redis的IO影響;
ii:該對象每次只需要存取部分?jǐn)?shù)據(jù)
可以像第一種做法一樣,分拆成幾個(gè)key-value, 也可以將這個(gè)存儲(chǔ)在一個(gè)hash中,每個(gè)field代表一個(gè)具體的屬性。
使用hget,hmget來獲取部分的value,使用hset,hmset來更新部分屬性。
2:value中存儲(chǔ)過多的元素
類似于場景一種的第一個(gè)做法,可以將這些元素分拆。
以hash為例,原先的正常存取流程是 hget(hashKey, field) ; hset(hashKey, field, value)。
現(xiàn)在,固定一個(gè)桶的數(shù)量,比如 10000, 每次存取的時(shí)候,先在本地計(jì)算field的hash值,模除 10000, 確定了該field落在哪個(gè)key上。
newHashKey = hashKey + ( set, zset, list 也可以類似上述做法。
但有些不適合的場景,比如,要保證 lpop 的數(shù)據(jù)的確是最早push到list中去的,這個(gè)就需要一些附加的屬性,或者是在 key的拼接上做一些工作(比如list按照時(shí)間來分拆)。
3:一個(gè)集群存儲(chǔ)了上億的key
如果key的個(gè)數(shù)過多會(huì)帶來更多的內(nèi)存空間占用。
i:key本身的占用(每個(gè)key 都會(huì)有一個(gè)Category前綴)。
ii:集群模式中,服務(wù)端需要建立一些slot2key的映射關(guān)系,這其中的指針占用在key多的情況下也是浪費(fèi)巨大空間。
這兩個(gè)方面在key個(gè)數(shù)上億的時(shí)候消耗內(nèi)存十分明顯(Redis 3.2及以下版本均存在這個(gè)問題,4.0有優(yōu)化);
所以減少key的個(gè)數(shù)可以減少內(nèi)存消耗,可以參考的方案是轉(zhuǎn)Hash結(jié)構(gòu)存儲(chǔ),即原先是直接使用Redis String 的結(jié)構(gòu)存儲(chǔ),現(xiàn)在將多個(gè)key存儲(chǔ)在一個(gè)Hash結(jié)構(gòu)中,具體場景參考如下:
一:key 本身就有很強(qiáng)的相關(guān)性,比如多個(gè)key 代表一個(gè)對象,每個(gè)key是對象的一個(gè)屬性,這種可直接按照特定對象的特征來設(shè)置一個(gè)新Key——Hash結(jié)構(gòu), 原先的key則作為這個(gè)新Hash 的field。
舉例說明:
原先存儲(chǔ)的三個(gè)key 。
這三個(gè)key本身就具有很強(qiáng)的相關(guān)特性,轉(zhuǎn)成Hash存儲(chǔ)就像這樣 key = user.zhangsan。
即redis中存儲(chǔ)的是一個(gè)key :user.zhangsan, 他有三個(gè) field, 每個(gè)field + key 就對應(yīng)原先的一個(gè)key。
二:key 本身沒有相關(guān)性,預(yù)估一下總量,采取和上述第二種場景類似的方案,預(yù)分一個(gè)固定的桶數(shù)量。
比如現(xiàn)在預(yù)估key 的總數(shù)為 2億,按照一個(gè)hash存儲(chǔ) 100個(gè)field來算,需要 2億 / 100 = 200W 個(gè)桶 (200W 個(gè)key占用的空間很少,2億可能有將近 20G )。
原先比如有三個(gè)key :
現(xiàn)在按照200W 固定桶分就是先計(jì)算出桶的序號(hào) hash(123456789) % 200W , 這里最好保證這個(gè) hash算法的值是個(gè)正數(shù),否則需要調(diào)整下模除的規(guī)則;
這樣算出三個(gè)key 的桶分別是 1 , 2, 2。 所以存儲(chǔ)的時(shí)候調(diào)用API hset(key, field, value),讀取的時(shí)候使用 hget (key, field)。
注意兩個(gè)地方:1,hash 取模對負(fù)數(shù)的處理; 2,預(yù)分桶的時(shí)候, 一個(gè)hash 中存儲(chǔ)的值最好不要超過 512 ,100 左右較為合適
4:大Bitmap或布隆過濾器(Bloom )拆分
使用bitmap或布隆過濾器的場景,往往是數(shù)據(jù)量極大的情況,在這種情況下,Bitmap和布隆過濾器使用空間也比較大,比如用于公司userid匹配的布隆過濾器,就需要512MB的大小,這對redis來說是絕對的大value了。
這種場景下,我們就需要對其進(jìn)行拆分,拆分為足夠小的Bitmap,比如將512MB的大Bitmap拆分為1024個(gè)512KB的Bitmap。不過拆分的時(shí)候需要注意,要將每個(gè)key落在一個(gè)Bitmap上。有些業(yè)務(wù)只是把Bitmap 拆開, 但還是當(dāng)做一個(gè)整體的bitmap看, 所以一個(gè) key 還是落在多個(gè) Bitmap 上,這樣就有可能導(dǎo)致一個(gè)key請求需要查詢多個(gè)節(jié)點(diǎn)、多個(gè)Bitmap。如下圖,被請求的值被hash到多個(gè)Bitmap上,也就是redis的多個(gè)key上,這些key還有可能在不同節(jié)點(diǎn)上,這樣拆分顯然大大降低了查詢的效率。
因此我們所要做的是把所有拆分后的Bitmap當(dāng)作獨(dú)立的bitmap,然后通過hash將不同的key分配給不同的bitmap上,而不是把所有的小Bitmap當(dāng)作一個(gè)整體。這樣做后每次請求都只要取redis中一個(gè)key即可。
有同學(xué)可能會(huì)問,通過這樣拆分后,相當(dāng)于Bitmap變小了,會(huì)不會(huì)增加布隆過濾器的誤判率?實(shí)際上是不會(huì)的,布隆過濾器的誤判率是哈希函數(shù)個(gè)數(shù)k,集合元素個(gè)數(shù)n,以及Bitmap大小m所決定的,其約等于。因此如果我們在第一步,也就是在分配key給不同Bitmap時(shí),能夠盡可能均勻的拆分,那么n/m的值幾乎是一樣的,誤判率也就不會(huì)改變。具體的誤判率推導(dǎo)可以參考wiki:Bloom_filter。
同時(shí),客戶端也提供便利的api (>=2.3.4版本), setBits/ getBits 用于一次操作同一個(gè)key的多個(gè)bit值 。
建議 :k 取 13 個(gè), 單個(gè)bloomfilter控制在 512KB 以下。