聲明:本文內(nèi)容來自《Redis開發(fā)與運維》一書第八章绸栅,如轉(zhuǎn)載請聲明螺男。
Redis所有的數(shù)據(jù)都在內(nèi)存中洛勉,而內(nèi)存又是非常寶貴的資源粘秆。對于如何優(yōu)化內(nèi)存使用一直是Redis用戶非常關(guān)注的問題。本文讓我們深入到Redis細(xì)節(jié)中收毫,學(xué)習(xí)內(nèi)存優(yōu)化的技巧攻走。分為如下幾個部分:
二.縮減鍵值對象
三.共享對象池
Redis存儲的所有值對象在內(nèi)部定義為redisObject結(jié)構(gòu)體,內(nèi)部結(jié)構(gòu)如下圖所示此再。
Redis存儲的數(shù)據(jù)都使用redisObject來封裝昔搂,包括string,hash,list,set,zset在內(nèi)的所有數(shù)據(jù)類型。理解redisObject對內(nèi)存優(yōu)化非常有幫助输拇,下面針對每個字段做詳細(xì)說明:
表示當(dāng)前對象使用的數(shù)據(jù)類型巩趁,Redis主要支持5種數(shù)據(jù)類型:string,hash,list,set,zset。可以使用type {key}命令查看對象所屬類型议慰,type命令返回的是值對象類型蠢古,鍵都是string類型。
表示Redis內(nèi)部編碼類型别凹,encoding在Redis內(nèi)部使用草讶,代表當(dāng)前對象內(nèi)部采用哪種數(shù)據(jù)結(jié)構(gòu)實現(xiàn)。理解Redis內(nèi)部編碼方式對于優(yōu)化內(nèi)存非常重要 炉菲,同一個對象采用不同的編碼實現(xiàn)內(nèi)存占用存在明顯差異堕战,具體細(xì)節(jié)見之后編碼優(yōu)化部分。
記錄對象最后一次被訪問的時間拍霜,當(dāng)配置了 maxmemory和maxmemory-policy=volatile-lru | allkeys-lru 時嘱丢, 用于輔助LRU算法刪除鍵數(shù)據(jù)§艚龋可以使用object idletime {key}命令在不更新lru字段情況下查看當(dāng)前鍵的空閑時間越驻。
1
開發(fā)提示:可以使用scan + object idletime? 命令批量查詢哪些鍵長時間未被訪問,找出長時間不訪問的鍵進(jìn)行清理降低內(nèi)存占用道偷。
記錄當(dāng)前對象被引用的次數(shù)缀旁,用于通過引用次數(shù)回收內(nèi)存,當(dāng)refcount=0時勺鸦,可以安全回收當(dāng)前對象空間并巍。使用object refcount {key}獲取當(dāng)前對象引用。當(dāng)對象為整數(shù)且范圍在[0-9999]時换途,Redis可以使用共享對象的方式來節(jié)省內(nèi)存懊渡。具體細(xì)節(jié)見之后共享對象池部分。
與對象的數(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)存操作漓拾。
1
開發(fā)提示:高并發(fā)寫入場景中阁最,在條件允許的情況下建議字符串長度控制在39字節(jié)以內(nèi),減少創(chuàng)建redisObject內(nèi)存分配次數(shù)從而提高性能骇两。
降低Redis內(nèi)存使用最直接的方式就是縮減鍵(key)和值(value)的長度速种。
key長度:如在設(shè)計鍵時,在完整描述業(yè)務(wù)情況下低千,鍵值越短越好配阵。
value長度:值對象縮減比較復(fù)雜馏颂,常見需求是把業(yè)務(wù)對象序列化成二進(jìn)制數(shù)組放入Redis。首先應(yīng)該在業(yè)務(wù)上精簡業(yè)務(wù)對象棋傍,去掉不必要的屬性避免存儲無效數(shù)據(jù)救拉。其次在序列化工具選擇上,應(yīng)該選擇更高效的序列化工具來降低字節(jié)數(shù)組大小瘫拣。以JAVA為例亿絮,內(nèi)置的序列化方式無論從速度還是壓縮比都不盡如人意,這時可以選擇更高效的序列化工具麸拄,如: protostuff派昧,kryo等,下圖是JAVA常見序列化工具空間壓縮對比拢切。
其中java-built-in-serializer表示JAVA內(nèi)置序列化方式蒂萎,更多數(shù)據(jù)見jvm-serializers項目:https://github.com/eishay/jvm-serializers/wiki,其它語言也有各自對應(yīng)的高效序列化工具淮椰。
值對象除了存儲二進(jìn)制數(shù)據(jù)之外五慈,通常還會使用通用格式存儲數(shù)據(jù)比如:json,xml等作為字符串存儲在Redis中实苞。這種方式優(yōu)點是方便調(diào)試和跨語言豺撑,但是同樣的數(shù)據(jù)相比字節(jié)數(shù)組所需的空間更大烈疚,在內(nèi)存緊張的情況下黔牵,可以使用通用壓縮算法壓縮json,xml后再存入Redis,從而降低內(nèi)存占用爷肝,例如使用GZIP壓縮后的json可降低約60%的空間猾浦。
1
開發(fā)提示:當(dāng)頻繁壓縮解壓json等文本數(shù)據(jù)時,開發(fā)人員需要考慮壓縮速度和計算開銷成本灯抛,這里推薦使用google的Snappy壓縮工具金赦,在特定的壓縮率情況下效率遠(yuǎn)遠(yuǎn)高于GZIP等傳統(tǒng)壓縮工具,且支持所有主流語言環(huán)境对嚼。
對象共享池指Redis內(nèi)部維護(hù)[0-9999]的整數(shù)對象池夹抗。創(chuàng)建大量的整數(shù)類型redisObject存在內(nèi)存開銷,每個redisObject內(nèi)部結(jié)構(gòu)至少占16字節(jié)纵竖,甚至超過了整數(shù)自身空間消耗漠烧。所以Redis內(nèi)存維護(hù)一個[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)?厕鹃,如下圖所示兢仰。
使用整數(shù)對象池究竟能降低多少內(nèi)存?讓我們通過測試來對比對象池的內(nèi)存優(yōu)化效果剂碴,如下表所示把将。
操作說明是否對象共享key大小value大小used_memused_memory_rss
插入200萬否20字節(jié)[0-9999]整數(shù)199.91MB205.28MB
插入200萬是20字節(jié)[0-9999]整數(shù)138.87MB143.28MB
注意本文所有測試環(huán)境都保持一致,信息如下:
服務(wù)器信息: 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ù)內(nèi)存使用降低30%以上察蹲。可見當(dāng)數(shù)據(jù)大量使用[0-9999]的整數(shù)時催训,共享對象池可以節(jié)約大量內(nèi)存洽议。需要注意的是對象池并不是只要存儲[0-9999]的整數(shù)就可以工作。當(dāng)設(shè)置maxmemory并啟用LRU相關(guān)淘汰策略如:volatile-lru漫拭,allkeys-lru時亚兄,Redis禁止使用共享對象池,測試命令如下:
redis> set key:1 99
OK //設(shè)置key:1=99
redis> object refcount key:1
(integer) 2 //使用了對象共享,引用數(shù)為2
redis> config set maxmemory-policy volatile-lru
OK //開啟LRU淘汰策略
redis> set key:2 99
OK //設(shè)置key:2=99
redis> object refcount key:2
(integer) 3 //使用了對象共享,引用數(shù)變?yōu)?
redis> config set maxmemory 1GB
OK //設(shè)置最大可用內(nèi)存
redis> set key:3 99
OK //設(shè)置key:3=99
redis> object refcount key:3
(integer) 1 //未使用對象共享,引用數(shù)為1
redis> config set maxmemory-policy volatile-ttl
OK //設(shè)置非LRU淘汰策略
redis> set key:4 99
OK //設(shè)置key:4=99
redis> object refcount key:4
(integer) 4 //又可以使用對象共享,引用數(shù)變?yōu)?
為什么開啟maxmemory和LRU淘汰策略后對象池?zé)o效?
LRU算法需要獲取對象最后被訪問時間采驻,以便淘汰最長未訪問數(shù)據(jù)审胚,每個對象最后訪問時間存儲在redisObject對象的lru字段。對象共享意味著多個引用共享同一個redisObject礼旅,這時lru字段也會被共享膳叨,導(dǎo)致無法獲取每個對象的最后訪問時間。如果沒有設(shè)置maxmemory痘系,直到內(nèi)存被用盡Redis也不會觸發(fā)內(nèi)存回收菲嘴,所以共享對象池可以正常工作。
綜上所述汰翠,共享對象池與maxmemory+LRU策略沖突龄坪,使用時需要注意。 對于ziplist編碼的值對象奴璃,即使內(nèi)部數(shù)據(jù)為整數(shù)也無法使用共享對象池悉默,因為ziplist使用壓縮且內(nèi)存連續(xù)的結(jié)構(gòu),對象共享判斷成本過高苟穆,ziplist編碼細(xì)節(jié)后面內(nèi)容詳細(xì)說明抄课。
首先整數(shù)對象池復(fù)用的幾率最大,其次對象共享的一個關(guān)鍵操作就是判斷相等性跟磨,Redis之所以只有整數(shù)對象池间聊,是因為整數(shù)比較算法時間復(fù)雜度為O(1),只保留一萬個整數(shù)為了防止對象池浪費抵拘。如果是字符串判斷相等性哎榴,時間復(fù)雜度變?yōu)镺(n),特別是長字符串更消耗性能(浮點數(shù)在Redis內(nèi)部使用字符串存儲)僵蛛。對于更復(fù)雜的數(shù)據(jù)結(jié)構(gòu)如hash,list等尚蝌,相等性判斷需要O(n2)。對于單線程的Redis來說充尉,這樣的開銷顯然不合理飘言,因此Redis只保留整數(shù)共享對象池。
字符串對象是Redis內(nèi)部最常用的數(shù)據(jù)類型驼侠。所有的鍵都是字符串類型姿鸿, 值對象數(shù)據(jù)除了整數(shù)之外都使用字符串存儲。比如執(zhí)行命令:lpush cache:type “redis” “memcache” “tair” “l(fā)evelDB” 倒源,Redis首先創(chuàng)建”cache:type”鍵字符串苛预,然后創(chuàng)建鏈表對象,鏈表對象內(nèi)再包含四個字符串對象笋熬,排除Redis內(nèi)部用到的字符串對象之外至少創(chuàng)建5個字符串對象热某。可見字符串對象在Redis內(nèi)部使用非常廣泛突诬,因此深刻理解Redis字符串對于內(nèi)存優(yōu)化非常有幫助:
Redis沒有采用原生C語言的字符串類型而是自己實現(xiàn)了字符串結(jié)構(gòu)苫拍,內(nèi)部簡單動態(tài)字符串(simple dynamic string)芜繁,簡稱SDS。結(jié)構(gòu)下圖所示。
Redis自身實現(xiàn)的字符串結(jié)構(gòu)有如下特點:
O(1)時間復(fù)雜度獲榷愕稹:字符串長度厉亏,已用長度,未用長度榔袋。
可用于保存字節(jié)數(shù)組周拐,支持安全的二進(jìn)制數(shù)據(jù)存儲。
內(nèi)部實現(xiàn)空間預(yù)分配機制凰兑,降低內(nèi)存再分配次數(shù)妥粟。
惰性刪除機制,字符串縮減后的空間不釋放吏够,作為預(yù)分配空間保留勾给。
因為字符串(SDS)存在預(yù)分配機制滩报,日常開發(fā)中要小心預(yù)分配帶來的內(nèi)存浪費,例如下表的測試用例播急。
階段數(shù)據(jù)量操作說明命令key大小value大小used_memused_memory_rssmem_fragmentation_ratio
階段1200w新插入200w數(shù)據(jù)set20字節(jié)60字節(jié)321.98MB331.44MB1.02
階段2200w在階段1上每個對象追加60字節(jié)數(shù)據(jù)append20字節(jié)60字節(jié)657.67MB752.80MB1.14
階段3200w重新插入200w數(shù)據(jù)set20字節(jié)120字節(jié)474.56MB482.45MB1.02
從測試數(shù)據(jù)可以看出脓钾,同樣的數(shù)據(jù)追加后內(nèi)存消耗非常嚴(yán)重,下面我們結(jié)合圖來分析這一現(xiàn)象桩警。階段1每個字符串對象空間占用如下圖所示可训。
階段1插入新的字符串后,free字段保留空間為0捶枢,總占用空間=實際占用空間+1字節(jié)握截,最后1字節(jié)保存‘\0’標(biāo)示結(jié)尾,這里忽略int類型len和free字段消耗的8字節(jié)烂叔。在階段1原有字符串上追加60字節(jié)數(shù)據(jù)空間占用如下圖所示川蒙。
追加操作后字符串對象預(yù)分配了一倍容量作為預(yù)留空間,而且大量追加操作需要內(nèi)存重新分配长已,造成內(nèi)存碎片率(mem_fragmentation_ratio)上升畜眨。直接插入與階段2相同數(shù)據(jù)的空間占用,如下圖所示术瓮。
階段3直接插入同等數(shù)據(jù)后康聂,相比階段2節(jié)省了每個字符串對象預(yù)分配的空間,同時降低了碎片率胞四。
字符串之所以采用預(yù)分配的方式是防止修改操作需要不斷重分配內(nèi)存和字節(jié)數(shù)據(jù)拷貝恬汁。但同樣也會造成內(nèi)存的浪費。字符串預(yù)分配每次并不都是翻倍擴容辜伟,空間預(yù)分配規(guī)則如下:
1) 第一次創(chuàng)建len屬性等于數(shù)據(jù)實際大小氓侧,free等于0,不做預(yù)分配导狡。
2) 修改后如果已有free空間不夠且數(shù)據(jù)小于1M约巷,每次預(yù)分配一倍容量。如原有l(wèi)en=60byte旱捧,free=0独郎,再追加60byte,預(yù)分配120byte枚赡,總占用空間:60byte+60byte+120byte+1byte氓癌。
3) 修改后如果已有free空間不夠且數(shù)據(jù)大于1MB,每次預(yù)分配1MB數(shù)據(jù)贫橙。如原有l(wèi)en=30MB贪婉,free=0,當(dāng)再追加100byte ,預(yù)分配1MB卢肃,總占用空間:1MB+100byte+1MB+1byte疲迂。
開發(fā)提示:盡量減少字符串頻繁修改操作如append星压,setrange, 改為直接使用set修改字符串,降低預(yù)分配帶來的內(nèi)存浪費和內(nèi)存碎片化鬼譬。
字符串重構(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
200W20字節(jié)stringjson字符串默認(rèn)612.62M
200W20字節(jié)hashkey-value對默認(rèn)默認(rèn) 1.88GB
200W20字節(jié)hashkey-value對hash-max-ziplist-value:66535.60M
根據(jù)測試結(jié)構(gòu),第一次默認(rèn)配置下使用hash類型避乏,內(nèi)存消耗不但沒有降低反而比字符串存儲多出2倍爷耀,而調(diào)整hash-max-ziplist-value=66之后內(nèi)存降低為535.60M。因為json的videoAlbumPic屬性長度是65拍皮,而hash-max-ziplist-value默認(rèn)值是64歹叮,Redis采用hashtable編碼方式,反而消耗了大量內(nèi)存铆帽。調(diào)整配置后hash類型內(nèi)部編碼方式變?yōu)閦iplist咆耿,相比字符串更省內(nèi)存且支持屬性的部分操作。下一節(jié)將具體介紹ziplist編碼優(yōu)化細(xì)節(jié)爹橱。
Redis對外提供了string,list,hash,set,zet等類型萨螺,但是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的對應(yīng)關(guān)系吻商。
表:type和encoding對應(yīng)關(guān)系表
類型編碼方式數(shù)據(jù)結(jié)構(gòu)
stringraw
embstr
int動態(tài)字符串編碼
優(yōu)化內(nèi)存分配的字符串編碼
整數(shù)編碼
hashhashtable
ziplist散列表編碼
壓縮列表編碼
listlinkedlist
ziplist
quicklist
雙向鏈表編碼
壓縮列表編碼
3.2版本新的列表編碼
sethashtable
intset散列表編碼
整數(shù)集合編碼
zsetskiplist
ziplist跳躍表編碼
壓縮列表編碼
了解編碼和類型對應(yīng)關(guān)系之后,我們不禁疑惑Redis為什么需要對一種數(shù)據(jù)結(jié)構(gòu)實現(xiàn)多種編碼方式惫确?
主要原因是Redis作者想通過不同編碼實現(xiàn)效率和空間的平衡手报。比如當(dāng)我們的存儲只有10個元素的列表蚯舱,當(dāng)使用雙向鏈表數(shù)據(jù)結(jié)構(gòu)時改化,必然需要維護(hù)大量的內(nèi)部字段如每個元素需要:前置指針,后置指針枉昏,數(shù)據(jù)指針等陈肛,造成空間浪費,如果采用連續(xù)內(nèi)存結(jié)構(gòu)的壓縮列表(ziplist)兄裂,將會節(jié)省大量內(nèi)存句旱,而由于數(shù)據(jù)長度較小阳藻,存取操作時間復(fù)雜度即使為O(n2)性能也可滿足需求。
Redis內(nèi)存優(yōu)化
編碼類型轉(zhuǎn)換在Redis寫入數(shù)據(jù)時自動完成谈撒,這個轉(zhuǎn)換過程是不可逆的腥泥,轉(zhuǎn)換規(guī)則只能從小內(nèi)存編碼向大內(nèi)存編碼轉(zhuǎn)換。例如:
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 //設(shè)置列表類型ziplist編碼最大允許4個元素
redis> lpush list:1 e
(integer) 5 //寫入第5個元素e
redis> object encoding list:1
"linkedlist" //編碼類型轉(zhuǎn)換為鏈表
redis> rpop list:1
"a" //彈出元素a
redis> llen list:1
(integer) 4 // 列表此時有4個元素
redis> object encoding list:1
"linkedlist" //編碼類型依然為鏈表啃匿,未做編碼回退
以上命令體現(xiàn)了list類型編碼的轉(zhuǎn)換過程蛔外,其中Redis之所以不支持編碼回退,主要是數(shù)據(jù)增刪頻繁時溯乒,數(shù)據(jù)向壓縮編碼轉(zhuǎn)換非常消耗CPU夹厌,得不償失。以上示例用到了list-max-ziplist-entries參數(shù)裆悄,這個參數(shù)用來決定列表長度在多少范圍內(nèi)使用ziplist編碼矛纹。當(dāng)然還有其它參數(shù)控制各種數(shù)據(jù)類型的編碼,如下表所示:
表:hash,list,set,zset內(nèi)部編碼配置
類型編碼決定條件
hashziplist滿足所有條件:
value最大空間(字節(jié))<=hash-max-ziplist-value
field個數(shù)<=hash-max-ziplist-entries
同上hashtable滿足任意條件:
value最大空間(字節(jié))>hash-max-ziplist-value
field個數(shù)>hash-max-ziplist-entries
listziplistziplist 滿足所有條件:
value最大空間(字節(jié))<=list-max-ziplist-value
鏈表長度<=list-max-ziplist-entries
同上linkedlist滿足任意條件
value最大空間(字節(jié))>list-max-ziplist-value
鏈表長度>list-max-ziplist-entries
同上quicklist3.2版本新編碼:
廢棄list-max-ziplist-entries和list-max-ziplist-entries配置
使用新配置:
list-max-ziplist-size:表示最大壓縮空間或長度,最大空間使用[-5-1]范圍配置光稼,默認(rèn)-2表示8KB,正整數(shù)表示最大壓縮長度
list-compress-depth:表示最大壓縮深度或南,默認(rèn)=0不壓縮
setintset滿足所有條件:
元素必須為整數(shù)
集合長度<=set-max-intset-entries
同上hashtable滿足任意條件
元素非整數(shù)類型
集合長度>hash-max-ziplist-entries
zsetziplist滿足所有條件:
value最大空間(字節(jié))<=zset-max-ziplist-value
有序集合長度<=zset-max-ziplist-entries
同上skiplist滿足任意條件:
value最大空間(字節(jié))>zset-max-ziplist-value
有序集合長度>zset-max-ziplist-entries
掌握編碼轉(zhuǎn)換機制,對我們通過編碼來優(yōu)化內(nèi)存使用非常有幫助艾君。下面以hash類型為例迎献,介紹編碼轉(zhuǎn)換的運行流程,如下圖所示腻贰。
理解編碼轉(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編碼主要目的是為了節(jié)約內(nèi)存翼闽,因此所有數(shù)據(jù)都是采用線性連續(xù)的內(nèi)存結(jié)構(gòu)。ziplist編碼是應(yīng)用范圍最廣的一種洲炊,可以分別作為hash感局、list、zset類型的底層數(shù)據(jù)結(jié)構(gòu)實現(xiàn)暂衡。首先從ziplist編碼結(jié)構(gòu)開始分析询微,它的內(nèi)部結(jié)構(gòu)類似這樣:<….>。一個ziplist可以包含多個entry(元素)狂巢,每個entry保存具體的數(shù)據(jù)(整數(shù)或者字節(jié)數(shù)組)撑毛,內(nèi)部結(jié)構(gòu)如下圖所示。
ziplist結(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ù)量,當(dāng)長度超過216-2時需要遍歷整個列表獲取長度做个,一般很少見艇肴。類型是int-16,長度為2字節(jié)
4) entry:記錄具體的節(jié)點叁温,長度根據(jù)實際存儲的數(shù)據(jù)而定再悼。
a) prev_entry_bytes_length:記錄前一個節(jié)點所占空間,用于快速定位上一個節(jié)點膝但,可實現(xiàn)列表反向迭代冲九。
b) encoding:標(biāo)示當(dāng)前節(jié)點編碼和長度,前兩位表示編碼類型:字符串/整數(shù)跟束,其余位表示數(shù)據(jù)長度莺奸。
c) contents:保存節(jié)點的值,針對實際數(shù)據(jù)長度做內(nèi)存占用優(yōu)化冀宴。
5) zlend:記錄列表結(jié)尾灭贷,占用一個字節(jié)。
根據(jù)以上對ziplist字段說明略贮,可以分析出該數(shù)據(jù)結(jié)構(gòu)特點如下:
1) 內(nèi)部表現(xiàn)為數(shù)據(jù)緊湊排列的一塊連續(xù)內(nèi)存數(shù)組甚疟。
2) 可以模擬雙向鏈表結(jié)構(gòu),以O(shè)(1)時間復(fù)雜度入隊和出隊逃延。
3) 新增刪除操作涉及內(nèi)存重新分配或釋放览妖,加大了操作的復(fù)雜性。
4) 讀寫操作涉及復(fù)雜的指針移動揽祥,最壞時間復(fù)雜度為O(n2)讽膏。
5) 適合存儲小對象和長度有限的數(shù)據(jù)。
下面通過測試展示ziplist編碼在不同類型中內(nèi)存和速度的表現(xiàn)拄丰,如下表所示府树。
表:ziplist在hash,list,zset內(nèi)存和速度測試
類型數(shù)據(jù)量key總數(shù)量長度value大小普通編碼內(nèi)存量/平均耗時壓縮編碼內(nèi)存量/平均耗時內(nèi)存降低比例耗時增長倍數(shù)
hash100萬1千1千36字節(jié)103.37M/0.84微秒43.83M/13.24微秒57.5%15倍
list100萬1千1千36字節(jié)92.46M/2.04微秒39.92M/5.45微秒56.8%2.5倍
zset100萬1千1千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提供了{(lán)type}-max-ziplist-value和{type}-max-ziplist-entries相關(guān)參數(shù)來做控制ziplist編碼轉(zhuǎn)換。最后再次強調(diào)使用ziplist壓縮編碼的原則:追求空間和時間的平衡馁筐。
開發(fā)提示:
1)針對性能要求較高的場景使用ziplist涂召,建議長度不要超過1000,每個元素大小控制在512字節(jié)以內(nèi)敏沉。
2)命令平均耗時使用info Commandstats命令獲取果正,包含每個命令調(diào)用次數(shù),總耗時盟迟,平均耗時秋泳,單位微秒。
intset編碼是集合(set)類型編碼的一種攒菠,內(nèi)部表現(xiàn)為存儲有序迫皱,不重復(fù)的整數(shù)集。當(dāng)集合只包含整數(shù)且長度不超過set-max-intset-entries配置時被啟用辖众。執(zhí)行以下命令查看intset表現(xiàn):
127.0.0.1:6379> sadd set:test 3 4 2 6 8 9 2
(integer) 6 //亂序?qū)懭?個整數(shù)
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" // 排序輸出整數(shù)結(jié)合
redis> config set set-max-intset-entries 6
OK //設(shè)置intset最大允許整數(shù)長度
redis> sadd set:test 5
(integer) 1 //寫入第7個整數(shù) 5
redis> object encoding set:test
"hashtable" // 編碼變?yōu)閔ashtable
redis> smembers set:test
"8" "3" "5" "9" "4" "2" "6" //亂序輸出
以上命令可以看出intset對寫入整數(shù)進(jìn)行排序卓起,通過O(log(n))時間復(fù)雜度實現(xiàn)查找和去重操作,intset編碼結(jié)構(gòu)如下圖所示凹炸。
intset的字段結(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ù)長度劃分锣光,當(dāng)保存的整數(shù)超出當(dāng)前類型時笆怠,將會觸發(fā)自動升級操作且升級后不再做回退。升級操作將會導(dǎo)致重新申請內(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)迂卢,如下表所示。
表:ziplist編碼在set下內(nèi)存和速度表現(xiàn)
數(shù)據(jù)量key大小value大小編碼集合長度內(nèi)存量內(nèi)存降低比例平均耗時
100w20byte7字節(jié)hashtable1千61.97MB–0.78毫秒
100w20byte7字節(jié)intset1千4.77MB92.6%0.51毫秒
100w20byte7字節(jié)ziplist1千8.67MB86.2%13.12毫秒
根據(jù)以上測試結(jié)果發(fā)現(xiàn)intset表現(xiàn)非常好,同樣的數(shù)據(jù)內(nèi)存占用只有不到hashtable編碼的十分之一而克。intset數(shù)據(jù)結(jié)構(gòu)插入命令復(fù)雜度為O(n)靶壮,查詢命令為O(log(n)),由于整數(shù)占用空間非常小员萍,所以在集合長度可控的基礎(chǔ)上腾降,寫入命令執(zhí)行速度也會非常快碎绎,因此當(dāng)使用整數(shù)集合時盡量使用intset編碼螃壤。上表測試第三行把ziplist-hash類型也放入其中,主要因為intset編碼必須存儲整數(shù)筋帖,當(dāng)集合內(nèi)保存非整數(shù)數(shù)據(jù)時奸晴,無法使用intset實現(xiàn)內(nèi)存優(yōu)化。這時可以使用ziplist-hash類型對象模擬集合類型日麸,hash的field當(dāng)作集合中的元素寄啼,value設(shè)置為1字節(jié)占位符即可。使用ziplist編碼的hash類型依然比使用hashtable編碼的集合節(jié)省大量內(nèi)存赘淮。
當(dāng)使用Redis存儲大量數(shù)據(jù)時辕录,通常會存在大量鍵,過多的鍵同樣會消耗大量內(nèi)存梢卸。Redis本質(zhì)是一個數(shù)據(jù)結(jié)構(gòu)服務(wù)器走诞,它為我們提供多種數(shù)據(jù)結(jié)構(gòu),如hash蛤高,list蚣旱,set,zset 等結(jié)構(gòu)戴陡。使用Redis時不要進(jìn)入一個誤區(qū)塞绿,大量使用get/set這樣的API,把Redis當(dāng)成Memcached使用恤批。對于存儲相同的數(shù)據(jù)內(nèi)容利用Redis的數(shù)據(jù)結(jié)構(gòu)降低外層鍵的數(shù)量异吻,也可以節(jié)省大量內(nèi)存。如下圖所示喜庞,通過在客戶端預(yù)估鍵規(guī)模诀浪,把大量鍵分組映射到多個hash結(jié)構(gòu)中降低鍵的數(shù)量。
hash結(jié)構(gòu)降低鍵數(shù)量分析:
根據(jù)鍵規(guī)模在客戶端通過分組映射到一組hash對象中延都,如存在100萬個鍵雷猪,可以映射到1000個hash中,每個hash保存1000個元素晰房。
hash的field可用于記錄原始key字符串求摇,方便哈希查找射沟。
hash的value保存原始值對象,確保不要超過hash-max-ziplist-value限制与境。
下面測試這種優(yōu)化技巧的內(nèi)存表現(xiàn)验夯,如下表所示。
數(shù)據(jù)量key大小value大小string類型占用內(nèi)存hash-ziplist類型占用內(nèi)存內(nèi)存降低比例string:set平均耗時hash:hset平均耗時
200w20byte512byte1392.64MB1000.97MB28.1%2.13微秒21.28微秒
200w20byte200byte596.62MB399.38MB33.1%1.49微秒16.08微秒
200w20byte100byte382.99MB211.88MB44.6%1.30微秒14.92微秒
200w20byte50byte291.46MB110.32MB62.1%1.28微秒13.48微秒
200w20byte20byte246.40MB55.63MB77.4%1.10微秒13.21微秒
200w20byte5byte199.93MB24.42MB87.7%1.10微秒13.06微秒
通過這個測試數(shù)據(jù)嚷辅,可以說明:
同樣的數(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)化技巧的關(guān)鍵點:
1) hash類型節(jié)省內(nèi)存的原理是使用ziplist編碼寺擂,如果使用hashtable編碼方式反而會增加內(nèi)存消耗。
2) ziplist長度需要控制在1000以內(nèi)泼掠,否則由于存取操作時間復(fù)雜度在O(n)到O(n2)之間怔软,長列表會導(dǎo)致CPU消耗嚴(yán)重,得不償失择镇。
3) ziplist適合存儲的小對象挡逼,對于大對象不但內(nèi)存優(yōu)化效果不明顯還會增加命令操作耗時。
4) 需要預(yù)估鍵的規(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) 當(dāng)鍵離散度較高時,可以按字符串位截取苏携,把后三位作為哈希的field做瞪,之前部分作為哈希的鍵。如:key=1948480 哈希key=group:hash:1948右冻,哈希field=480装蓬。
2) 當(dāng)鍵離散度較低時,可以使用哈希算法打散鍵国旷,如:使用crc32(key)&10000函數(shù)把所有的鍵映射到“0-9999”整數(shù)范圍內(nèi)矛物,哈希field存儲鍵的原始值。
3) 盡量減少hash鍵和field的長度跪但,如使用部分鍵內(nèi)容履羞。
使用hash結(jié)構(gòu)控制鍵的規(guī)模雖然可以大幅降低內(nèi)存峦萎,但同樣會帶來問題,需要提前做好規(guī)避處理忆首。如下:
客戶端需要預(yù)估鍵的規(guī)模并設(shè)計hash分組規(guī)則爱榔,加重客戶端開發(fā)成本。
hash重構(gòu)后所有的鍵無法再使用超時(expire)和LRU淘汰機制自動刪除糙及,需要手動維護(hù)刪除详幽。
對于大對象,如1KB以上的對象浸锨。使用hash-ziplist結(jié)構(gòu)控制鍵數(shù)量唇聘。
不過瑕不掩瑜,對于大量小對象的存儲場景柱搜,非常適合使用ziplist編碼的hash類型控制鍵的規(guī)模來降低內(nèi)存迟郎。
開發(fā)提示:使用ziplist+hash優(yōu)化keys后,如果想使用超時刪除功能聪蘸,開發(fā)人員可以存儲每個對象寫入的時間宪肖,再通過定時任務(wù)使用hscan命令掃描數(shù)據(jù),找出hash內(nèi)超時的數(shù)據(jù)項刪除即可健爬。
本文主要講解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)整衷咽,從而達(dá)到空間和效率的最佳平衡。建議使用Redis存儲大量數(shù)據(jù)時蒜绽,把內(nèi)存優(yōu)化環(huán)節(jié)加入到前期設(shè)計階段镶骗,否則數(shù)據(jù)大幅增長后,開發(fā)人員需要面對重新優(yōu)化內(nèi)存所帶來開發(fā)和數(shù)據(jù)遷移的雙重成本躲雅。當(dāng)Redis內(nèi)存不足時鼎姊,首先考慮的問題不是加機器做水平擴展,應(yīng)該先嘗試做內(nèi)存優(yōu)化相赁。當(dāng)遇到瓶頸時相寇,再去考慮水平擴展。即使對于集群化方案钮科,垂直層面優(yōu)化也同樣重要唤衫,避免不必要的資源浪費和集群化后的管理成本。