redis的內存優化【轉】

Redis全部的數據都在內存中,而內存又是很是寶貴的資源。對於如何優化內存使用一直是Redis用戶很是關注的問題。本文讓咱們深刻到Redis細節中,學習內存優化的技巧。分爲以下幾個部分:java

一.redisObject對象git

二.縮減鍵值對象github

三.共享對象池redis

四.字符串優化算法

五.編碼優化json

六.控制key的數量數組

一. redisObject對象

Redis存儲的全部值對象在內部定義爲redisObject結構體,內部結構以下圖所示。安全

Redis存儲的數據都使用redisObject來封裝,包括string,hash,list,set,zset在內的全部數據類型。理解redisObject對內存優化很是有幫助,下面針對每一個字段作詳細說明:ruby

1.type字段:

表示當前對象使用的數據類型,Redis主要支持5種數據類型:string,hash,list,set,zset。可使用type {key}命令查看對象所屬類型,type命令返回的是值對象類型,鍵都是string類型。bash

2.encoding字段:

表示Redis內部編碼類型,encoding在Redis內部使用,表明當前對象內部採用哪一種數據結構實現。理解Redis內部編碼方式對於優化內存很是重要 ,同一個對象採用不一樣的編碼實現內存佔用存在明顯差別,具體細節見以後編碼優化部分。

3.lru字段:

記錄對象最後一次被訪問的時間,當配置了 maxmemory和maxmemory-policy=volatile-lru | allkeys-lru 時, 用於輔助LRU算法刪除鍵數據。可使用object idletime {key}命令在不更新lru字段狀況下查看當前鍵的空閒時間。

開發提示:可使用scan + object idletime 命令批量查詢哪些鍵長時間未被訪問,找出長時間不訪問的鍵進行清理下降內存佔用。 

4.refcount字段:

記錄當前對象被引用的次數,用於經過引用次數回收內存,當refcount=0時,能夠安全回收當前對象空間。使用object refcount {key}獲取當前對象引用。當對象爲整數且範圍在[0-9999]時,Redis可使用共享對象的方式來節省內存。具體細節見以後共享對象池部分。

5. *ptr字段:

與對象的數據內容相關,若是是整數直接存儲數據,不然表示指向數據的指針。Redis在3.0以後對值對象是字符串且長度<=39字節的數據,內部編碼爲embstr類型,字符串sds和redisObject一塊兒分配,從而只要一次內存操做。

開發提示:高併發寫入場景中,在條件容許的狀況下建議字符串長度控制在39字節之內,減小建立redisObject內存分配次數從而提升性能。

二. 縮減鍵值對象

下降Redis內存使用最直接的方式就是縮減鍵(key)和值(value)的長度。

  • key長度:如在設計鍵時,在完整描述業務狀況下,鍵值越短越好。

  • value長度:值對象縮減比較複雜,常見需求是把業務對象序列化成二進制數組放入Redis。首先應該在業務上精簡業務對象,去掉沒必要要的屬性避免存儲無效數據。其次在序列化工具選擇上,應該選擇更高效的序列化工具來下降字節數組大小。以JAVA爲例,內置的序列化方式不管從速度仍是壓縮比都不盡如人意,這時能夠選擇更高效的序列化工具,如: protostuff,kryo等,下圖是JAVA常見序列化工具空間壓縮對比。

其中java-built-in-serializer表示JAVA內置序列化方式,更多數據見jvm-serializers項目: https://github.com/eishay/jvm-serializers/wiki,其它語言也有各自對應的高效序列化工具。

值對象除了存儲二進制數據以外,一般還會使用通用格式存儲數據好比:json,xml等做爲字符串存儲在Redis中。這種方式優勢是方便調試和跨語言,可是一樣的數據相比字節數組所需的空間更大,在內存緊張的狀況下,可使用通用壓縮算法壓縮json,xml後再存入Redis,從而下降內存佔用,例如使用GZIP壓縮後的json可下降約60%的空間。

開發提示:當頻繁壓縮解壓json等文本數據時,開發人員須要考慮壓縮速度和計算開銷成本,這裏推薦使用google的Snappy壓縮工具,在特定的壓縮率狀況下效率遠遠高於GZIP等傳統壓縮工具,且支持全部主流語言環境。

三. 共享對象池

