這個頁面仍然在維護中。當你在使用redis的過程中遇到一些與記憶體相關的問題時,你需要關注下面的事情。
Redis2.2版本及以後,存儲集合資料的時候會采用記憶體壓縮技術,以使用更少的記憶體存儲更多的資料。如Hashes,Lists,Sets和Sorted Sets,當這些集合中的所有數都小于一個給定的元素,并且集合中元素數量小于某個值時,存儲的資料會被以一種非常節省記憶體的方式進行編碼,使用這種編碼理論上至少會節省10倍以上記憶體(平均節省5倍以上記憶體)。并且這種編碼技術對使用者和redis api透明。因為使用這種編碼是用CPU換記憶體,是以我們提供了更改門檻值的方法,隻需在redis.conf裡面進行修改即可.
(集合中)如果某個值超過了配置檔案中設定的最大值,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 指令為每個使用者存儲一個位元組的資訊。這僅是一個例子,實際上你可以使用這些原始資料類型解決更多問題。
小散清單(是說散清單裡面存儲的數少)使用的記憶體非常小,是以你應該盡可能的将你的資料模型抽象到一個散清單裡面。比如你的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個數字做為散清單的字段。使用指令:
如你所見,每個散清單将(理論上)包含100個字段,這是CPU資源和記憶體資源之間的一個折中.
另一個需要你關注的是在這種模式下,無論緩存多少對象,每個散清單都會配置設定100個字段。因為我們的對象總是以數字結尾,而不是一個随機的字元串。從某些方面來說,這是一種隐性的預分片。
對于小數字怎麼處理?比如object:2,我們采用object:作為key,所有剩下的數字作為一個字段。是以object:2和object:10都會被存儲到key為object:的散清單中,但是一個使用2作為字段,一個使用10作為字段。
這種方式将節省多少記憶體?
我使用了下面的Ruby程式進行了測試:
在redis2.2的64位版本上測試結果:
當開啟優化時使用記憶體1.7M
當未開啟優化時使用記憶體11M
從結果看出,這是一個數量級的優化,我認為這種優化使redis成為最出色的鍵值緩存。
特别提示: 要使上面的程式較好的工作,别忘記設定你的redis:
相應的最大鍵值長度設定:
每次散清單的元素數量或者值超過了門檻值,散列将被擴充為一張真正的散清單進行存儲,此時節約存儲的優勢就沒有了.
或許你想問,你為什麼不自動将這些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被釋放的時候,此時記憶體的<code>mem_used / RSS</code>就比較高.
如果 <code>maxmemory</code> 沒有設定,redis就會一直向OS申請記憶體,直到OS的所有記憶體都被使用完。是以通常建議設定上redis的記憶體限制。或許你也想設定 <code>maxmemory-policy</code> 的值為 <code>noeviction</code>(在redis的某些老版本預設 并 不是這樣)
設定了maxmemory後,當redis的記憶體達到記憶體限制後,再向redis發送寫指令,會傳回一個記憶體耗盡的錯誤。錯誤通常會觸發一個應用程式錯誤,但是不會導緻整台機器宕掉.
本文作者:陳群
本文來自雲栖社群合作夥伴rediscn,了解相關資訊可以關注redis.cn網站。