Redis所有的數據都在內存中,而內存又是非常寶貴的資源。對於如何優化內存使用一直是Redis用戶非常關注的問題。本文讓我們深入到Redis細節中,學習內存優化的技巧。分為如下幾個部分:
一.redisObject對象
二.縮減鍵值對象
三.共享對象池
四.字符串優化
五.編碼優化
六.控制key的數量
一. redisObject對象
Redis存儲的所有值對象在內部定義為redisObject結構體,內部結構如下圖所示。
Redis存儲的數據都使用redisObject來封裝,包括string,hash,list,set,zset在內的所有數據類型。理解redisObject對內存優化非常有幫助,下面針對每個字段做詳細說明:
1.type字段:
表示當前對象使用的數據類型,Redis主要支持5種數據類型:string,hash,list,set,zset。可以使用type {key}命令查看對象所屬類型,type命令返回的是值對象類型,鍵都是string類型。
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結構控制鍵的規模雖然可以大幅降低內存,但同樣會帶來問題,需要提前做好規避處理。如下:
- 客戶端需要預估鍵的規模並設計hash分組規則,加重客戶端開發成本。
- hash重構后所有的鍵無法再使用超時(expire)和LRU淘汰機制自動刪除,需要手動維護刪除。
- 對於大對象,如1KB以上的對象。使用hash-ziplist結構控制鍵數量。
不過瑕不掩瑜,對於大量小對象的存儲場景,非常適合使用ziplist編碼的hash類型控制鍵的規模來降低內存。
開發提示:使用ziplist+hash優化keys后,如果想使用超時刪除功能,開發人員可以存儲每個對象寫入的時間,再通過定時任務使用hscan命令掃描數據,找出hash內超時的數據項刪除即可。
本文主要講解Redis內存優化技巧,Redis的數據特性是”ALL IN MEMORY”,優化內存將變得非常重要。對於內存優化建議讀者先要掌握Redis內存存儲的特性比如字符串,壓縮編碼,整數集合等,再根據數據規模和所用命令需求去調整,從而達到空間和效率的最佳平衡。建議使用Redis存儲大量數據時,把內存優化環節加入到前期設計階段,否則數據大幅增長后,開發人員需要面對重新優化內存所帶來開發和數據遷移的雙重成本。當Redis內存不足時,首先考慮的問題不是加機器做水平擴展,應該先嘗試做內存優化。當遇到瓶頸時,再去考慮水平擴展。即使對於集群化方案,垂直層面優化也同樣重要,避免不必要的資源浪費和集群化后的管理成本。
原文地址:http://www.tuicool.com/articles/NBBnUjM