Redis的內(nèi)存優(yōu)化

Redis的內(nèi)存優(yōu)化

聲明:本文內(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)化的技巧攻走。分為如下幾個部分:

一.redisObject對象

二.縮減鍵值對象

三.共享對象池

四.字符串優(yōu)化

五.編碼優(yōu)化

六.控制key的數(shù)量

一. redisObject對象

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ì)說明:

1.type字段:

表示當(dāng)前對象使用的數(shù)據(jù)類型巩趁,Redis主要支持5種數(shù)據(jù)類型:string,hash,list,set,zset。可以使用type {key}命令查看對象所屬類型议慰,type命令返回的是值對象類型蠢古,鍵都是string類型。

2.encoding字段:

表示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)化部分。

3.lru字段:

記錄對象最后一次被訪問的時間拍霜,當(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)存占用道偷。

4.refcount字段:

記錄當(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é)見之后共享對象池部分。

5. *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)存操作漓拾。

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ù)對象池唱星?

首先整數(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ù)共享對象池。

四. 字符串優(yōu)化

字符串對象是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)化非常有幫助:

1.字符串結(jié)構(gò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ù)分配空間保留勾给。

2.預(yù)分配機制

因為字符串(SDS)存在預(yù)分配機制滩报,日常開發(fā)中要小心預(yù)分配帶來的內(nèi)存浪費,例如下表的測試用例播急。

表:字符串內(nèi)存預(yù)分配測試

階段數(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)存碎片化鬼譬。

3.字符串重構(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)巩螃,如下表所示演怎。

表:測試內(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é)爹橱。

五. 編碼優(yōu)化

1.了解編碼

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)化

2.控制編碼類型

編碼類型轉(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)換写烤。

3.ziplist編碼

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ù),總耗時盟迟,平均耗時秋泳,單位微秒。

4.intset編碼

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)存赘淮。

六 控制key的數(shù)量

當(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)验夯,如下表所示。

表:hash分組控制鍵規(guī)模測試

數(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)化也同樣重要唤衫,避免不必要的資源浪費和集群化后的管理成本。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绵脯,一起剝皮案震驚了整個濱河市佳励,隨后出現(xiàn)的幾起案子休里,更是在濱河造成了極大的恐慌,老刑警劉巖赃承,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妙黍,死亡現(xiàn)場離奇詭異,居然都是意外死亡瞧剖,警方通過查閱死者的電腦和手機拭嫁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抓于,“玉大人做粤,你說我怎么就攤上這事≌庇剑” “怎么了驮宴?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵逮刨,是天一觀的道長呕缭。 經(jīng)常有香客問我,道長修己,這世上最難降的妖魔是什么恢总? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮睬愤,結(jié)果婚禮上片仿,老公的妹妹穿的比我還像新娘。我一直安慰自己尤辱,他們只是感情好砂豌,可當(dāng)我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著光督,像睡著了一般阳距。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上结借,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天筐摘,我揣著相機與錄音,去河邊找鬼船老。 笑死咖熟,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的柳畔。 我是一名探鬼主播馍管,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼薪韩!你這毒婦竟也來了确沸?” 一聲冷哼從身側(cè)響起堪置,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎张惹,沒想到半個月后舀锨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡宛逗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年坎匿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雷激。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡替蔬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屎暇,到底是詐尸還是另有隱情承桥,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布根悼,位于F島的核電站凶异,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏挤巡。R本人自食惡果不足惜剩彬,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望矿卑。 院中可真熱鬧喉恋,春花似錦、人聲如沸母廷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽琴昆。三九已至氓鄙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間椎咧,已是汗流浹背玖详。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留勤讽,地道東北人蟋座。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像脚牍,于是被迫代替她去往敵國和親向臀。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容