Redis全部的數據都存在內存中,當前內存雖然愈來愈便宜,但跟廉價的硬盤相比成本仍是比較昂貴,所以如何高效利用Redis內存變得很是重要。redis
高效利用Redis內存首先須要理解Redis內存消耗在哪裏,如何管理內存,最後才能考慮如何優化內存。算法
1、內存消耗數據庫
有些內存消耗是必不可少的,而有些能夠經過參數調整和合理使用來規避內存浪費。json
內存消耗能夠分爲進程自身消耗和子進程消耗。數組
1.內存使用統計緩存
root@bigjun:~# redis-cli 127.0.0.1:6379> info memory # Memory used_memory:848336 Redis分配器分配的內存總量,也就是內部存儲全部數據內存佔用量 used_memory_human:828.45K 以可讀的形式返回user_memory used_memory_rss:4304896 從操做系統的角度顯示Redis進程佔用的物理內存空間 used_memory_rss_human:4.11M 以可讀的形式返回used_memory_rss used_memory_peak:848336 內存使用的最大值,表示used_memory的峯值 used_memory_peak_human:828.45K 以可讀的形式返回used_memory_peak used_memory_peak_perc:100.01% used_memory_overhead:836078 used_memory_startup:786448 used_memory_dataset:12258 used_memory_dataset_perc:19.81% total_system_memory:4112789504 系統總內存大小 total_system_memory_human:3.83G 以可讀的形式返回total_system_memory used_memory_lua:37888 Lua引擎所消耗的內存大小 used_memory_lua_human:37.00K 以可讀的形式返回used_memory_lua maxmemory:0 maxmemory_human:0B maxmemory_policy:noeviction mem_fragmentation_ratio:5.07 used_memory_rss/used_memory比值,表示內存碎片率 mem_allocator:jemalloc-4.0.3 Redis使用的內存分配器,默認爲jemalloc active_defrag_running:0 lazyfree_pending_objects:0 須要重點關注的指標有:used_memory_rss、used_memory以及他們的比值mem_fragmentation_ratio 當mem_fragmentation_ratio > 1時,說明used_memory_rss - used_memory多處的部份內存並無用於數據存儲,而是被內存碎片所消耗,若是二者相差很大,說明碎片率嚴重。 當mem_fragmentation_ratio < 1 時,這種狀況通常出如今操做系統把Redis內存交換到硬盤致使,出現這種狀況要格外關注,因爲硬盤速度遠遠慢於內存,Redis性能會變得不好,甚至僵死。
2.內存消耗劃分安全
Redis進程內消耗主要包括:自身內存+對象內存+緩衝內存+內存碎片,其中Redis空進程自身內存消耗很是少,一般used_memory_rss在3MB左右,used_memory在800KB左右,一個空的Redis進程消耗內存能夠忽略不計。服務器
(1)對象內存數據結構
對象內存是Redis內存佔用最大的一塊,存儲着用戶全部的數據。Redis全部的數據都採用key-value數據類型,每次建立鍵值對時,至少建立兩個類型對象:key對象和value對象。架構
對象內存消耗能夠簡單理解爲sizeof(keys)+sizeof(values)。
鍵對象都是字符串,在使用Redis時很容易忽略鍵對內存消耗的影響,應當避免使用過長的鍵。
value對象更復雜些,主要包含5種基本數據類型:字符串、列表、哈希、集合、有序集合。
其餘數據類型都是創建在這5種數據結構之上實現的,如:Bitmaps和HyperLogLog使用字符串實現,GEO使用有序集合實現等。每種value對象類型根據使用規模不一樣,佔用內存不一樣。在使用時必定要合理預估並監控value對象佔用狀況,避免內存溢出。
(2)緩衝內存
緩衝內存主要包括:客戶端緩衝、複製積壓緩衝區、AOF緩衝區。
(3)內存碎片
Redis默認的內存分配器採用jemalloc,可選的分配器還有:glibc、tcmalloc。
內存分配器爲了更好地管理和重複利用內存,分配內存策略通常採用固定範圍的內存塊進行分配。
例如jemalloc在64位系統中將內存空間劃分爲:小、大、巨大三個範圍。每一個範圍內又劃分爲多個小的內存塊單位,以下所示:
好比當保存5KB對象時jemalloc可能會採用8KB的塊存儲,而剩下的3KB空間變爲了內存碎片不能再分配給其餘對象存儲。內存碎片問題雖然是全部內存服務的通病,可是jemalloc針對碎片化問題專門作了優化,通常不會存在過分碎片化的問題,正常的碎片率(mem_fragmentation_ratio)在1.03左右。
可是當存儲的數據長短差別較大時,如下場景容易出現高內存碎片問題:
出現高內存碎片問題時常見的解決方式以下:
3.子進程內存消耗
子進程內存消耗主要指執行AOF/RDB重寫時Redis建立的子進程內存消耗。
Redis執行fork操做產生的子進程內存佔用量對外表現爲與父進程相同,理論上須要一倍的物理內存來完成重寫操做。但Linux具備寫時複製技術(copy-on-write),父子進程會共享相同的物理內存頁,當父進程處理寫請求時會對須要修改的頁複製出一份副本完成寫操做,而子進程依然讀取fork時整個父進程的內存快照。
子進程內存消耗總結以下:
2、內存管理
Redis主要經過控制內存上限和回收策略實現內存管理。
1.設置內存上限
Redis使用maxmemory參數限制最大可用內存。限制內存的目的主要有:
須要注意,maxmemory限制的是Redis實際使用的內存量,也就是used_memory統計項對應的內存。因爲內存碎片率的存在,實際消耗的內存可能會比maxmemory設置的更大,實際使用時要當心這部份內存溢出。經過設置內存上限能夠很是方便地實現一臺服務器部署多個Redis進程的內存控制。
好比一臺24GB內存的服務器,爲系統預留4GB內存,預留4GB空閒內存給其餘進程或Redis fork進程,留給Redis16GB內存,這樣能夠部署4個maxmemory=4GB的Redis進程。得益於Redis單線程架構和內存限制機制,即便沒有采用虛擬化,不一樣的Redis進程之間也能夠很好地實現CPU和內存的隔離性。
2.動態調整內存上限
Redis的內存上限能夠經過config set maxmemory進行動態修改,即修改最大可用內存。
例如以前的示例,當發現Redis-2沒有作好內存預估,實際只用了不到2GB內存,而Redis-1實例須要擴容到6GB內存纔夠用,這時能夠分別執行以下命令進行調整:
Redis-1>config set maxmemory 6GB Redis-2>config set maxmemory 2GB
經過動態修改maxmemory,能夠實如今當前服務器下動態伸縮Redis內存的目的。
若是此時Redis-3和Redis-4實例也須要分別擴容到6GB,這時超出系統物理內存限制就不能簡單的經過調整maxmemory來達到擴容的目的,須要採用在線遷移數據或者經過複製切換服務器來達到擴容的目的。
3.內存回收策略
Redis的內存回收機制主要體如今如下兩個方面:
(1)刪除過時鍵對象
Redis全部的鍵均可以設置過時屬性,內部保存在過時字典中。因爲進程內保存大量的鍵,維護每一個鍵精準的過時刪除機制會致使消耗大量的CPU,對於單線程的Redis來講成本太高,所以Redis採用惰性刪除和定時任務刪除機制實現過時鍵的內存回收。
流程說明:
(2)內存溢出控制策略
當Redis所用內存達到maxmemory上限時會觸發相應的溢出控制策略。具體策略受maxmemory-policy參數控制,Redis支持6種策略,以下所示:
3、內存優化
1.RedisObject對象
Redis存儲的全部值對象在內部定義爲redisObject結構體,內部結構如圖
Redis存儲的數據都使用redisObject來封裝,包括string、hash、list、set、zset在內的全部數據類型。
2.縮減鍵值對象
下降Redis內存使用最直接的方式就是縮減鍵(key)和值(value)的長度。
值對象除了存儲二進制數據以外,一般還會使用通用格式存儲數據好比:json、xml等做爲字符串存儲在Redis中。這種方式優勢是方便調試和跨語言,可是一樣的數據相比字節數組所需的空間更大,在內存緊張的狀況下,可使用通用壓縮算法壓縮json、xml後再存入Redis,從而下降內存佔用,例如使用GZIP壓縮後的json可下降約60%的空間。
3.共享對象池
共享對象池是指Redis內部維護[0-9999]的整數對象池。建立大量的整數類型redisObject存在內存開銷,每一個redisObject內部結構至少佔16字節,甚至超過了整數自身空間消耗。因此Redis內存維護一個[0-9999]的整數對象池,用於節約內存。除了整數值對象,其餘類型如list、hash、set、zset內部元素也可使用整數對象池。所以開發中在知足需求的前提下,儘可能使用整數對象以節省內存。
Object refcount命令主要用於調試,可以返回指定key所對應的value被引用的次數,在0-9999之間的整數,都是共享內存的,因此返回值是同一個數:2147483647。
因爲100是0-9999之間的數,因此都會共享內存。 127.0.0.1:6379> keys * (empty list or set) 127.0.0.1:6379> set test:1 100 OK 127.0.0.1:6379> object refcount test:1 (integer) 2147483647 127.0.0.1:6379> set test:2 100 OK 127.0.0.1:6379> object refcount test:2 (integer) 2147483647
開啓共享對象池以後,
redis> set foo 100 OK redis> object refcount foo (integer) 2 redis> set bar 100 OK redis> object refcount bar (integer) 3
4.字符串優化
字符串對象是Redis內部最經常使用的數據類型。全部的鍵都是字符串類型,值對象數據除了整數以外都使用字符串存儲。
好比執行命令:lpush cache:type "redis" "memcache" "tair" "levelDB",Redis首先建立"cache:type"鍵字符串,而後建立鏈表對象,鏈表對象內再包含四個字符串對象,排除Redis內部用到的字符串對象以外至少建立5個字符串對象。
(1)字符串結構
Redis沒有采用原生C語言的字符串類型而是本身實現了字符串結構,內部簡單動態字符串(simple dynamic string,SDS)。
Redis自身實現的字符串結構有以下特色:
(2)預分配機制
字符串之因此採用預分配的方式是防止修改操做須要不斷重分配內存和字節數據拷貝。但一樣也會形成內存的浪費。字符串預分配每次並不都是翻倍擴容,空間預分配規則以下:
(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結構,能夠看到使用內存的差別:
根據測試結構,第一次默認配置下使用hash類型,內存消耗不但沒有下降反而比字符串存儲多出2倍,而調整hash-max-ziplist-value=66以後內存下降爲535.60M。由於json的videoAlbumPic屬性長度是65,而hash-max-ziplist-value默認值是64,Redis採用hashtable編碼方式,反而消耗了大量內存。調整配置後hash類型內部編碼方式變爲ziplist,相比字符串更省內存且支持屬性的部分操做。
5.編碼優化
(1)編碼類型
Redis對外提供了string、list、hash、set、zet等類型,可是Redis內部針對不一樣類型存在編碼的概念,所謂編碼就是具體使用哪一種底層數據結構來實現。編碼不一樣將直接影響數據的內存佔用和讀寫效率。使用object encoding {key}命令獲取編碼類型。
127.0.0.1:6379> set str:1 hello OK 127.0.0.1:6379> object encoding str:1 "embstr" 127.0.0.1:6379> lpush list:1 1 2 3 (integer) 3 127.0.0.1:6379> object encoding list:1 "quicklist"
Redis針對每種數據類型(type)能夠採用至少兩種編碼方式來實現。
(2)控制編碼類型
編碼類型轉換在Redis寫入數據時自動完成,這個轉換過程是不可逆的,轉換規則只能從小內存編碼向大內存編碼轉換。
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" // 編碼類型依然爲鏈表,未作編碼回退
Redis之因此不支持編碼回退,主要是數據增刪頻繁時,數據向壓縮編碼轉換很是消耗CPU,得不償失。以上示例用到了list-max-ziplist-entries參數,這個參數用來決定列表長度在多少範圍內使用ziplist編碼。固然還有其餘參數控制各類數據類型的編碼。
6.控制鍵的數量
當使用Redis存儲大量數據時,一般會存在大量鍵,過多的鍵一樣會消耗大量內存。
Redis本質是一個數據結構服務器,它爲咱們提供多種數據結構,如hash、list、set、zset等。使用Redis時不要進入一個誤區,大量使用get/set這樣的API,把Redis當成Memcached使用。
對於存儲相同的數據內容利用Redis的數據結構下降外層鍵的數量,也能夠節省大量內存。
能夠經過在客戶端預估鍵規模,把大量鍵分組映射到多個hash結構中下降鍵的數量,如圖:
hash結構下降鍵數量分析:
經過這個測試數據,能夠說明:
使用hash重構後節省內存量效果很是明顯,特別對於存儲小對象的場景,內存只有不到原來的1/5。下面分析這種內存優化技巧的關鍵點:
關於hash鍵和field鍵的設計:
使用hash結構控制鍵的規模雖然能夠大幅下降內存,但一樣會帶來問題,須要提早作好規避處理。以下所示:
不過瑕不掩瑜,對於大量小對象的存儲場景,很是適合使用ziplist編碼的hash類型控制鍵的規模來下降內存。