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