Redis 內存使用優化與存儲

Redis 經常使用數據類型mysql

 

Redis 最爲經常使用的數據類型主要有如下五種:redis

 

• Stringsql

• Hash數據庫

• List數組

• Set數據結構

• Sorted set併發

 

在具體描述這幾種數據類型以前,咱們先經過一張圖瞭解下 Redis 內部內存管理中是如何描述這些不一樣數據類型的:運維

 

 

首先 Redis 內部使用一個 redisObject 對象來表示全部的 key 和 value,redisObject 最主要的信息如上圖所示:type  表明一個 value 對象具體是何種數據類型,encoding 是不一樣數據類型在 redis 內部的存儲方式,好比:type=string 表明 value 存儲的是一個普通字符串,那麼對應的 encoding 能夠是 raw 或者是 int,若是是 int 則表明實際 redis 內部是按數值型類存儲和表示這個字符串的,固然前提是這個字符串自己能夠用數值表示,好比:」123″ 「456」這樣的字符串。ide

 

這裏須要特殊說明一下 vm 字段,只有打開了 Redis 的虛擬內存功能,此字段纔會真正的分配內存,該功能默認是關閉狀態的,該功能會在後面具體描述。經過上圖咱們能夠發現 Redis 使用 redisObject 來表示全部的 key/value 數據是比較浪費內存的,固然這些內存管理成本的付出主要也是爲了給 Redis 不一樣數據類型提供一個統一的管理接口,實際做者也提供了多種方法幫助咱們儘可能節省內存使用,咱們隨後會具體討論。性能

 

下面咱們先來逐一的分析下這五種數據類型的使用和內部實現方式:

 

String

 

經常使用命令:

 

set,get,decr,incr,mget 等。

 

應用場景:

 

String 是最經常使用的一種數據類型,普通的 key/value 存儲均可以歸爲此類,這裏就不所作解釋了。

 

實現方式:

 

String 在 redis 內部存儲默認就是一個字符串,被 redisObject 所引用,當遇到 incr,decr 等操做時會轉成數值型進行計算,此時 redisObject 的 encoding 字段爲int。

 

Hash

 

經常使用命令:

 

hget,hset,hgetall 等。

 

應用場景:

 

咱們簡單舉個實例來描述下 Hash 的應用場景,好比咱們要存儲一個用戶信息對象數據,包含如下信息:

 

用戶 ID 爲查找的 key,存儲的 value 用戶對象包含姓名,年齡,生日等信息,若是用普通的 key/value 結構來存儲,主要有如下2種存儲方式:

 

 

第一種方式將用戶 ID 做爲查找 key,把其餘信息封裝成一個對象以序列化的方式存儲,這種方式的缺點是,增長了序列化/反序列化的開銷,而且在須要修改其中一項信息時,須要把整個對象取回,而且修改操做須要對併發進行保護,引入CAS等複雜問題。

 

 

第二種方法是這個用戶信息對象有多少成員就存成多少個 key-value 對兒,用用戶 ID +對應屬性的名稱做爲惟一標識來取得對應屬性的值,雖然省去了序列化開銷和併發問題,可是用戶 ID 爲重複存儲,若是存在大量這樣的數據,內存浪費仍是很是可觀的。

 

那麼 Redis 提供的 Hash 很好的解決了這個問題,Redis 的 Hash 實際是內部存儲的 Value 爲一個 HashMap,並提供了直接存取這個 Map 成員的接口,以下圖:

 

 

也就是說,Key 仍然是用戶 ID,value 是一個 Map,這個 Map 的 key 是成員的屬性名,value 是屬性值,這樣對數據的修改和存取均可以直接經過其內部 Map 的 Key(Redis 裏稱內部 Map 的 key 爲 field),也就是經過 key(用戶 ID) + field(屬性標籤)就能夠操做對應屬性數據了,既不須要重複存儲數據,也不會帶來序列化和併發修改控制的問題。很好的解決了問題。

 