對象共享池指Redis內部維護[0-9999]的整數對象池。建立大量的整數類型redisObject存在內存開銷,每一個redisObject內部結構至少佔16字節,甚至超過了整數自身空間消耗。因此Redis內存維護一個[0-9999]的整數對象池,用於節約內存。 除了整數值對象,其餘類型如list,hash,set,zset內部元素也可使用整數對象池。所以開發中在知足需求的前提下,儘可能使用整數對象以節省內存。

整數對象池在Redis中經過變量REDIS_SHARED_INTEGERS定義,不能經過配置修改。能夠經過object refcount 命令查看對象引用數驗證是否啓用整數對象池技術,以下:

redis> set foo 100 OK redis> object refcount foo (integer) 2 redis> set bar 100 OK redis> object refcount bar (integer) 3 

設置鍵foo等於100時,直接使用共享池內整數對象,所以引用數是2,再設置鍵bar等於100時,引用數又變爲3,以下圖所示。

使用整數對象池究竟能下降多少內存?讓咱們經過測試來對比對象池的內存優化效果,以下表所示。

操做說明 是否對象共享 key大小 value大小 used_mem used_memory_rss
插入200萬 20字節 [0-9999]整數 199.91MB 205.28MB
插入200萬 20字節 [0-9999]整數 138.87MB 143.28MB
注意本文全部測試環境都保持一致,信息以下:
服務器信息: cpu=Intel-Xeon E5606@2.13GHz memory=32GB Redis版本:Redis server v=3.0.7 sha=00000000:0 malloc=jemalloc-3.6.0 bits=64 

使用共享對象池後,相同的數據內存使用下降30%以上。可見當數據大量使用[0-9999]的整數時,共享對象池能夠節約大量內存。須要注意的是對象池並非只要存儲[0-9999]的整數就能夠工做。當設置maxmemory並啓用LRU相關淘汰策略如:volatile-lru,allkeys-lru時,Redis禁止使用共享對象池,測試命令以下:

redis> set key:1 99 OK //設置key:1=99 redis> object refcount key:1 (integer) 2 //使用了對象共享,引用數爲2 redis> config set maxmemory-policy volatile-lru OK //開啓LRU淘汰策略 redis> set key:2 99 OK //設置key:2=99 redis> object refcount key:2 (integer) 3 //使用了對象共享,引用數變爲3 redis> config set maxmemory 1GB OK //設置最大可用內存 redis> set key:3 99 OK //設置key:3=99 redis> object refcount key:3 (integer) 1 //未使用對象共享,引用數爲1 redis> config set maxmemory-policy volatile-ttl OK //設置非LRU淘汰策略 redis> set key:4 99 OK //設置key:4=99 redis> object refcount key:4 (integer) 4 //又可使用對象共享,引用數變爲4 

爲何開啓maxmemory和LRU淘汰策略後對象池無效?

LRU算法須要獲取對象最後被訪問時間,以便淘汰最長未訪問數據,每一個對象最後訪問時間存儲在redisObject對象的lru字段。對象共享意味着多個引用共享同一個redisObject,這時lru字段也會被共享,致使沒法獲取每一個對象的最後訪問時間。若是沒有設置maxmemory,直到內存被用盡Redis也不會觸發內存回收,因此共享對象池能夠正常工做。

綜上所述,共享對象池與maxmemory+LRU策略衝突,使用時須要注意。 對於ziplist編碼的值對象,即便內部數據爲整數也沒法使用共享對象池,由於ziplist使用壓縮且內存連續的結構,對象共享判斷成本太高,ziplist編碼細節後面內容詳細說明。

爲何只有整數對象池?

首先整數對象池複用的概率最大,其次對象共享的一個關鍵操做就是判斷相等性,Redis之因此只有整數對象池,是由於整數比較算法時間複雜度爲O(1),只保留一萬個整數爲了防止對象池浪費。若是是字符串判斷相等性,時間複雜度變爲O(n),特別是長字符串更消耗性能(浮點數在Redis內部使用字符串存儲)。對於更復雜的數據結構如hash,list等,相等性判斷須要O(n 2 )。對於單線程的Redis來講,這樣的開銷顯然不合理,所以Redis只保留整數共享對象池。

四. 字符串優化

