小的聚合類型數(shù)據(jù)的特殊編碼處理
redis2.2版本及以后,存儲集合數(shù)據(jù)的時候會采用內(nèi)存壓縮技術(shù),以使用更少的內(nèi)存存儲更多的數(shù)據(jù)。如Hashes,Lists,Sets和Sorted Sets,當(dāng)這些集合中的所有數(shù)都小于一個給定的元素,并且集合中元素數(shù)量小于某個值時,存儲的數(shù)據(jù)會被以一種非常節(jié)省內(nèi)存的方式進行編碼,使用這種編碼理論上至少會節(jié)省10倍以上內(nèi)存(平均節(jié)省5倍以上內(nèi)存)。并且這種編碼技術(shù)對用戶和redis api透明。因為使用這種編碼是用CPU換內(nèi)存,所以我們提供了更改閾值的方法,只需在redis.conf里面進行修改即可.
hash-max-zipmap-entries 64 (2.6以上使用hash-max-ziplist-entries) hash-max-zipmap-value 512 (2.6以上使用hash-max-ziplist-value) list-max-ziplist-entries 512 list-max-ziplist-value 64 zset-max-ziplist-entries 128 zset-max-ziplist-value 64 set-max-intset-entries 512
(集合中)如果某個值超過了配置文件中設(shè)置的最大值,redis將自動把把它(集合)轉(zhuǎn)換為正常的散列表。這種操作對于比較小的數(shù)值是非常快的,但是,如果你為了使用這種編碼技術(shù)而把配置進行了更改,你最好做一下基準測試(和正常的不采用編碼做一下對比).
使用32位的redis
使用32位的redis,對于每一個key,將使用更少的內(nèi)存,因為32位程序,指針占用的字節(jié)數(shù)更少。但是32的redis整個實例使用的內(nèi)存將被限制在4G以下。使用make 32bit命令編譯生成32位的redis。RDB和AOF文件是不區(qū)分32位和64位的(包括字節(jié)順序),所以你可以使用64位的reidis恢復(fù)32位的RDB備份文件,相反亦然.
位級別和字級別的操作
Redis 2.2引入了位級別和字級別的操作: GETRANGE, SETRANGE, GETBIT 和 SETBIT.使用這些命令,你可以把redis的字符串當(dāng)做一個隨機讀取的(字節(jié))數(shù)組。例如你有一個應(yīng)用,用來標志用戶的ID是連續(xù)的整數(shù),你可以使用一個位圖標記用戶的性別,使用1表示男性,0表示女性,或者其他的方式。這樣的話,1億個用戶將僅使用12 M的內(nèi)存。你可以使用同樣的方法,使用 GETRANGE 和 SETRANGE 命令為每個用戶存儲一個字節(jié)的信息。這僅是一個例子,實際上你可以使用這些原始數(shù)據(jù)類型解決更多問題。
盡可能使用散列表(hashes)
小散列表(是說散列表里面存儲的數(shù)少)使用的內(nèi)存非常小,所以你應(yīng)該盡可能的將你的數(shù)據(jù)模型抽象到一個散列表里面。比如你的web系統(tǒng)中有一個用戶對象,不要為這個用戶的名稱,姓氏,郵箱,密碼設(shè)置單獨的key,而是應(yīng)該把這個用戶的所有信息存儲到一張散列表里面.
如果你想了解更多關(guān)于這方面的知識,請讀下一段.
使用散列結(jié)構(gòu)高效存儲抽象的鍵值對
我知道這部分的標題很嚇人,但是我將詳細解釋這部分內(nèi)容.
一般而言,把一個模型(model)表示為key-value的形式存儲在redis中非常容易,當(dāng)然value必須為字符串,這樣存儲不僅比一般的key value存儲高效,并且比memcached存儲還高效.
讓我們做個對比:一些key存儲了一個對象的多個字段要比一個散列表存儲對象的多個字段占用更多的內(nèi)存。這怎么可能?從原理上講,為了保證查找一個數(shù)據(jù)總是在一個常量時間內(nèi)(O(1)),需要一個常量時間復(fù)雜度的數(shù)據(jù)結(jié)構(gòu),比如說散列表.
但是,通常情況下,散列表只包括極少的幾個字段。當(dāng)散列表非常小的時候,我們采用將數(shù)據(jù)encode為一個O(N)的數(shù)據(jù)結(jié)構(gòu),你可以認為這是一個帶有長度屬性的線性數(shù)組。只有當(dāng)N是比較小的時候,才會采用這種encode,這樣使用HGET和HSET命令的復(fù)雜度仍然是O(1):當(dāng)散列表包含的元素增長太多的時候,散列表將被轉(zhuǎn)換為正常的散列表(極限值可以在redis.conf進行配置).
無論是從時間復(fù)雜度還是從常量時間的角度來看,采用這種encode理論上都不會有多大性能提升,但是,一個線性數(shù)組通常會被CPU的緩存更好的命中(線性數(shù)組有更好的局部性),從而提升了訪問的速度.
既然散列表的字段及其對應(yīng)的值并不是用redis objects表示,所以散列表的字段不能像普通的key一樣設(shè)置過期時間。但是這毫不影響對散列表的使用,因為散列表本來就是這樣設(shè)計的(我們相信簡潔比多功能更重要,所以嵌入對象是不允許的,散列表字段設(shè)置單獨的過期時間是不允許的).
所以散列表能高效利用內(nèi)存。這非常有用,當(dāng)你使用一個散列表存儲一個對象或者抽象其他一類相關(guān)的字段為一個模型時。但是,如果我們有一個普通的key value業(yè)務(wù)需求怎么辦?
假如我們想使用redis存儲許多小對象,這些對象可以使用json字符串表示,也可能是html片段和簡單的key->boolean鍵值對。概況的說,一切皆字符串,都可以使用string:string的形式表示.
我們假設(shè)要緩存的對象使用數(shù)字后綴進行編碼,如:
- object:102393
- object:1234
- object:5
我們可以這樣做。每次SET的時候,把key分為兩部分,第一部分當(dāng)做一個key,第二部當(dāng)做散列表字段。比如“object:1234”,分成兩部分:
- a Key named object:12
- a Field named 34
我們使用除最后2個數(shù)字的部分作為key,最后2個數(shù)字做為散列表的字段。使用命令:
HSET object:12 34 somevalue
如你所見,每個散列表將(理論上)包含100個字段,這是CPU資源和內(nèi)存資源之間的一個折中.
另一個需要你關(guān)注的是在這種模式下,無論緩存多少對象,每個散列表都會分配100個字段。因為我們的對象總是以數(shù)字結(jié)尾,而不是一個隨機的字符串。從某些方面來說,這是一種隱性的預(yù)分片。
對于小數(shù)字怎么處理?比如object:2,我們采用object:作為key,所有剩下的數(shù)字作為一個字段。所以object:2和object:10都會被存儲到key為object:的散列表中,但是一個使用2作為字段,一個使用10作為字段。
這種方式將節(jié)省多少內(nèi)存?
我使用了下面的Ruby程序進行了測試:
require 'rubygems' require 'redis' Useoptimization = true def hash_get_key_field(key) s = key.split(":") if s[1].length > 2 {:key => s[0]+":"+s[1][0..-3], :field => s[1][-2..-1]} else {:key => s[0]+":", :field => s[1]} end end def hash_set(r,key,value) kf = hash_get_key_field(key) r.hset(kf[:key],kf[:field],value) end def hash_get(r,key,value) kf = hash_get_key_field(key) r.hget(kf[:key],kf[:field],value) end r = Redis.new (0..100000).each{|id| key = "object:#{id}" if UseOptimization hash_set(r,key,"val") else r.set(key,"val") end }
在redis2.2的64位版本上測試結(jié)果:
- 當(dāng)開啟優(yōu)化時使用內(nèi)存1.7M
- 當(dāng)未開啟優(yōu)化時使用內(nèi)存11M
從結(jié)果看出,這是一個數(shù)量級的優(yōu)化,我認為這種優(yōu)化使redis成為最出色的鍵值緩存。
特別提示: 要使上面的程序較好的工作,別忘記設(shè)置你的redis:
hash-max-zipmap-entries 256
相應(yīng)的最大鍵值長度設(shè)置:
hash-max-zipmap-value 1024
每次散列表的元素數(shù)量或者值超過了閾值,散列將被擴展為一張真正的散列表進行存儲,此時節(jié)約存儲的優(yōu)勢就沒有了.
或許你想問,你為什么不自動將這些key進行轉(zhuǎn)化以提高內(nèi)存利用率?有兩個原因:第一是因為我們更傾向于讓這些權(quán)衡明確,而且必須在很多事情之間權(quán)衡:CPU,內(nèi)存,最大元素大小限制。第二是頂級的鍵空間支持很多有趣的特性,比如過期,LRU算法,所以這種做法并不是一種通用的方法.
Redis的一貫風(fēng)格是用戶必須理解它是如何運作的,必須能夠做出最好的選擇和權(quán)衡,并且清楚它精確的運行方式.
內(nèi)存分配
為了存儲用戶數(shù)據(jù),當(dāng)設(shè)置了maxmemory后Redis會分配幾乎和maxmemory一樣大的內(nèi)存(然而也有可能還會有其他方面的一些內(nèi)存分配).
精確的值可以在配置文件中設(shè)置,或者在啟動后通過 CONFIG SET 命令設(shè)置(see Using memory as an LRU cache for more info). Redis內(nèi)存管理方面,你需要注意以下幾點:
- 當(dāng)某些緩存被刪除后Redis并不是總是立即將內(nèi)存歸還給操作系統(tǒng)。這并不是redis所特有的,而是函數(shù)malloc()的特性。例如你緩存了5G的數(shù)據(jù),然后刪除了2G數(shù)據(jù),從操作系統(tǒng)看,redis可能仍然占用了5G的內(nèi)存(這個內(nèi)存叫RSS,后面會用到這個概念),即使redis已經(jīng)明確聲明只使用了3G的空間。這是因為redis使用的底層內(nèi)存分配器不會這么簡單的就把內(nèi)存歸還給操作系統(tǒng),可能是因為已經(jīng)刪除的key和沒有刪除的key在同一個頁面(page),這樣就不能把完整的一頁歸還給操作系統(tǒng).
- 上面的一點意味著,你應(yīng)該基于你可能會用到的 最大內(nèi)存 來指定redis的最大內(nèi)存。如果你的程序時不時的需要10G內(nèi)存,即便在大多數(shù)情況是使用5G內(nèi)存,你也需要指定最大內(nèi)存為10G.
- 內(nèi)存分配器是智能的,可以復(fù)用用戶已經(jīng)釋放的內(nèi)存。所以當(dāng)使用的內(nèi)存從5G降低到3G時,你可以重新添加更多的key,而不需要再向操作系統(tǒng)申請內(nèi)存。分配器將復(fù)用之前已經(jīng)釋放的2G內(nèi)存.
- 因為這些,當(dāng)redis的peak內(nèi)存非常高于平時的內(nèi)存使用時,碎片所占可用內(nèi)存的比例就會波動很大。當(dāng)前使用的內(nèi)存除以實際使用的物理內(nèi)存(RSS)就是fragmentation;因為RSS就是peak memory,所以當(dāng)大部分key被釋放的時候,此時內(nèi)存的mem_used / RSS就比較高.
如果 maxmemory 沒有設(shè)置,redis就會一直向OS申請內(nèi)存,直到OS的所有內(nèi)存都被使用完。所以通常建議設(shè)置上redis的內(nèi)存限制。或許你也想設(shè)置 maxmemory-policy 的值為 noeviction(在redis的某些老版本默認 并 不是這樣)
設(shè)置了maxmemory后,當(dāng)redis的內(nèi)存達到內(nèi)存限制后,再向redis發(fā)送寫指令,會返回一個內(nèi)存耗盡的錯誤。錯誤通常會觸發(fā)一個應(yīng)用程序錯誤,但是不會導(dǎo)致整臺機器宕掉.