這裏同時須要注意,Redis 提供了接口(hgetall)能夠直接取到所有的屬性數據,可是若是內部 Map 的成員不少,那麼涉及到遍歷整個內部 Map 的操做,因爲 Redis 單線程模型的緣故,這個遍歷操做可能會比較耗時,而另其它客戶端的請求徹底不響應,這點須要格外注意。

 

實現方式:

 

上面已經說到 Redis Hash 對應 Value 內部實際就是一個 HashMap,實際這裏會有2種不一樣實現,這個 Hash 的成員比較少時 Redis 爲了節省內存會採用相似一維數組的方式來緊湊存儲,而不會採用真正的 HashMap 結構,對應的 value redisObject 的 encoding 爲 zipmap,當成員數量增大時會自動轉成真正的 HashMap,此時 encoding 爲 ht。

 

List

 

經常使用命令:

 

lpush,rpush,lpop,rpop,lrange等。

 

應用場景:

 

Redis list 的應用場景很是多,也是 Redis 最重要的數據結構之一,好比 twitter 的關注列表,粉絲列表等均可以用 Redis 的 list 結構來實現,比較好理解,這裏再也不重複。

 

實現方式:

 

Redis list 的實現爲一個雙向鏈表,便可以支持反向查找和遍歷,更方便操做,不過帶來了部分額外的內存開銷,Redis 內部的不少實現,包括髮送緩衝隊列等也都是用的這個數據結構。

 

Set

 

經常使用命令:

 

sadd,spop,smembers,sunion 等。

 

應用場景:

 

Redis set 對外提供的功能與 list 相似是一個列表的功能,特殊之處在於 set 是能夠自動排重的,當你須要存儲一個列表數據,又不但願出現重複數據時,set 是一個很好的選擇,而且 set 提供了判斷某個成員是否在一個 set 集合內的重要接口,這個也是 list 所不能提供的。

 

實現方式:

 

set 的內部實現是一個 value 永遠爲 null 的 HashMap,實際就是經過計算 hash 的方式來快速排重的,這也是 set 能提供判斷一個成員是否在集合內的緣由。

 

Sorted set

 

經常使用命令:

 

zadd,zrange,zrem,zcard等

 

使用場景:

 

Redis sorted set 的使用場景與 set 相似,區別是 set 不是自動有序的,而 sorted set 能夠經過用戶額外提供一個優先級(score)的參數來爲成員排序,而且是插入有序的,即自動排序。當你須要一個有序的而且不重複的集合列表,那麼能夠選擇 sorted set 數據結構,好比 twitter 的 public timeline 能夠以發表時間做爲 score 來存儲,這樣獲取時就是自動按時間排好序的。

 

實現方式:

 

Redis sorted set 的內部使用 HashMap 和跳躍表(SkipList)來保證數據的存儲和有序,HashMap 裏放的是成員到 score 的映射,而跳躍表裏存放的是全部的成員,排序依據是 HashMap 裏存的 score,使用跳躍表的結構能夠得到比較高的查找效率,而且在實現上比較簡單。

 

經常使用內存優化手段與參數

 

經過咱們上面的一些實現上的分析能夠看出 redis 實際上的內存管理成本很是高,即佔用了過多的內存,做者對這點也很是清楚,因此提供了一系列的參數和手段來控制和節省內存,咱們分別來討論下。

 

首先最重要的一點是不要開啓 Redis 的 VM 選項,即虛擬內存功能,這個原本是做爲 Redis 存儲超出物理內存數據的一種數據在內存與磁盤換入換出的一個持久化策略,可是其內存管理成本也很是的高,而且咱們後續會分析此種持久化策略並不成熟,因此要關閉 VM 功能,請檢查你的 redis.conf 文件中 vm-enabled 爲 no。

 

其次最好設置下 redis.conf 中的 maxmemory 選項,該選項是告訴 Redis 當使用了多少物理內存後就開始拒絕後續的寫入請求,該參數能很好的保護好你的 Redis 不會由於使用了過多的物理內存而致使 swap,最終嚴重影響性能甚至崩潰。

 