字符串對象是Redis內部最經常使用的數據類型。全部的鍵都是字符串類型, 值對象數據除了整數以外都使用字符串存儲。好比執行命令:lpush cache:type 「redis」 「memcache」 「tair」 「levelDB」 ,Redis首先建立」cache:type」鍵字符串,而後建立鏈表對象,鏈表對象內再包含四個字符串對象,排除Redis內部用到的字符串對象以外至少建立5個字符串對象。可見字符串對象在Redis內部使用很是普遍,所以深入理解Redis字符串對於內存優化很是有幫助:

1.字符串結構

Redis沒有采用原生C語言的字符串類型而是本身實現了字符串結構,內部簡單動態字符串(simple dynamic string),簡稱SDS。結構下圖所示。

Redis自身實現的字符串結構有以下特色:

  • O(1)時間複雜度獲取:字符串長度,已用長度,未用長度。
  • 可用於保存字節數組,支持安全的二進制數據存儲。
  • 內部實現空間預分配機制,下降內存再分配次數。
  • 惰性刪除機制,字符串縮減後的空間不釋放,做爲預分配空間保留。

2.預分配機制

由於字符串(SDS)存在預分配機制,平常開發中要當心預分配帶來的內存浪費,例以下表的測試用例。

表:字符串內存預分配測試

階段 數據量 操做說明 命令 key大小 value大小 used_mem used_memory_rss mem_fragmentation_ratio
階段1 200w 新插入200w數據 set 20字節 60字節 321.98MB 331.44MB 1.02
階段2 200w 在階段1上每一個對象追加60字節數據 append 20字節 60字節 657.67MB 752.80MB 1.14
階段3 200w 從新插入200w數據 set 20字節 120字節 474.56MB 482.45MB 1.02

從測試數據能夠看出,一樣的數據追加後內存消耗很是嚴重,下面咱們結合圖來分析這一現象。階段1每一個字符串對象空間佔用以下圖所示。

階段1插入新的字符串後,free字段保留空間爲0,總佔用空間=實際佔用空間+1字節,最後1字節保存‘\0’標示結尾,這裏忽略int類型len和free字段消耗的8字節。在階段1原有字符串上追加60字節數據空間佔用以下圖所示。

追加操做後字符串對象預分配了一倍容量做爲預留空間,並且大量追加操做須要內存從新分配,形成內存碎片率(mem_fragmentation_ratio)上升。直接插入與階段2相同數據的空間佔用,以下圖所示。

階段3直接插入同等數據後,相比階段2節省了每一個字符串對象預分配的空間,同時下降了碎片率。

字符串之因此採用預分配的方式是防止修改操做須要不斷重分配內存和字節數據拷貝。但一樣也會形成內存的浪費。字符串預分配每次並不都是翻倍擴容,空間預分配規則以下:

  • 1) 第一次建立len屬性等於數據實際大小,free等於0,不作預分配。
  • 2) 修改後若是已有free空間不夠且數據小於1M,每次預分配一倍容量。如原有len=60byte,free=0,再追加60byte,預分配120byte,總佔用空間:60byte+60byte+120byte+1byte。
  • 3) 修改後若是已有free空間不夠且數據大於1MB,每次預分配1MB數據。如原有len=30MB,free=0,當再追加100byte ,預分配1MB,總佔用空間:1MB+100byte+1MB+1byte。 
    開發提示:儘可能減小字符串頻繁修改操做如append,setrange, 改成直接使用set修改字符串,下降預分配帶來的內存浪費和內存碎片化。

3.字符串重構

字符串重構:指不必定把每份數據做爲字符串總體存儲,像json這樣的數據可使用hash結構,使用二級結構存儲也能幫咱們節省內存。同時可使用hmget,hmset命令支持字段的部分讀取修改,而不用每次總體存取。例以下面的json數據:

{
    "vid": "413368768", "title": "搜狐屌絲男士", "videoAlbumPic": "http://photocdn.sohu.com/60160518/vrsa_ver8400079_ae433_pic26.jpg", "pid": "6494271", "type": "1024", "playlist": "6494271", "playTime": "468" } 

分別使用字符串和hash結構測試內存表現,以下表所示。

表:測試內存表現

數據量 key 存儲類型 value 配置 used_mem
200W 20字節 string json字符串 默認 612.62M
200W 20字節 hash key-value對 默認 默認 1.88GB
200W 20字節 hash key-value對 hash-max-ziplist-value:66 535.60M

