本文從實際工做中方案選型出發,以Redis的特性爲着眼點,逐層剝開Redis技術內幕,並對工做中容易出現的使用誤區進行了總結。redis
對於有狀態的服務而言,數據庫每每會成爲系統的瓶頸所在。在用戶活躍的高峯期,或者因爲PUSH、活動等引起的請求突增,都會給後端的數據庫形成巨大的壓力。數據庫
由存儲系統的特性咱們知道,從內存讀一個數據,比從通常的磁盤讀要快10000倍左右,基於這樣的緣由,數據庫自己也會有必定的內存cache。可是當熱數據集比較大的時候,本地cache會頻繁淘汰,此時會觸發大量磁盤IO,性能急劇降低,每每也會伴隨有大量的慢日誌。另外,有些數據是須要經過複雜的查詢或計算後獲得且又不會頻繁變化的。後端
雖然說數據庫能夠經過讀寫分離來擴展讀的能力,但存在增長slave實例的成本、主從延遲致使數據不一致等問題。子曾經曰過,「計算機科學領域的任何問題能夠經過增長一箇中間層來解決」,因而咱們考慮在系統中再增長一個cache層,這裏暫不討論cache的設計實現。數組
須要cache的熱點數據緩存
基於以上數據的特色,咱們最終選擇了redis。服務器
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes with radius queries and streams. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster.網絡
以上引自官網的原話,歸納起來有這樣一些特性:數據結構
雖然是單進程單線程模型,可是讀寫性能很是優異,單機可支持10wQPS,緣由主要有如下幾點:多線程
固然這種單線程事件機制也是有缺陷的,因爲全部的事件都是串行執行,一旦某個事件比較重就會阻塞其它事件,從而致使整個系統的吞吐率降低。好比某個客戶端執行了一個比較重的lua函數、或者使用了諸如keys*、zrange(0,-1)、hgetall等全集合掃描的操做,又或者刪除的過時鍵是個big key,又或者使用了較多內存的redis實例進行bgsave時,都會致使服務器必定程度的阻塞,通常伴隨會有相應的慢日誌。因此咱們在實際使用redis的過程當中,必需要給每一次的操做分配合理的時間片。架構
對於內存型數據庫,好比redis和memcache,若是數據狀態不落盤,一旦服務器進程退出,那麼這些數據狀態也就會所有消失不見。數據狀態的重建須要從後端數據庫回源,這會給後端數據庫形成很是大的壓力,最壞的狀況可能會把數據庫壓垮,致使服務不可用。
爲了解決這個問題,Redis提供了RDB和AOF兩種持久化方式。前者會生成一分內存快照--RDB文件,該文件是通過壓縮的二進制格式,記錄的是鍵值對數據;後者則是以Redis的命令請求協議格式來保存,記錄的是命令操做;
因爲RDB SAVE和AOF重寫會阻塞主線程,因此都支持BG模式執行,至於持久化的具體實現這裏就不展開討論了。
比較巧妙的是,Redis並無使用固定的數據結構來存儲各類類型的數據,而是建立了一套對象系統,對於同一個對象,能夠對應一個或多個不一樣的底層數據結構(或者叫作編碼方式),某些特定的編碼方式在時空間的效率上有所優化,經過執行"Object Encoding"能夠查詢當前編碼方式。
Redis的高可用,主要經過主從複製機制以及Sentinel集羣來實現。
當從服務器有2個或者多個時,Redis的主從架構能夠有兩種形式。一種是,全部的從服務器直接掛在主服務器上,這種模式的優勢是,全部從服務器複製的延遲相對較低,而缺點在於加大了主服務器的複製壓力;另外一種形式,是採用級聯的方式,S1從M複製,S2從S1複製,以此類推,這種模式的優勢是,將主服務器的複製壓力分攤到多個服務器上,而缺點在於越處於級聯下游的從實例,複製延遲就越大。
從主從複製模式能夠看出,Redis的數據只能保證最終一致,不能保證強一致性。
讀擴展,基於主從架構,能夠很好的平行擴展讀的能力。寫擴展,主要受限於主服務器的硬件資源的限制,一是單個實例內存容量受限,二是一個實例只使用到CPU一個核。下面討論基於多套主從架構Redis實例的集羣實現,目前主要有如下幾種方案:
Redis的key是string類型,最大能夠是512MB,那麼實際中是否是也能夠這樣用呢?答案是否認的,redis將key保存在一個全局的hashtable,若是key過大,一是佔用過多的內存,二是計算hash和字符串比較都會更耗時;通常建議key的大小不超過2kB。
或者說是big value,這會致使刪除key的操做比較耗時,會阻塞主線程。好比有些同窗喜歡用集合類的對象,動輒上百萬的元素。對於這類超大集合,通常有兩種優化方案,一是採起分片的方式,將每一個集合分片控制在較小的範圍內,好比小於1000個元素;二是起一個異步任務,對集合中的元素分批進行老化。
好比在業務代碼使用了keys*,hgetall,zrange(0, -1)等返回集合中全部元素,這些都屬於阻塞操做,通常考慮用scan,hscan等迭代操做代替。
內存過大有什麼問題呢?上文中在講到持久化的時候其實有說到,不管是生成RDB文件,仍是AOF重寫,都是要對整個實例的內存數據進行掃描,很是消耗CPU和磁盤資源;當使用Backgroud方式建立子進程時也會涉及到內存空間的拷貝,即使使用了COW機制,也會佔用至關的內存開銷。另外,在主從複製的第一階段,save、傳輸和加載RDB文件的開銷,也會隨着RDB文件的變大而變大。當單個實例達到瓶頸時,更好的解決方案應該是採用集羣方案。
redis刪除過時鍵採用了惰性刪除和按期刪除相結合的策略,惰性刪除則是在每次GET/SET操做時去刪,按期刪除,則是在時間事件中,從整個key空間隨機取樣,直到過時鍵比率小於25%,若是同時有大量key過時的話,很可能致使主線程阻塞。通常能夠經過作散列來優化處理。