另外 Redis 爲不一樣數據類型分別提供了一組參數來控制內存使用,咱們在前面詳細分析過 Redis Hash 是 value 內部爲一個 HashMap,若是該 Map 的成員數比較少,則會採用相似一維線性的緊湊格式來存儲該 Map,即省去了大量指針的內存開銷,這個參數控制對應在 redis.conf 配置文件中下面2項:

 

hash-max-zipmap-entries 64 

hash-max-zipmap-value 512 

hash-max-zipmap-entries

 

含義是當 value 這個 Map 內部不超過多少個成員時會採用線性緊湊格式存儲,默認是64,即 value 內部有64個如下的成員就是使用線性緊湊存儲,超過該值自動轉成真正的 HashMap。

 

hash-max-zipmap-value 含義是當 value 這個 Map 內部的每一個成員值長度不超過多少字節就會採用線性緊湊存儲來節省空間。

 

以上2個條件任意一個條件超過設置值都會轉換成真正的 HashMap,也就不會再節省內存了,那麼這個值是否是設置的越大越好呢,答案固然是否認的,HashMap 的優點就是查找和操做的時間複雜度都是 O(1) 的,而放棄 Hash 採用一維存儲則是 O(n) 的時間複雜度,若是成員數量不多,則影響不大,不然會嚴重影響性能,因此要權衡好這個值的設置,整體上仍是最根本的時間成本和空間成本上的權衡。

 

一樣相似的參數還有:

 

list-max-ziplist-entries 512

 

說明:list 數據類型多少節點如下會採用去指針的緊湊存儲格式。

 

list-max-ziplist-value 64

 

說明:list 數據類型節點值大小小於多少字節會採用緊湊存儲格式。

 

set-max-intset-entries 512

 

說明:set 數據類型內部數據若是所有是數值型,且包含多少節點如下會採用緊湊格式存儲。

 

最後想說的是 Redis 內部實現沒有對內存分配方面作過多的優化,在必定程度上會存在內存碎片,不過大多數狀況下這個不會成爲 Redis 的性能瓶 頸,不過若是在 Redis 內部存儲的大部分數據是數值型的話,Redis 內部採用了一個 shared integer 的方式來省去分配內存的開銷,即在系統啓動時先分配一個從 1~n 那麼多個數值對象放在一個池子中,若是存儲的數據剛好是這個數值範圍內的數據,則直接從池子裏取出該對象,而且經過引用計數的方式來共享,這樣在系統存儲了大量數值下,也能必定程度上節省內存而且提升性能,這個參數值 n 的設置須要修改源代碼中的一行宏定義 REDIS_SHARED_INTEGERS,該值 默認是 10000,能夠根據本身的須要進行修改,修改後從新編譯就能夠了。

 

Redis 的持久化機制

 

Redis 因爲支持很是豐富的內存數據結構類型,如何把這些複雜的內存組織方式持久化到磁盤上是一個難題,因此 Redis 的持久化方式與傳統數據庫的方式有比較多的差異,Redis 一共支持四種持久化方式,分別是:

 

– 定時快照方式(snapshot)

 

– 基於語句追加文件的方式(aof)

 

– 虛擬內存(vm)

 

– Diskstore 方式

 

在設計思路上,前兩種是基於所有數據都在內存中,即小數據量下提供磁盤落地功能,然後兩種方式則是做者在嘗試存儲數據超過物理內存時,即大數據量的數據存儲,截止到本文,後兩種持久化方式仍然是在實驗階段,而且 vm 方式基本已經被做者放棄,因此實際能在生產環境用的只有前兩種,換句話說 Redis 目前還只能做爲小數據量存儲(所有數據可以加載在內存中),海量數據存儲方面並非 Redis 所擅長的領域。下面分別介紹下這幾種持久化方式:

 

定時快照方式(snapshot):

 