根據測試結構,第一次默認配置下使用hash類型,內存消耗不但沒有下降反而比字符串存儲多出2倍,而調整hash-max-ziplist-value=66以後內存下降爲535.60M。由於json的videoAlbumPic屬性長度是65,而hash-max-ziplist-value默認值是64,Redis採用hashtable編碼方式,反而消耗了大量內存。調整配置後hash類型內部編碼方式變爲ziplist,相比字符串更省內存且支持屬性的部分操做。下一節將具體介紹ziplist編碼優化細節。

五. 編碼優化

1.瞭解編碼

Redis對外提供了string,list,hash,set,zet等類型,可是Redis內部針對不一樣類型存在編碼的概念,所謂編碼就是具體使用哪一種底層數據結構來實現。編碼不一樣將直接影響數據的內存佔用和讀寫效率。使用object encoding {key}命令獲取編碼類型。以下:

redis> set str:1 hello
OK redis> object encoding str:1 "embstr" // embstr編碼字符串 redis> lpush list:1 1 2 3 (integer) 3 redis> object encoding list:1 "ziplist" // ziplist編碼列表 

Redis針對每種數據類型(type)能夠採用至少兩種編碼方式來實現,下表表示type和encoding的對應關係。

表:type和encoding對應關係表

類型 編碼方式 數據結構
string raw 
embstr 
int
動態字符串編碼 
優化內存分配的字符串編碼 
整數編碼
hash hashtable 
ziplist
散列表編碼 
壓縮列表編碼
list linkedlist 
ziplist 
quicklist
雙向鏈表編碼 
壓縮列表編碼 
3.2版本新的列表編碼
set hashtable 
intset
散列表編碼 
整數集合編碼
zset skiplist 
ziplist
跳躍表編碼 
壓縮列表編碼

瞭解編碼和類型對應關係以後,咱們不由疑惑Redis爲何須要對一種數據結構實現多種編碼方式?

主要緣由是Redis做者想經過不一樣編碼實現效率和空間的平衡。好比當咱們的存儲只有10個元素的列表,當使用雙向鏈表數據結構時,必然須要維護大量的內部字段如每一個元素須要:前置指針,後置指針,數據指針等,形成空間浪費,若是採用連續內存結構的壓縮列表(ziplist),將會節省大量內存,而因爲數據長度較小,存取操做時間複雜度即便爲O(n2)性能也可知足需求。

Redis內存優化

2.控制編碼類型

編碼類型轉換在Redis寫入數據時自動完成,這個轉換過程是不可逆的,轉換規則只能從小內存編碼向大內存編碼轉換。例如:

redis> lpush list:1 a b c d (integer) 4 //存儲4個元素 redis> object encoding list:1 "ziplist" //採用ziplist壓縮列表編碼 redis> config set list-max-ziplist-entries 4 OK //設置列表類型ziplist編碼最大容許4個元素 redis> lpush list:1 e (integer) 5 //寫入第5個元素e redis> object encoding list:1 "linkedlist" //編碼類型轉換爲鏈表 redis> rpop list:1 "a" //彈出元素a redis> llen list:1 (integer) 4 // 列表此時有4個元素 redis> object encoding list:1 "linkedlist" //編碼類型依然爲鏈表,未作編碼回退 

以上命令體現了list類型編碼的轉換過程,其中Redis之因此不支持編碼回退,主要是數據增刪頻繁時,數據向壓縮編碼轉換很是消耗CPU,得不償失。以上示例用到了list-max-ziplist-entries參數,這個參數用來決定列表長度在多少範圍內使用ziplist編碼。固然還有其它參數控制各類數據類型的編碼,以下表所示:

表:hash,list,set,zset內部編碼配置

類型 編碼 決定條件
hash ziplist 知足全部條件: 
value最大空間(字節)<=hash-max-ziplist-value 
field個數<=hash-max-ziplist-entries
同上 hashtable 知足任意條件: 
value最大空間(字節)>hash-max-ziplist-value 
field個數>hash-max-ziplist-entries
list ziplist ziplist 知足全部條件: 
value最大空間(字節)<=list-max-ziplist-value 
鏈表長度<=list-max-ziplist-entries
同上 linkedlist 知足任意條件 
value最大空間(字節)>list-max-ziplist-value 
鏈表長度>list-max-ziplist-entries
同上 quicklist 3.2版本新編碼: 
廢棄list-max-ziplist-entries和list-max-ziplist-entries配置 
使用新配置: 
list-max-ziplist-size:表示最大壓縮空間或長度,最大空間使用[-5-1]範圍配置,默認-2表示8KB,正整數表示最大壓縮長度 
list-compress-depth:表示最大壓縮深度,默認=0不壓縮
set intset 知足全部條件: 
元素必須爲整數 
集合長度<=set-max-intset-entries
同上 hashtable 知足任意條件 
元素非整數類型 
集合長度>hash-max-ziplist-entries
zset ziplist 知足全部條件: 
value最大空間(字節)<=zset-max-ziplist-value 
有序集合長度<=zset-max-ziplist-entries
同上 skiplist 知足任意條件: 
value最大空間(字節)>zset-max-ziplist-value 
有序集合長度>zset-max-ziplist-entries

