Redis內存使用優化與存儲

本文將對Redis的常見數據類型的使用場景以及在內存優化方面和性能優化方面作一些詳細的分析。mysql

Redis經常使用數據類型

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

  • String
  • Hash
  • List
  • Set
  • Sorted set

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

 

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

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

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

  • String

    經常使用命令:數據結構

    set,get,decr,incr,mget 等。併發

    應用場景:運維

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

    實現方式:

    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。

下一篇Redis的文章會討論下Redis的複製與可靠集羣搭建與在線擴容的問題。

相關文章
相關標籤/搜索