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