掌握編碼轉換機制,對咱們經過編碼來優化內存使用很是有幫助。下面以hash類型爲例,介紹編碼轉換的運行流程,以下圖所示。

理解編碼轉換流程和相關配置以後,可使用config set命令設置編碼相關參數來知足使用壓縮編碼的條件。對於已經採用非壓縮編碼類型的數據如hashtable,linkedlist等,設置參數後即便數據知足壓縮編碼條件,Redis也不會作轉換,須要重啓Redis從新加載數據才能完成轉換。

3.ziplist編碼

ziplist編碼主要目的是爲了節約內存,所以全部數據都是採用線性連續的內存結構。ziplist編碼是應用範圍最廣的一種,能夠分別做爲hash、list、zset類型的底層數據結構實現。首先從ziplist編碼結構開始分析,它的內部結構相似這樣: <….> 。一個ziplist能夠包含多個entry(元素),每一個entry保存具體的數據(整數或者字節數組),內部結構以下圖所示。

ziplist結構字段含義:

  • 1) zlbytes:記錄整個壓縮列表所佔字節長度,方便從新調整ziplist空間。類型是int-32,長度爲4字節
  • 2) zltail:記錄距離尾節點的偏移量,方便尾節點彈出操做。類型是int-32,長度爲4字節
  • 3) zllen:記錄壓縮鏈表節點數量,當長度超過216-2時須要遍歷整個列表獲取長度,通常不多見。類型是int-16,長度爲2字節
  • 4) entry:記錄具體的節點,長度根據實際存儲的數據而定。
    • a) prev_entry_bytes_length:記錄前一個節點所佔空間,用於快速定位上一個節點,可實現列表反向迭代。
    • b) encoding:標示當前節點編碼和長度,前兩位表示編碼類型:字符串/整數,其他位表示數據長度。
    • c) contents:保存節點的值,針對實際數據長度作內存佔用優化。
  • 5) zlend:記錄列表結尾,佔用一個字節。

根據以上對ziplist字段說明,能夠分析出該數據結構特色以下:

  • 1) 內部表現爲數據緊湊排列的一塊連續內存數組。
  • 2) 能夠模擬雙向鏈表結構,以O(1)時間複雜度入隊和出隊。
  • 3) 新增刪除操做涉及內存從新分配或釋放,加大了操做的複雜性。
  • 4) 讀寫操做涉及複雜的指針移動,最壞時間複雜度爲O(n2)。
  • 5) 適合存儲小對象和長度有限的數據。

下面經過測試展現ziplist編碼在不一樣類型中內存和速度的表現,以下表所示。

表:ziplist在hash,list,zset內存和速度測試

類型 數據量 key總數量 長度 value大小 普通編碼內存量/平均耗時 壓縮編碼內存量/平均耗時 內存下降比例 耗時增加倍數
hash 100萬 1千 1千 36字節 103.37M/0.84微秒 43.83M/13.24微秒 57.5% 15倍
list 100萬 1千 1千 36字節 92.46M/2.04微秒 39.92M/5.45微秒 56.8% 2.5倍  
zset 100萬 1千 1千 36字節 151.84M/1.85微秒 43.83M/77.88微秒 71% 42倍

測試數據採用100W個36字節數據,劃分爲1000個鍵,每一個類型長度統一爲1000。從測試結果能夠看出:

1) 使用ziplist能夠分別做爲hash,list,zset數據類型實現。

2) 使用ziplist編碼類型能夠大幅下降內存佔用。

3) ziplist實現的數據類型相比原生結構,命令操做更加耗時,不一樣類型耗時排序:list < hash < zset。

