選擇合適Redis資料結構,減少80%的記憶體佔用
前言
redis作為目前最流行的nosql快取資料庫,憑藉其優異的效能、豐富的資料結構已成為大部分場景下首選的快取工具。
由於redis是一個純記憶體的資料庫,在存放大量資料時,記憶體的佔用將會非常可觀。那麼在一些場景下,透過選用合適資料結
構來儲存,可以大幅減少記憶體的佔用,甚至於可以減少80%-99%的記憶體佔用。
利用zipList來替代大量的Key-Value
先來看一下場景,在Dsp廣告系統、海量使用者系統經常會碰到這樣的需求,要求根據使用者的某個唯一標識迅速查到該使用者id。
譬如根據mac地址或uuid或手機號的md5,去查詢到該使用者的id。
特點是資料量很大、千萬或億級別,key是比較長的字串,如32位的md5或者uuid這種。
如果不加以處理,直接以key-value形式進行儲存,我們可以簡單測試一下,往redis裡插入1千萬條資料,1550000000 - 155
9999999,形式就是key(md5(1550000000))→ value(1550000000)這種。
然後在Redis內用命令info memory看一下記憶體佔用。
可以看到,這1千萬條資料,佔用了redis共計1.17G的記憶體。當資料量變成1個億時,實測大約佔用8個G。
同樣的一批資料,我們換一種儲存方式,先來看結果:
在我們利用zipList後,記憶體佔用為123M,大約減少了85%的空間佔用,這是怎麼做到的呢?
redis的底層儲存來剖析。
redis資料結構和編碼方式
redis如何儲存字串
string是redis裡最常用的資料結構,redis的預設字串和C語言的字串不同,它是自己構建了一種名為“簡單動態字串
SDS”的抽象型別。
具體到string的底層儲存,redis共用了三種方式,分別是int、embstr和raw。
譬如set k1 abc和set k2 123就會分別用embstr、int。當value的長度大於44(或39,不同版本不一樣)個位元組時,會採用
raw。
int是一種定長的結構,佔8個位元組(注意,相當於java裡的long),只能用來儲存長整形。
embstr是動態擴容的,每次擴容1倍,超過1M時,每次只擴容1M。
raw用來儲存大於44個位元組的字串。
具體到我們的案例中,key是32個位元組的字串(embstr),value是一個長整形(int),所以如果能將32位的md5變成int,
那麼在key的儲存上就可以直接減少3/4的記憶體佔用。
這是第一個最佳化點。
redis如何儲存Hash
從1.1的圖上我們可以看到Hash資料結構,在編碼方式上有兩種,1是hashTable,2是zipList。
hashTable大家很熟悉,和java裡的hashMap很像,都是陣列+連結串列的方式。java裡hashmap為了減少hash衝突,設定了負載
因子為0.75。同樣,redis的hash也有類似的擴容負載因子。細節不提,只需要留個印象,用hashTable編碼的話,則會花費至
少大於儲存的資料25%的空間才能存下這些資料。它大概長這樣:
zipList,壓縮連結串列,它大概長這樣:
可以看到,zipList最大的特點就是,它根本不是hash結構,而是一個比較長的字串,將key-value都按順序依次擺放到一個
長長的字串裡來儲存。如果要找某個key的話,就直接遍歷整個長字串就好了。
所以很明顯,zipList要比hashTable佔用少的多的空間。但是會耗費更多的cpu來進行查詢。
那麼何時用hashTable、zipList呢?在redis.conf檔案中可以找到:
就是當這個hash結構的內層field-value數量不超過512,並且value的位元組數不超過64時,就使用zipList。
透過實測,value數量在512時,效能和單純的hashTable幾乎無差別,在value數量不超過1024時,效能僅有極小的降低,很
多時候可以忽略掉。
而記憶體佔用,zipList可比hashTable降低了極多。
這是第二個最佳化點。
用zipList來代替key-value
透過上面的知識,我們得出了兩個結論。用int作為key,會比string省很多空間。用hash中的zipList,會比key-value省巨大
的空間。
那麼我們就來改造一下當初的1千萬個key-value。
第一步:
我們要將1千萬個鍵值對,放到N個bucket中,每個bucket是一個redis的hash資料結構,並且要讓每個bucket內不超過預設
的512個元素(如果改了配置檔案,如1024,則不能超過修改後的值),以避免hash將編碼方式從zipList變成hashTable。
1千萬 / 512 = 19531。由於將來要將所有的key進行雜湊演算法,來儘量均攤到所有bucket裡,但由於雜湊函式的不確定性,
未必能完全平均分配。所以我們要預留一些空間,譬如我分配25000個bucket,或30000個bucket。
第二步:
選用雜湊演算法,決定將key放到哪個bucket。這裡我們採用高效而且均衡的知名演算法crc32,該雜湊演算法可以將一個字串變成
一個long型的數字,透過獲取這個md5型的key的crc32後,再對bucket的數量進行取餘,就可以確定該key要被放到哪個
bucket中。
第三步:
透過第二步,我們確定了key即將存放在的redis裡hash結構的外層key,對於內層field,我們就選用另一個hash演算法,以避免
兩個完全不同的值,透過crc32(key) % COUNT後,發生field再次相同,產生hash衝突導致值被覆蓋的情況。內層field我
們選用bkdr雜湊演算法(或直接選用Java的hashCode),該演算法也會得到一個long整形的數字。value的儲存保持不變。
第四步:
裝入資料。原來的資料結構是key-value,0eac261f1c2d21e0bfdbd567bb270a68 → 1550000000。
現在的資料結構是hash,key為14523,field是1927144074,value是1550000000。
透過實測,將1千萬資料存入25000個bucket後,整體hash比較均衡,每個bucket下大概有300多個field-value鍵值對。理論
上只要不發生兩次hash演算法後,均產生相同的值,那麼就可以完全依靠key-field來找到原始的value。這一點可以透過計算總
量進行確認。實際上,在bucket數量較多時,且每個bucket下,value數量不是很多,發生連續碰撞機率極低,實測在儲存
50億個手機號情況下,未發生明顯碰撞。
測試查詢速度:
在儲存完這1千萬個資料後,我們進行了查詢測試,採用key-value型和hash型,分別查詢100萬條資料,看一下對查詢速度
的影響。
key-value耗時:10653、10790、11318、9900、11270、11029毫秒
hash-field耗時:12042、11349、11126、11355、11168毫秒。
可以看到,整體上採用hash儲存後,查詢100萬條耗時,也僅僅增加了500毫秒不到。對效能的影響極其微小。但記憶體佔用從
1.1G變成了120M,帶來了接近90%的記憶體節省。
總結
大量的key-value,佔用過多的key,redis裡為了處理hash碰撞,需要佔用更多的空間來儲存這些key-value資料。
如果key的長短不一,譬如有些40位,有些10位,因為對齊問題,那麼將產生巨大的記憶體碎片,佔用空間情況更為嚴重。所以
,保持key的長度統一(譬如統一採用int型,定長8個位元組),也會對記憶體佔用有幫助。
string型的md5,佔用了32個位元組。而透過hash演算法後,將32降到了8個位元組的長整形,這顯著降低了key的空間佔用。
zipList比hashTable明顯減少了記憶體佔用,它的儲存非常緊湊,對查詢效率影響也很小。所以應善於利用zipList,避免在
hash結構裡,存放超過512個field-value元素。
如果value是字串、物件等,應儘量採用byte[]來儲存,同樣可以大幅降低記憶體佔用。譬如可以選用google的Snappy壓縮算
法,將字串轉為byte[],非常高效,壓縮率也很高。
為減少redis對字串的預分配和擴容(每次翻倍),造成記憶體碎片,不應該使用append,setrange等。而是直接用set,替
換原來的。
方案缺點:
hash結構不支援對單個field的超時設定。但可以透過程式碼來控制刪除,對於那些不需要超時的長期存放的資料,則沒有這種
顧慮。
存在較小的hash衝突機率,對於對資料要求極其精確的場合,不適合用這種壓縮方式。
基於上述方案,我改寫了springboot原始碼的redisTemplate,提供了一個CompressRedisTemplate類,可以直接當成
redisTemplate使用,它會自動將key-value轉為hash進行儲存,以達到上述目的。
後續,我們會基於更極端一些的場景,如統計獨立訪客等,來看一下redis的不常見的資料結構,是如何將記憶體佔用由20G降低
到5M。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946034/viewspace-2671727/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 字串池化,減少1/3記憶體佔用字串記憶體
- 減少.NET應用程式記憶體佔用的一則實踐記憶體
- 谷歌Chrome瀏覽器引入省記憶體/省電模式:減少記憶體佔用谷歌Chrome瀏覽器記憶體模式
- 減少Spring Boot的JVM記憶體佔用的Docker三種配置Spring BootJVM記憶體Docker
- 揭秘!Vue3.5響應式重構如何讓記憶體佔用減少56%Vue記憶體
- python使用迭代生成器yield減少記憶體佔用的方法Python記憶體
- 選擇合適的資料型別資料型別
- 如何選擇合適的NoSQL資料庫SQL資料庫
- curl 中減少記憶體分配操作記憶體
- 資料結構的選擇資料結構
- Redis的資料被刪除,佔用記憶體咋還那麼大?Redis記憶體
- 如何選擇合適的雲資料庫架構與規格資料庫架構
- 資源記憶體佔用記憶體
- 記憶體資料庫適合多大規模的資料集?UY記憶體資料庫
- Oracle - 資料庫的記憶體結構Oracle資料庫記憶體
- 如何讓NoSQL記憶體資料庫適合企業級應用SQL記憶體資料庫
- 電腦記憶體選購知識,什麼樣的記憶體適合自己?記憶體
- filebeat實踐-記憶體佔用-最大記憶體佔用記憶體
- Redis資料已經過期了,為什麼還佔用記憶體?Redis記憶體
- Redis 實戰 —— 12. 降低記憶體佔用Redis記憶體
- 【資料結構】選擇排序!!!資料結構排序
- 【資料結構】選擇排序資料結構排序
- 如何檢視MySQL資料庫佔多大記憶體,佔用太多記憶體怎麼辦?MySql資料庫記憶體
- 通過減少記憶體使用改善.NET效能記憶體
- 使用String.intern減少記憶體使用記憶體
- Effective C#:儘量減少記憶體垃圾C#記憶體
- Win10開機後記憶體佔用高80%以上怎麼回事 Win10開機後記憶體佔用高80%以上的處理方法Win10記憶體
- python定時爬蟲啟用時如何減少記憶體?Python爬蟲記憶體
- Postgresql資料庫體系結構-程式和記憶體結構SQL資料庫記憶體
- 瀚高資料庫記憶體結構資料庫記憶體
- Redis 雜湊結構記憶體模型剖析Redis記憶體模型
- 根據開源資料庫選擇合適的工具資料庫
- 如何選擇一款合適的圖資料庫?資料庫
- Redis Quicklist 竟讓記憶體佔用狂降50%?RedisUI記憶體
- 三步選擇適合的CRM軟體
- 資料結構32:選擇排序資料結構排序
- JVM結構-記憶體結構(執行時資料區)JVM記憶體
- NIO的JVM記憶體和機器記憶體的選擇JVM記憶體