內(nèi)存優(yōu)化
Redis所有的數(shù)據(jù)都在內(nèi)存中,而內(nèi)存又是非常寶貴的資源偷办。如何優(yōu)化內(nèi)存的使用一直是Redis用戶非常關(guān)注的問題米奸。本節(jié)深入到Redis細節(jié)中,探索內(nèi)存優(yōu)化的技巧爽篷。
-
redisObject對象
Redis存儲的所有值對象在內(nèi)部定義為redisObject結(jié)構(gòu)體,內(nèi)部結(jié)構(gòu)如圖:
2019-05-01-19-10-30.pngRedis存儲的數(shù)據(jù)都是用redisObject來封裝慢睡,包括string逐工、hash、list漂辐、set泪喊、zset在內(nèi)的所有數(shù)據(jù)類型。理解redisObject對內(nèi)存優(yōu)化非常有幫助髓涯,下面針對每個字段做詳細說明:
type字段:表示當前對象使用的數(shù)據(jù)類型袒啼,Redis主要支持5中數(shù)據(jù)類型:string、hash纬纪、list蚓再、set、zset包各≌觯可以使用type {key}命令查看對象所屬類型,type命令返回的是值對象類型问畅,鍵都是string類型娃属。
encoding字段:表示Redis內(nèi)存編碼類型六荒,encoding在Redis內(nèi)部使用,代表當前對象內(nèi)部采用那種數(shù)據(jù)結(jié)構(gòu)實現(xiàn)矾端。理解Redis內(nèi)部編碼方式對于優(yōu)化內(nèi)存非常重要掏击,同一個對象采用不同的編碼實現(xiàn)內(nèi)存占用存在明顯差異。
lru字段:記錄對象最后一次被訪問的時間秩铆,當配置了maxmemory和maxmemory-policy-volatile-lru或者allkeys-lru是砚亭,用于輔助LRU算法刪除鍵數(shù)據(jù)〔蜓可以使用object idletime {key}命令在不更新lru字段情況下查看當前鍵的空閑時間钠惩。
開發(fā)提示:可以使用scan+object idletime命令批量查詢哪些鍵長時間未被訪問,找出長時間不訪問的鍵進行清理族阅,可降低內(nèi)存占用篓跛。
refcount字段:記錄當前對象被引用的次數(shù),用于通過引用次數(shù)回收內(nèi)存坦刀,當refcount=0時愧沟,可以安全回收當前對象空間。使用object refcount {key}獲取當前對象引用鲤遥。當對象為整數(shù)且范圍在[0-9999]時沐寺,Redis可以使用共享對象的方式來節(jié)省內(nèi)存。
*ptr字段:與對象的數(shù)據(jù)內(nèi)容相關(guān)盖奈,如果是整數(shù)混坞,直接存儲數(shù)據(jù);否則表示指向數(shù)據(jù)的指針钢坦。Redis在3.0之后對值對象是字符串且長度<=39字節(jié)的數(shù)據(jù)究孕,內(nèi)部編碼為embstr類型,字符串sds和redisObject一起分配爹凹,從而只要一次內(nèi)存操作即可厨诸。
開發(fā)提示:高并發(fā)寫入場景中,在條件允許的情況下禾酱,建議字符串長度控制在39字節(jié)以內(nèi)微酬,減少創(chuàng)建redisObject內(nèi)存分配次數(shù),從而提高性能颤陶。
-
縮減鍵值對象
降低Redis使用最直接的方式就是縮減鍵(key)和值(value)的長度颗管。
- key長度:如在設(shè)計鍵是,在完整描述業(yè)務情況下滓走,鍵值越短越好忙上。如user:{uid}:friends:notify:{fid}可以簡化為u:{uid}:fs:nt:{fid}。
-value長度:值對象縮減比較復雜闲坎,常見需求是把業(yè)務對象序列化成二進制數(shù)組放入Redis疫粥。首先應該在業(yè)務上精簡業(yè)務對象茬斧,去掉不必要的屬性避免存儲無效數(shù)據(jù)。其次在序列化工具選擇上梗逮,應該選擇更高效的序列化工具來降低字節(jié)數(shù)組大小项秉。以Java為例,內(nèi)置的序列化方式無論從速度還是壓縮比都不盡如人意慷彤,這是可以選擇更搞笑的序列化工具娄蔼,如:protostuff、kryo等底哗,其中Java常見序列化工具空間壓縮比例對比如下:
protostuff-graph > kryo-serializer > hessian > jackson > xml/JAXB > java-build-in-serializer > jboss-serialization
其中java-build-in-serializer表示Java內(nèi)置序列化方式岁诉,其他語言也有各自對應的高效序列化工具。
值對象除了存儲二進制數(shù)據(jù)之外跋选,通常還會使用通用格式存儲數(shù)據(jù)比如:json涕癣、xml等作為字符串存儲在Redis中。這種方式是方便調(diào)試和跨語言前标,但是同樣的數(shù)據(jù)相比字節(jié)數(shù)組所需的空間更大坠韩,在內(nèi)存緊張的情況下,可以使用通用壓縮算法壓縮json炼列、xml后在存入Redis只搁,從而降低內(nèi)存占用,例如使用GZIP壓縮后的json可降低60%的空間俭尖。
開發(fā)提示:當頻繁壓縮解壓json等文本數(shù)據(jù)時氢惋,開發(fā)人員考慮壓縮速度和計算開銷成本,這里推薦使用Google的Snappy壓縮工具稽犁,在特定的壓縮率情況下效率遠遠高于GZIP等傳統(tǒng)壓縮工具明肮,且支持所有主流語言環(huán)境。
-
共享對象池
共享對象池是指Redis內(nèi)部維護[0-9999]的整數(shù)對象池缭付。創(chuàng)建大量的數(shù)據(jù)類型redisObject存在內(nèi)存開銷,每個redisObject內(nèi)內(nèi)部結(jié)構(gòu)至少占16字節(jié)循未,甚至超過了整數(shù)自身空間消耗陷猫。所以Redis內(nèi)部維護一個[0-9999]的整數(shù)對象池,用于節(jié)約內(nèi)存的妖。除了整數(shù)值對象绣檬,其他類型如list、hash嫂粟、set娇未、zset內(nèi)部元素也可以使用整數(shù)對象池。因此開發(fā)中在滿足需求的前提下星虹,盡量使用整數(shù)對象以節(jié)省內(nèi)存零抬。
整數(shù)對象池在Redis中通過變量REDIS_SHARED_INTEGERS定義镊讼,不能通過配置修改∑揭梗可以通過object refcount命令查看對象引用數(shù)驗證是否啟用整數(shù)對象池技術(shù)蝶棋,如下:
redis> set foo 100 OK redis> object refcount foo (integer) 2 redis> set bar 100 OK redis> object refcount bar (integer) 3
設(shè)置鍵foo等于100時,直接使用共享池內(nèi)整數(shù)對象忽妒,因此引用數(shù)是2玩裙,再設(shè)置鍵bar等于100時,引用數(shù)又變?yōu)?段直,如下圖所示:
2019-05-01-22-08-05.png使用整數(shù)對象池究竟能降低多少內(nèi)存吃溅?讓我們通過測試來對比對象池的內(nèi)存優(yōu)化效果,如下表:
操作說明 是否對象共享 key大小 value大小 used_mem used_memory_rss 插入100萬 否 20字節(jié) [0-9999]整數(shù) 199.91MB 205.28MB 插入200萬 是 20字節(jié) [0-9999] 138.87MB 143.28MB 注意:本章所有測試環(huán)境都保持一致鸯檬,信息如下:
服務器信息: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
使用共享對象池后决侈,相同的數(shù)據(jù)結(jié)構(gòu)內(nèi)存使用降低30%以上【┤颍可見當數(shù)據(jù)大量使用[0-9999]的整數(shù)時颜及,共享對象池可以節(jié)約大量內(nèi)存。需要注意的是對象池并不是只要存儲[0-9999]的整數(shù)就可以工作蹂楣。當設(shè)置maxmemory并啟用LRU相關(guān)淘汰策略如:volatile-lru俏站,allkeys-lru時,Redis禁止使用共享對象池痊土。
為什么開啟maxmemory和LRU淘汰策略后對象池無效肄扎?
LRU算法需要獲取對象最后被訪問時間,以便淘汰最長未訪問數(shù)據(jù)赁酝,每個對象最后訪問時間存儲在redisObject對象的lru字段犯祠。對象共享意味著多個引用共享同一個redisObject,這時lru字段也會被共享酌呆,導致無法獲取每個對象的最后訪問時間衡载。如果沒有設(shè)置maxmemory,知道內(nèi)存被用盡Redis也不會觸發(fā)內(nèi)存回收隙袁,所以共享對象池可以正常工作痰娱。
綜上所述,共享對象池與maxmemory+LRU策略沖突菩收,使用時需要注意梨睁。對于ziplist編碼的值對象,即使內(nèi)部數(shù)據(jù)為整數(shù)也無法使用共享對象池娜饵,因為ziplist使用壓縮且內(nèi)存連續(xù)的結(jié)構(gòu)坡贺,對象共享判斷成本過高,ziplist編碼細節(jié)構(gòu)面內(nèi)容詳細說明。
為什么只有整數(shù)對象池遍坟?
首先整數(shù)對象池復用的幾率最大拳亿,其次對象共享的一個關(guān)鍵操作就是判斷相等性,Redis之所以只有整數(shù)對象池政鼠,是因為整數(shù)比較算法時間復雜度O(1)风瘦,只保留一萬個整數(shù)為了防止對象池浪費。如果是字符串判斷相等性公般,時間復雜度變?yōu)镺(n)万搔,特別是長字符串更消耗性能(浮點數(shù)在Redis內(nèi)部使用字符串存儲)。對于更復雜的數(shù)據(jù)結(jié)構(gòu)如hash官帘、list等瞬雹,相等性判斷需要O(n^2)。對于單線程的Redis來說刽虹,這樣的開銷顯然不合理酗捌,因此Redis只保留整數(shù)共享對象池。
-
字符串優(yōu)化
字符串對象是Redis內(nèi)部最常用的數(shù)據(jù)類型涌哲。所有的鍵都是字符串類型胖缤,值對象數(shù)據(jù)除了整數(shù)之外都是用字符串存儲。比如執(zhí)行命令:lpush cache:type "redis" "memcache" "tair" "levelDB"阀圾,Redis首先創(chuàng)建"cache:type"鍵字符串哪廓,然后創(chuàng)建鏈表對象,鏈表對象內(nèi)再包含四個字符串對象初烘,排除Redis內(nèi)部用到的字符串對象之外至少創(chuàng)建5個字符串對象涡真。可見字符串對象在Redis內(nèi)部使用非常廣泛肾筐,因此深刻理解Redis字符串對于內(nèi)存優(yōu)化非常有幫助哆料。
-
字符串結(jié)構(gòu)
Redis沒有采用原生C語言的字符串類型而是自己實現(xiàn)了字符串結(jié)構(gòu),內(nèi)部簡單動態(tài)字符串(simple dynamic string, SDS)吗铐。結(jié)構(gòu)如下圖:
2019-05-01-22-38-34.pngRedis自身實現(xiàn)的字符串結(jié)構(gòu)有如下特點:
O(1)時間復雜度獲榷唷:字符串長度、已用長度唬渗、未用長度典阵。
可用于保存字節(jié)數(shù)組,支持安全的二進制數(shù)據(jù)存儲谣妻。
內(nèi)部實現(xiàn)空間預分配機制,降低內(nèi)存再分配次數(shù)卒稳。
惰性刪除機制蹋半,字符串縮減后的空間不釋放,作為預分配空間保留充坑。
-
預分配機制
因為字符串(SDS)存在預分配機制减江,日常開發(fā)中要小心預分配帶來的內(nèi)存浪費染突,例如下表的測試用例:
階段 數(shù)據(jù)量 操作說明 命令 key大小 value大小 used_mem used_memory_rss mem_fragmentation_ratio 階段1 200w 新插入200w數(shù)據(jù) set 20字節(jié) 60字節(jié) 321.98MB 332.44MB 1.02 階段2 200w 在階段1上每個對象追加60字節(jié)數(shù)據(jù) append 20字節(jié) 60字節(jié) 657.67MB 752.80MB 1.14 階段3 200w 重新插入200w數(shù)據(jù) set 20字節(jié) 120字節(jié) 474.56MB 482.45MB 1.02 從測試數(shù)據(jù)可以看出,同樣的數(shù)據(jù)追加后內(nèi)存消耗非常嚴重辈灼,下面我們結(jié)合圖來分析這一現(xiàn)象份企。階段1每個字符串對象空間占用如下圖:
2019-05-01-22-51-24.png階段1插入新的字符串后,free字段保留空間為0巡莹,總占用空間=實際占用空間+1字節(jié)司志,最后1字節(jié)保存'\O'標示結(jié)尾,這里忽略int類型len和free字段消耗的8字節(jié)降宅。在階段1原有字符串上追加60字節(jié)數(shù)據(jù)空間占用如下圖:
2019-05-01-22-56-37.png追加操作后字符串對象預分配了一倍容量作為預留空間骂远,而且大量追加操作需要內(nèi)存重新分配,造成內(nèi)存碎片率(mem_fragmentation_ratio)上升腰根。直接插入與階段2相同數(shù)據(jù)的空間占用蔑歌,如下圖:
2019-05-01-22-59-14.png階段3直接插入同等數(shù)據(jù)后领突,相比階段2節(jié)省了每個字符串對象預分配的空間,同時降低了碎片率。
字符串之所以采用預分配的方式是防止修改操作需要不斷重分配內(nèi)存和字節(jié)數(shù)據(jù)拷貝供汛。但同樣也會造成內(nèi)存的浪費。字符串預分配每次并不都是翻倍擴容构拳,空間預分配規(guī)則如下:
1)第一次創(chuàng)建len屬性等于數(shù)據(jù)實際大小掠哥,free等于0,不做預分配捕儒。
2)修改后如果已有free空間不夠且數(shù)據(jù)小于1M冰啃,每次預分配一倍容量。如原有l(wèi)en=60byte, free=0刘莹,再追加60byte阎毅,預分配120byte,總占用空間:60byte+60byte+120byte+1byte点弯。
3)修改后如果已有free空間不夠且數(shù)據(jù)大于1MB扇调,每次預分配1MB數(shù)據(jù)。如原有l(wèi)en=30MB抢肛,free=0狼钮,當在追加100byte,預分配1MB捡絮,總占用空間:30MB+100byte+1MB+1byte熬芜。
開發(fā)提示:盡量減少字符串頻繁修改操作如append、setrange福稳,改為直接使用set修改字符串涎拉,降低預分配帶來的內(nèi)存浪費和內(nèi)存碎片化。
-
字符串重構(gòu)
字符串重構(gòu):指不一定把每份數(shù)據(jù)作為字符串整體存儲,像json這樣的數(shù)據(jù)可以使用hash結(jié)構(gòu)鼓拧,使用二級結(jié)構(gòu)存儲也能幫我們節(jié)省內(nèi)存半火。同時可以使用hmget、hmset命令支持字段的部分讀取修改季俩,而不用每次整體存取钮糖。例如下面的json數(shù)據(jù):
{ "vid" : "413368768", "title" : "搜狐屌絲男士", "videoAlbumPic" : "http://photocdn.sohu.com/60160518/vrsa_ver8400079_ae433_pic26.jpg", "pid" : "6494271", "type" : "1024", "playlist" : "6494271", "playTime" : "468" }
分別使用字符串和hash結(jié)構(gòu)測試內(nèi)存表現(xiàn),如下表:
數(shù)據(jù)量|key|存儲類型|value|配置|used_mem
-|-|-|-|-|-|-
200w|20字節(jié)|string|json字符串|默認|612.62MB
200w|20字節(jié)|hash|key-value對|默認|1.88GB
200w|20字節(jié)|hash|key-value對|hash-max-ziplist-value:66|535.60MB根據(jù)測試結(jié)果酌住,第一次默認配置下使用hash類型店归,內(nèi)存消耗不但沒有降低反而比字符串存儲多出2倍,而調(diào)整hash-max-ziplist-value=66之后內(nèi)存降低為535.60MB赂韵。因為json的videoAlbumPic屬性長度是65娱节,而hash-max-ziplist-value默認值是64,Redis采用hashtable編碼方式祭示,反而消耗了大量內(nèi)存肄满。調(diào)整配置后hash類型內(nèi)部編碼方式變?yōu)閦iplist,相比字符串更省內(nèi)存且支持屬性的部分操作质涛。
-
-
編碼優(yōu)化
-
了解編碼
Redis對外提供了string稠歉、list、hash汇陆、set怒炸、zset等類型,但是Redis內(nèi)部針對不同類型存在編碼的概念毡代,所謂編碼就是具體使用哪種底層數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)阅羹。編碼不同將直接影響數(shù)據(jù)的內(nèi)存占用和讀寫效率。使用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針對每種數(shù)據(jù)類型(type)可以采用至少兩種編碼方式來實現(xiàn)捏鱼,下表表示type和encoding的對應關(guān)系:
類型 編碼方式 數(shù)據(jù)結(jié)構(gòu) string raw 動態(tài)字符串編碼 string embstr 優(yōu)化內(nèi)存分配的字符串編碼 string int 整數(shù)編碼 hash hashtable 散列表編碼 hash ziplist 壓縮列表編碼 list linkedlist 雙向鏈表編碼 list ziplist 壓縮列表編碼 list quicklist 3.2版本新的列表編碼 set hashtable 散列表編碼 set intset 整數(shù)集合編碼 zset skiplist 跳躍表編碼 zset ziplist 壓縮列表編碼 了解編碼和類型對應關(guān)系之后,我們不禁疑惑Redis為什么對一種數(shù)據(jù)結(jié)構(gòu)實現(xiàn)多種編碼方式酪耕?
主要原因是Redis作者想通過不同編碼實現(xiàn)效率和空間的平衡导梆。比如當我們的存儲只有10個元素的列表,當時用雙向鏈表數(shù)據(jù)結(jié)構(gòu)時迂烁,必然需要維護大量的內(nèi)部字段如每個元素需要:前置指針看尼、后置指針、數(shù)據(jù)指針等盟步,造成空間浪費藏斩,如果采用連續(xù)內(nèi)存結(jié)構(gòu)的壓縮列表(ziplist),將會節(jié)省大量內(nèi)存却盘,而由于數(shù)據(jù)長度較小狰域,存取操作時間復雜度即視為O(n^2)性能也可滿足需求窜觉。
-
控制編碼類型
編碼類型轉(zhuǎn)換在Redis寫入數(shù)據(jù)時自動完成,這個轉(zhuǎn)換過程是不可逆的北专,轉(zhuǎn)換規(guī)則只能從小內(nèi)存編碼向大內(nèi)存編碼轉(zhuǎn)換。
Redis之所以不支持編碼回退旬陡,主要是數(shù)據(jù)增刪頻繁時拓颓,數(shù)據(jù)向壓縮編碼轉(zhuǎn)換非常消耗CPU,得不償失描孟。下表是控制各種數(shù)據(jù)類型的編碼的參數(shù):
類型 編碼 決定條件 hash ziplist 滿足所有條件:value最大空間(字節(jié))<=hash-max-ziplist-value驶睦,field個數(shù)<=hash-max-ziplist-entries hash hashtable 滿足任意條件:滿足任意條件:value最大空間(字節(jié))>hash-max-ziplist-value,filed個數(shù)>hash-max-ziplist-entries list ziplist 滿足所有條件:value最大空間(字節(jié))<=list-max-ziplist-value匿醒,鏈表長度<=list-max-ziplist-entries list linkedlist 滿足任意條件:value最大空間(字節(jié))>list-max-ziplist-value场航,鏈表長度>list-max-ziplist-entries list quicklist 3.2版本新編碼:廢棄list-max-ziplist-entries和list-max-ziplist-value配置。使用新配置:list-max-ziplist-size:表示最大壓縮空間或長度廉羔,最大空間使用[-5-1]范圍配置溉痢,默認-2表示8KB,正整數(shù)表示最大壓縮長度憋他,list-compress-depth:表示最大壓縮深度孩饼,默認=0不壓縮。 set intset 滿足所有條件:元素必須為整數(shù)竹挡,集合長度<=set-max-intset-entries set hashtable 滿足任意條件:元素非整數(shù)類型镀娶,集合長度>set-max-intset-entries zset ziplist 滿足所有條件:value最大空閑(字節(jié))<=zset-max-ziplist-value,有序集合長度<=zset-max-ziplist-entries zset skiplist 滿足任意條件:value最大空間(字節(jié))>zset-max-ziplist-value揪罕,有序集合長度>zset-max-ziplist-entries 掌握編碼轉(zhuǎn)換機制梯码,對我們通過編碼來優(yōu)化內(nèi)存使用非常有幫助。下面以hash類型為例好啰,介紹編碼轉(zhuǎn)換的運行流程轩娶,如下圖所示:
2019-05-02-00-01-45.png理解編碼轉(zhuǎn)換流程和相關(guān)配置之后,可以使用config set命令設(shè)置編碼相關(guān)參數(shù)來滿足使用壓縮編碼的條件坎怪。對于已經(jīng)采用非壓縮編碼類型的數(shù)據(jù)如hashtable罢坝、linkedlist等,設(shè)置參數(shù)后及時數(shù)據(jù)滿足壓縮編碼條件搅窿,Redis也不會做轉(zhuǎn)換嘁酿,需要重啟Redis重新加載數(shù)據(jù)才能完成轉(zhuǎn)換。
-
ziplist編碼
ziplist編碼主要目的是為了節(jié)約內(nèi)存男应,因此所有數(shù)據(jù)都是采用線性連續(xù)的內(nèi)存結(jié)構(gòu)闹司。ziplist編碼是應用范圍最廣的一種,可以分別作為hash沐飘、list游桩、zset類型的底層數(shù)據(jù)結(jié)構(gòu)實現(xiàn)牲迫。首先從ziplist編碼結(jié)構(gòu)開始分析,它的內(nèi)部結(jié)構(gòu)類似這樣:<zlbytes><zltail><zllen><entry-1><entry-2><...><entry-n><zlend>借卧。一個ziplist可以包含多個entry(元素)盹憎,每個entry保存具體的數(shù)據(jù)(整數(shù)或者字節(jié)數(shù)組),內(nèi)部結(jié)構(gòu)如下圖:
2019-05-02-00-12-52.pngziplist結(jié)構(gòu)字段含義:
1)zlbytes:記錄整個壓縮列表所占字節(jié)長度铐刘,方便重新調(diào)整ziplist空間陪每。類型是int-32,長度為4字節(jié)镰吵。
2)zltail:記錄距離為節(jié)點的偏移量檩禾,方便尾節(jié)點彈出操作。類型是int-32疤祭,長度為4字節(jié)盼产。
3)zllen:記錄壓縮鏈表節(jié)點數(shù)量,當長度超過2^16-2時需要遍歷整個列表獲取長度勺馆,一般很少見戏售。類型是int-16,長度為2字節(jié)草穆。
4)entry:記錄具體的節(jié)點蜈项,長度根據(jù)實際存儲的數(shù)據(jù)而定。
prev_entry_bytes_length:記錄前一個節(jié)點所占空間续挟,用于快速定位上一個節(jié)點紧卒,可實現(xiàn)列表反向迭代。
encoding:標示當前節(jié)點編碼和長度诗祸,前兩位表示編碼類型:字符型/整數(shù)跑芳,其余位表示數(shù)據(jù)長度。
contents:保存節(jié)點的值直颅,針對實際數(shù)據(jù)長度做內(nèi)存占用優(yōu)化博个。
5)zlend:記錄列表結(jié)尾,占用一個字節(jié)功偿。
根據(jù)以上對ziplist字段說明盆佣,可以分析出該數(shù)據(jù)結(jié)構(gòu)特點如下:
內(nèi)部表現(xiàn)為數(shù)據(jù)緊湊排列的一塊連續(xù)內(nèi)存數(shù)組。
可以模擬雙向鏈表結(jié)構(gòu)械荷,以O(shè)(1)時間復雜度入隊和出隊共耍。
新增刪除操作涉及內(nèi)存重新分配或釋放,加大了操作的復雜性吨瞎。
讀寫操作涉及復雜的指針移動痹兜,最壞時間復雜度為O(n^2)。
適合存儲小對象和長度有限的數(shù)據(jù)颤诀。
下面通過測試展示ziplist編碼在不同類型中內(nèi)存和速度的表現(xiàn)字旭,如下表:
類型|數(shù)據(jù)量|key總數(shù)量|長度|value大小|普通編碼內(nèi)存量/平均耗時|壓縮編碼內(nèi)存量/平均耗時|內(nèi)存降低比例|耗時增長倍數(shù)
-|-|-|-|-|-|-|-|-|-
hash|100w|1k|1k|36字節(jié)|103.37M/0.84微秒|43.83M/13.24微秒|57.5%|15倍
list|100w|1k|1k|36字節(jié)|92.46M/2.04微秒|39.92M/5.45微秒|56.8%|2.5倍
zset|100w|1k|1k|36字節(jié)|151.84M/1.85微秒|43.83M/77.88微秒|71%|42倍測試數(shù)據(jù)采用100w個36字節(jié)數(shù)據(jù)对湃,劃分為1000個鍵,每個類型長度統(tǒng)一為1000遗淳。從測試結(jié)果可以看出:
1)使用ziplist可以分別作為hash拍柒、list、zset數(shù)據(jù)類型實現(xiàn)屈暗。
2)使用ziplist編碼類型可以大幅降低內(nèi)存占用斤儿。
3)ziplist實現(xiàn)的數(shù)據(jù)類型相比原生結(jié)構(gòu),命令操作更加耗時恐锦,不同類型耗時排序:list < hash < zset。
ziplist壓縮編碼的性能表現(xiàn)跟值長度和元素個數(shù)密切相關(guān)疆液,正因為如此Redis提供了{type}-max-ziplist-value和{type}-max-ziplist-entries相關(guān)參數(shù)來做控制ziplist編碼轉(zhuǎn)換一铅。最后再次強調(diào)使用ziplist壓縮編碼的原則:追求空間和時間的平衡。
開發(fā)提示:針對性能要求較高的場景使用ziplist堕油,建議長度不要超過1000潘飘,每個元素大小控制在512字節(jié)以內(nèi)。命令平均耗時使用info Commandstats命令獲取掉缺,包含每個命令調(diào)用次數(shù)卜录、總耗時、平均耗時眶明、單位為微秒艰毒。
-
intset編碼
intset編碼是集合(set)類型編碼的一種,內(nèi)部表現(xiàn)為存儲有序搜囱、不重復的整數(shù)集丑瞧。當集合只包含整數(shù)且長度不超過set-max-intset-entires配置是被啟用。
intset對寫入整數(shù)進行排序蜀肘,通過O(log(n))時間復雜度實現(xiàn)查找和去重操作绊汹,intset編碼結(jié)構(gòu)如圖所示:
2019-05-02-00-45-47.pngintset的字段結(jié)構(gòu)含義:
1)encoding:整數(shù)表示類型,根據(jù)集合內(nèi)最長整數(shù)值確定類型扮宠,整數(shù)類型劃分為三種:int-16西乖、int-32、int-64坛增。
2)length:表示集合元素個數(shù)获雕。
3)contents:整數(shù)數(shù)組,按從小到大順序保存收捣。
intset保存的整數(shù)類型根據(jù)長度劃分,當保存的整數(shù)超出當前類型時坏晦,將會觸發(fā)自動升級操作且升級后不再做回退萝玷。升級操作將會導致重新申請內(nèi)存空間嫁乘,把原有數(shù)據(jù)按轉(zhuǎn)換類型后拷貝到新數(shù)組。
開發(fā)提示:使用intset編碼的集合時球碉,盡量保持整數(shù)范圍一致蜓斧,如都在int-16范圍內(nèi)。防止個別 大整數(shù)觸發(fā)集合升級操作睁冬,產(chǎn)生內(nèi)存浪費挎春。
下面通過測試查看ziplist編碼的集合內(nèi)存和速度表現(xiàn),如下表:
數(shù)據(jù)量 key大小 value大小 編碼 集合長度 內(nèi)存量 內(nèi)存降低比例 平均耗時 100w 20字節(jié) 7字節(jié) hashtable 1k 61.79MB --- 0.78毫秒 100w 20字節(jié) 7字節(jié) intset 1k 4.77MB 92.6% 0.51毫秒 100w 20字節(jié) 7字節(jié) ziplist 1k 8.67MB 86.2% 0.13.12毫秒 根據(jù)以上測試結(jié)果發(fā)現(xiàn)intset表現(xiàn)非常好豆拨,同樣的數(shù)據(jù)內(nèi)存占用只有不到hashtable編碼的十分之一直奋。intset數(shù)據(jù)結(jié)構(gòu)插入命令復雜度為O(n),查詢命令為O(log(n))施禾,由于整數(shù)占用空間非常小脚线,所以在集合長度可控的基礎(chǔ)上,寫入命令執(zhí)行速度也會非趁指悖快邮绿,因此當時用整數(shù)集合時盡量使用intset編碼。上表第三行把ziplist-hash類型也放入其中攀例,主要因為intset編碼必須存儲整數(shù)船逮,當集合內(nèi)保存非整數(shù)數(shù)據(jù)時,無法使用intset實現(xiàn)內(nèi)存優(yōu)化粤铭。這時可以使用ziplist-hash類型對象模擬集合類型挖胃,hash的field當做集合中的元素,value設(shè)置為1字節(jié)占位符即可梆惯。使用ziplist編碼的hash類型依然比使用hashtable編碼的集合節(jié)省大量內(nèi)存冠骄。
-
-
控制鍵的數(shù)量
當使用Redis存儲大量數(shù)據(jù)時,通常會存在大量鍵加袋,過多的鍵同樣會消耗大量內(nèi)存凛辣。Redis本質(zhì)是一個數(shù)據(jù)結(jié)構(gòu)服務器,它為我們提供多種數(shù)據(jù)結(jié)構(gòu)职烧,如hash扁誓、list、set蚀之、zset等蝗敢。使用Redis時不要進入一個誤區(qū),大量使用get/set這樣的這樣的API足删,把Redis當成Memcached使用寿谴。對于存儲相同的數(shù)據(jù)內(nèi)容利用Redis的數(shù)據(jù)結(jié)構(gòu)降低外層鍵的數(shù)量,也可以節(jié)省大量內(nèi)存失受。如下圖讶泰,通過在客戶端預估鍵規(guī)模咏瑟,把大量鍵分組映射到多個hash結(jié)構(gòu)中降低鍵的數(shù)量。
hash結(jié)構(gòu)降低鍵數(shù)量分析:
根據(jù)鍵規(guī)模在客戶端通過分組映射到一組hash對象中痪署,如存在100w個鍵码泞,可以映射到1000個hash中,每個hash保存1000個元素狼犯。
hash的field可用于記錄原始key字符串余寥,方便哈希查找。
hash的value保存原始值對象悯森,確保不要超過hash-max-ziplist-value限制宋舷。
通過這種優(yōu)化,可以得出以下結(jié)論:
同樣的數(shù)據(jù)使用ziplist編碼的hash類型存儲比string類型節(jié)約內(nèi)存瓢姻。
節(jié)省內(nèi)存量隨著value空間的減少越來越明顯祝蝠。
hash-ziplist類型比string類型寫入耗時,但隨著value空間的減少汹来,耗時逐漸降低。
使用hash重構(gòu)后節(jié)省內(nèi)存量效果非常明顯改艇,特別對于存儲小對象的場景收班,內(nèi)存只有不到原來的1/5。下面分析這種內(nèi)存優(yōu)化技巧的優(yōu)缺點:
1)hash類型節(jié)省內(nèi)存的原理是使用ziplist編碼谒兄,如果使用hashtable編碼方式反而會增加占內(nèi)存消耗摔桦。
2)ziplist長度需要控制在1000以內(nèi),否則由于存取操作時間復雜度在O(n)到O(n^2)之間承疲,長列表會導致CPU消耗嚴重邻耕,得不償失邦鲫。
3)ziplist適合存儲小對象科盛,對于大對象不但內(nèi)存優(yōu)化效果不明顯還會增加命令操作耗時。
4)需要預估鍵的規(guī)模邻薯,從而確定每個hash結(jié)構(gòu)需要存儲的元素數(shù)量啊研。
5)根據(jù)hash長度和元素大小御滩,調(diào)整hash-max-ziplist-entries和hash-max-ziplist-value參數(shù),確保hash類型使用ziplist編碼党远。
關(guān)于hash鍵和field鍵的設(shè)計:
1)當鍵離散度較高時削解,可以按字符串位截取,把后三位作為哈希的field沟娱,之前部分作為哈希的鍵氛驮。如:key=1948480哈希key=group:hash:1948,哈希field=480济似。
2)當鍵離散度較低時矫废,可以使用哈希算法打散鍵盏缤,如:使用crc32(key)&10000函數(shù)把所有的鍵映射到"0-9999"整數(shù)范圍內(nèi),哈希field存儲鍵的原始值磷脯。
3)盡量減少hash鍵和field的長度蛾找。如使用部分鍵內(nèi)容。
使用hash結(jié)構(gòu)控制鍵的規(guī)模雖然可以大幅降低內(nèi)存赵誓,但同樣會帶來問題打毛,需要提前做好規(guī)避處理,如下所示:
客戶端需要預估鍵的規(guī)模并設(shè)計hash分組規(guī)則俩功,加重客戶端開發(fā)成本幻枉。
hash重構(gòu)后所有的鍵無法再使用超時(expire)和LRU淘汰機制自動刪除,需要手動維護刪除诡蜓。
對于大對象熬甫,如1KB以上的對象,使用hash-ziplist結(jié)構(gòu)控制鍵數(shù)量反而得不償失蔓罚。
不過瑕不掩瑜椿肩,對于大量小對象的存儲場景,非常適合使用ziplist編碼的hash類型控制鍵的規(guī)模來降低內(nèi)存豺谈。
開發(fā)提示:使用ziplist+hash優(yōu)化keys后郑象,如果想使用超時刪除功能,開發(fā)人員可以存儲每個對象寫入的時間茬末,在通過定時任務使用hscan命令掃描數(shù)據(jù)厂榛,找出hash內(nèi)超時的數(shù)據(jù)項刪除即可。
本節(jié)主要講解Redis內(nèi)存優(yōu)化技巧丽惭,Redis的數(shù)據(jù)特性是“all in memory”击奶,優(yōu)化內(nèi)存將變得非常重要。對于內(nèi)存優(yōu)化建議讀者先要掌握Redis內(nèi)存存儲的特性比如字符串责掏、壓縮編碼柜砾、整數(shù)解和等,再根據(jù)數(shù)據(jù)規(guī)模和所用命令需求去調(diào)整换衬,從而達到空間和效率的最佳平衡局义。建議使用Redis存儲大量數(shù)據(jù)時,把內(nèi)存優(yōu)化環(huán)節(jié)加入到前期設(shè)計階段冗疮,否則數(shù)據(jù)大幅增長后萄唇,開發(fā)人員需要面重新優(yōu)化內(nèi)存所帶來開發(fā)和數(shù)據(jù)遷移的雙重成本。當Redis內(nèi)存不足時术幔,首先考慮的問題不是加機器做水平擴展另萤,應該先嘗試做內(nèi)存優(yōu)化,當遇到瓶頸時,再去考慮水平擴展四敞。即使對于集群化方案泛源,垂直層面優(yōu)化也同樣重要,避免不必要的資源浪費和集群化后的管理成本忿危。