ziplist壓縮編碼的性能表現跟值長度和元素個數密切相關,正由於如此Redis提供了{type}-max-ziplist-value和{type}-max-ziplist-entries相關參數來作控制ziplist編碼轉換。最後再次強調使用ziplist壓縮編碼的原則:追求空間和時間的平衡。

開發提示:

1)針對性能要求較高的場景使用ziplist,建議長度不要超過1000,每一個元素大小控制在512字節之內。

2)命令平均耗時使用info Commandstats命令獲取,包含每一個命令調用次數,總耗時,平均耗時,單位微秒。

4.intset編碼

intset編碼是集合(set)類型編碼的一種,內部表現爲存儲有序,不重複的整數集。當集合只包含整數且長度不超過set-max-intset-entries配置時被啓用。執行如下命令查看intset表現:

127.0.0.1:6379> sadd set:test 3 4 2 6 8 9 2 (integer) 6 //亂序寫入6個整數 127.0.0.1:6379> object encoding set:test "intset" //使用intset編碼 127.0.0.1:6379> smembers set:test "2" "3" "4" "6" "8" "9" // 排序輸出整數結合 redis> config set set-max-intset-entries 6 OK //設置intset最大容許整數長度 redis> sadd set:test 5 (integer) 1 //寫入第7個整數 5 redis> object encoding set:test "hashtable" // 編碼變爲hashtable redis> smembers set:test "8" "3" "5" "9" "4" "2" "6" //亂序輸出 

以上命令能夠看出intset對寫入整數進行排序,經過O(log(n))時間複雜度實現查找和去重操做,intset編碼結構以下圖所示。

intset的字段結構含義:

1) encoding:整數表示類型,根據集合內最長整數值肯定類型,整數類型劃分三種:int-16,int-32,int-64。

2) length:表示集合元素個數。

3) contents:整數數組,按從小到大順序保存。

intset保存的整數類型根據長度劃分,當保存的整數超出當前類型時,將會觸發自動升級操做且升級後再也不作回退。升級操做將會致使從新申請內存空間,把原有數據按轉換類型後拷貝到新數組。

開發提示:使用intset編碼的集合時,儘可能保持整數範圍一致,如都在int-16範圍內。防止個別大整數觸發集合升級操做,產生內存浪費。

下面經過測試查看ziplist編碼的集合內存和速度表現,以下表所示。

表:ziplist編碼在set下內存和速度表現

數據量 key大小 value大小 編碼 集合長度 內存量 內存下降比例 平均耗時
100w 20byte 7字節 hashtable 1千 61.97MB 0.78毫秒
100w 20byte 7字節 intset 1千 4.77MB 92.6% 0.51毫秒
100w 20byte 7字節 ziplist 1千 8.67MB 86.2% 13.12毫秒

根據以上測試結果發現intset表現很是好,一樣的數據內存佔用只有不到hashtable編碼的十分之一。intset數據結構插入命令複雜度爲O(n),查詢命令爲O(log(n)),因爲整數佔用空間很是小,因此在集合長度可控的基礎上,寫入命令執行速度也會很是快,所以當使用整數集合時儘可能使用intset編碼。上表測試第三行把ziplist-hash類型也放入其中,主要由於intset編碼必須存儲整數,當集合內保存非整數數據時,沒法使用intset實現內存優化。這時可使用ziplist-hash類型對象模擬集合類型,hash的field看成集合中的元素,value設置爲1字節佔位符便可。使用ziplist編碼的hash類型依然比使用hashtable編碼的集合節省大量內存。

六 控制key的數量

當使用Redis存儲大量數據時,一般會存在大量鍵,過多的鍵一樣會消耗大量內存。Redis本質是一個數據結構服務器,它爲咱們提供多種數據結構,如hash,list,set,zset 等結構。使用Redis時不要進入一個誤區,大量使用get/set這樣的API,把Redis當成Memcached使用。對於存儲相同的數據內容利用Redis的數據結構下降外層鍵的數量,也能夠節省大量內存。以下圖所示,經過在客戶端預估鍵規模,把大量鍵分組映射到多個hash結構中下降鍵的數量。

hash結構下降鍵數量分析:

  • 根據鍵規模在客戶端經過分組映射到一組hash對象中,如存在100萬個鍵,能夠映射到1000個hash中,每一個hash保存1000個元素。
  • hash的field可用於記錄原始key字符串,方便哈希查找。
  • hash的value保存原始值對象,確保不要超過hash-max-ziplist-value限制。 
    下面測試這種優化技巧的內存表現,以下表所示。