該持久化方式實際是在 Redis 內部一個定時器事件,每隔固定時間去檢查當前數據發生的改變次數與時間是否知足配置的持久化觸發的條件,若是知足則經過操做系統 fork 調用來建立出一個子進程,這個子進程默認會與父進程共享相同的地址空間,這時就能夠經過子進程來遍歷整個內存來進行存儲操做,而主進程則仍然能夠提供服務,當有寫入時由操做系統按照內存頁(page)爲單位來進行 copy-on-write 保證父子進程之間不會互相影響。

 

該持久化的主要缺點是定時快照只是表明一段時間內的內存映像,因此係統重啓會丟失上次快照與重啓之間全部的數據。

 

基於語句追加方式(aof):

 

aof 方式實際相似 mysql 基於語句的 binlog 方式,即每條會使 Redis 內存數據發生改變的命令都會追加到一個 log 文件中,也就是說這個 log 文件就是 Redis 的持久化數據。

 

aof 的方式的主要缺點是追加 log 文件可能致使體積過大,當系統重啓恢復數據時若是是 aof 的方式則加載數據會很是慢,幾十G的數據可能須要幾小時才能加載完,固然這個耗時並非由於磁盤文件讀取速度慢,而是因爲讀取的全部命令都要在內存中執行一遍。另外因爲每條命令都要寫 log,因此使用 aof 的方式,Redis 的讀寫性能也會有所降低。

 

虛擬內存方式:

 

虛擬內存方式是 Redis 來進行用戶空間的數據換入換出的一個策略,此種方式在實現的效果上比較差,主要問題是代碼複雜,重啓慢,複製慢等等,目前已經被做者放棄。

 

diskstore 方式:

 

diskstore 方式是做者放棄了虛擬內存方式後選擇的一種新的實現方式,也就是傳統的 B-tree 的方式,目前仍在實驗階段,後續是否可用咱們能夠拭目以待。

 

Redis 持久化磁盤 IO 方式及其帶來的問題

 

有 Redis 線上運維經驗的人會發現 Redis 在物理內存使用比較多,但尚未超過實際物理內存總容量時就會發生不穩定甚至崩潰的問題,有人認爲是基於快照方式持久化的 fork 系統調用形成內存佔用加倍而致使的,這種觀點是不許確的,由於 fork 調用的 copy-on-write 機制是基於操做系統頁這個單位的,也就是隻有有寫入的髒頁會被複制,可是通常你的系統不會在短期內全部的頁都發生了寫入而致使複製,那麼是什麼緣由致使 Redis 崩潰的呢?

 

答案是 Redis 的持久化使用了 Buffer IO 形成的,所謂 Buffer IO 是指 Redis 對持久化文件的寫入和讀取操做都會使用物理內存的 Page Cache,而大多數數據庫系統會使用 Direct IO 來繞過這層 Page Cache 並自行維護一個數據的 Cache,而當 Redis 的持久化文件過大(尤爲是快照文件),並對其進行讀寫時,磁盤文件中的數據都會被加載到物理內 存中做爲操做系統對該文件的一層 Cache,而這層 Cache 的數據與 Redis 內存中管理的數據實際是重複存儲的,雖然內核在物理內存緊張時會作 Page Cache 的剔除工做,但內核極可能認爲某塊 Page Cache 更重要,而讓你的進程開始 Swap,這時你的系統就會開始出現不穩定或者崩潰了。咱們的經驗是當你的 Redis 物理內存使用超過內存總容量的3/5時就會開始比較危險了。

 

下圖是 Redis 在讀取或者寫入快照文件 dump.rdb 後的內存數據圖:

 

 

總結:

 

1. 根據業務須要選擇合適的數據類型,併爲不一樣的應用場景設置相應的緊湊存儲參數。

 

2. 當業務場景不須要數據持久化時,關閉全部的持久化方式能夠得到最佳的性能以及最大的內存使用量。

 

3. 若是須要使用持久化,根據是否能夠容忍重啓丟失部分數據在快照方式與語句追加方式之間選擇其一,不要使用虛擬內存以及 diskstore 方式。

 

4. 不要讓你的 Redis 所在機器物理內存使用超過實際內存總量的3/5。

相關文章
相關標籤/搜索