表:hash分組控制鍵規模測試

數據量 key大小 value大小 string類型佔用內存 hash-ziplist類型佔用內存 內存下降比例 string:set平均耗時 hash:hset平均耗時
200w 20byte 512byte 1392.64MB 1000.97MB 28.1% 2.13微秒 21.28微秒
200w 20byte 200byte 596.62MB 399.38MB 33.1% 1.49微秒 16.08微秒
200w 20byte 100byte 382.99MB 211.88MB 44.6% 1.30微秒 14.92微秒
200w 20byte 50byte 291.46MB 110.32MB 62.1% 1.28微秒 13.48微秒
200w 20byte 20byte 246.40MB 55.63MB 77.4% 1.10微秒 13.21微秒
200w 20byte 5byte 199.93MB 24.42MB 87.7% 1.10微秒 13.06微秒

經過這個測試數據,能夠說明:

  • 一樣的數據使用ziplist編碼的hash類型存儲比string類型節約內存
  • 節省內存量隨着value空間的減小,愈來愈明顯。
  • hash-ziplist類型比string類型寫入耗時,但隨着value空間的減小,耗時逐漸下降。 
    使用hash重構後節省內存量效果很是明顯,特變對於存儲小對象的場景,內存只有不到原來的1/5。下面分析這種內存優化技巧的關鍵點: 
    1) hash類型節省內存的原理是使用ziplist編碼,若是使用hashtable編碼方式反而會增長內存消耗。 
    2) ziplist長度須要控制在1000之內,不然因爲存取操做時間複雜度在O(n)到O(n2)之間,長列表會致使CPU消耗嚴重,得不償失。 
    3) ziplist適合存儲的小對象,對於大對象不但內存優化效果不明顯還會增長命令操做耗時。 
    4) 須要預估鍵的規模,從而肯定每一個hash結構須要存儲的元素數量。 
    5) 根據hash長度和元素大小,調整hash-max-ziplist-entries和hash-max-ziplist-value參數,確保hash類型使用ziplist編碼。

關於hash鍵和field鍵的設計:

1) 當鍵離散度較高時,能夠按字符串位截取,把後三位做爲哈希的field,以前部分做爲哈希的鍵。如:key=1948480 哈希key=group:hash:1948,哈希field=480。

2) 當鍵離散度較低時,可使用哈希算法打散鍵,如:使用crc32(key)&10000函數把全部的鍵映射到「0-9999」整數範圍內,哈希field存儲鍵的原始值。

3) 儘可能減小hash鍵和field的長度,如使用部分鍵內容。

使用hash結構控制鍵的規模雖然能夠大幅下降內存,但一樣會帶來問題,須要提早作好規避處理。以下:

  1. 客戶端須要預估鍵的規模並設計hash分組規則,加劇客戶端開發成本。
  2. hash重構後全部的鍵沒法再使用超時(expire)和LRU淘汰機制自動刪除,須要手動維護刪除。
  3. 對於大對象,如1KB以上的對象。使用hash-ziplist結構控制鍵數量。 
    不過瑕不掩瑜,對於大量小對象的存儲場景,很是適合使用ziplist編碼的hash類型控制鍵的規模來下降內存。
開發提示:使用ziplist+hash優化keys後,若是想使用超時刪除功能,開發人員能夠存儲每一個對象寫入的時間,再經過定時任務使用hscan命令掃描數據,找出hash內超時的數據項刪除便可。 

本文主要講解Redis內存優化技巧,Redis的數據特性是」ALL IN MEMORY」,優化內存將變得很是重要。對於內存優化建議讀者先要掌握Redis內存存儲的特性好比字符串,壓縮編碼,整數集合等,再根據數據規模和所用命令需求去調整,從而達到空間和效率的最佳平衡。建議使用Redis存儲大量數據時,把內存優化環節加入到前期設計階段,不然數據大幅增加後,開發人員須要面對從新優化內存所帶來開發和數據遷移的雙重成本。當Redis內存不足時,首先考慮的問題不是加機器作水平擴展,應該先嚐試作內存優化。當遇到瓶頸時,再去考慮水平擴展。即便對於集羣化方案,垂直層面優化也一樣重要,避免沒必要要的資源浪費和集羣化後的管理成本。

原文地址:http://www.tuicool.com/articles/NBBnUjM

相關文章
相關標籤/搜索