一文弄懂Redis

引言

本文從實際工做中方案選型出發,以Redis的特性爲着眼點,逐層剝開Redis技術內幕,並對工做中容易出現的使用誤區進行了總結。redis

面臨的問題

對於有狀態的服務而言,數據庫每每會成爲系統的瓶頸所在。在用戶活躍的高峯期,或者因爲PUSH、活動等引起的請求突增,都會給後端的數據庫形成巨大的壓力。數據庫

由存儲系統的特性咱們知道,從內存讀一個數據,比從通常的磁盤讀要快10000倍左右,基於這樣的緣由,數據庫自己也會有必定的內存cache。可是當熱數據集比較大的時候,本地cache會頻繁淘汰,此時會觸發大量磁盤IO,性能急劇降低,每每也會伴隨有大量的慢日誌。另外,有些數據是須要經過複雜的查詢或計算後獲得且又不會頻繁變化的。後端

雖然說數據庫能夠經過讀寫分離來擴展讀的能力,但存在增長slave實例的成本、主從延遲致使數據不一致等問題。子曾經曰過,「計算機科學領域的任何問題能夠經過增長一箇中間層來解決」,因而咱們考慮在系統中再增長一個cache層,這裏暫不討論cache的設計實現。數組

讓數聽說話

須要cache的熱點數據緩存

  • 用戶我的資料
  • 關注、粉絲、好友列表
  • 關注數、粉絲數、好友數
  • 用戶之間的關係
  • 用戶瀏覽過的TopN...

基於以上數據的特色,咱們最終選擇了redis。服務器

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.網絡

以上引自官網的原話,歸納起來有這樣一些特性:數據結構

  • 純內存數據存儲,而且支持多種持久化
  • 支持豐富的數據結構,好比string,hash,list,set,sorted set,bitmap,hyperloglog, geospatial index等
  • 支持複製、Lua腳本、LRU淘汰、事務
  • 基於Sentinel實現高可用
  • Cluster模式支持自動分區

看得見的優點

0. 性能強悍

雖然是單進程單線程模型,可是讀寫性能很是優異,單機可支持10wQPS,緣由主要有如下幾點:多線程

  • 純內存操做,避免了與磁盤的交互
  • 單線程,天生的隊列模式,避免了因多線程競爭而致使的上下文切換和搶鎖的開銷
  • 事件機制,Redis服務器將全部處理的任務分爲兩類事件,一類是採用I/O多路複用處理客戶端請求的網絡事件;一類是處理定時任務的時間事件,包括更新統計信息、清理過時鍵、持久化、主從同步等;

固然這種單線程事件機制也是有缺陷的,因爲全部的事件都是串行執行,一旦某個事件比較重就會阻塞其它事件,從而致使整個系統的吞吐率降低。好比某個客戶端執行了一個比較重的lua函數、或者使用了諸如keys*、zrange(0,-1)、hgetall等全集合掃描的操做,又或者刪除的過時鍵是個big key,又或者使用了較多內存的redis實例進行bgsave時,都會致使服務器必定程度的阻塞,通常伴隨會有相應的慢日誌。因此咱們在實際使用redis的過程當中,必需要給每一次的操做分配合理的時間片。架構

1. 支持持久化

對於內存型數據庫,好比redis和memcache,若是數據狀態不落盤,一旦服務器進程退出,那麼這些數據狀態也就會所有消失不見。數據狀態的重建須要從後端數據庫回源,這會給後端數據庫形成很是大的壓力,最壞的狀況可能會把數據庫壓垮,致使服務不可用。

爲了解決這個問題,Redis提供了RDB和AOF兩種持久化方式。前者會生成一分內存快照--RDB文件,該文件是通過壓縮的二進制格式,記錄的是鍵值對數據;後者則是以Redis的命令請求協議格式來保存,記錄的是命令操做;

  • RDB的特色,文件體積小,加載速度快;但由於是對整個實例的內存生成快照,因此操做比較重,通常持久化的間隔不宜太快,因此保存的數據相對比較舊一些;
  • AOF的特色,文件體積較大(能夠用AOF重寫進行覆蓋);全部的寫操做會追加到AOF緩衝區,持久化的行爲可配置,分爲三種,always(每次刷盤)、everysec(異步線程每隔1秒刷一次)和no(只寫到page cache,交給操做系統來刷盤);相對來講,AOF文件數據保存的比較新一些,因此若是開啓了AOF,那麼Redis服務器恢復的時候會優先加載AOF文件。

因爲RDB SAVE和AOF重寫會阻塞主線程,因此都支持BG模式執行,至於持久化的具體實現這裏就不展開討論了。

2. 豐富的數據類型

比較巧妙的是,Redis並無使用固定的數據結構來存儲各類類型的數據,而是建立了一套對象系統,對於同一個對象,能夠對應一個或多個不一樣的底層數據結構(或者叫作編碼方式),某些特定的編碼方式在時空間的效率上有所優化,經過執行"Object Encoding"能夠查詢當前編碼方式。

  • String 字符串對象,最大可支持512MB,memcache最大隻支持1MB.編碼能夠是int,raw或者embstr,int對應整型數據,可方便計數,embstr用來保存長度小於等於39字節的字符串值,採用連續的空間進行存儲,更好利用緩存優點;字符串對象經常使用來進行計數,或者緩存序列化的對象;
  • List 列表對象,編碼能夠是ziplist或linkedlist,ziplist是爲了節約內存而開發的,是一個通過特殊編碼的連續內存塊組成的順序結構,當列表對象元素的個數較少以及元素的長度較短時會採用這種方式;列表對象通常可用來實現消息隊列;
  • Hash 哈希對象,編碼能夠是ziplist或hashtable,在Redis的實現裏,採用的鏈式衝突來解決衝突問題,而且爲了維護hash表的負載因子在一個合理的範圍,會執行漸進式rehash;哈希對象通常用於存儲某個對象的屬性數據,便於選擇性查詢,這個效率要比粗暴的序列化和反序列化要高不少,好比用戶的我的資料;另外一個用法,則是利用ziplist編碼方式實現壓縮存儲,節省內存;
  • Set 集合對象,編碼能夠是intset或hashtable,當集合的元素很少且都是整數時,Redis就會使用整數集intset,底層是一個以有序、無重複的方式進行排列的數組,能有效的節約內存;這個對象通常用於去重,好比派獎;
  • Sorted Set 有序集合對象,編碼能夠是ziplist或者skiplist,跳躍表skiplist是一種查找效率可媲美平衡樹的數據結構,平均O(logN),最壞O(N),並且實現更加簡單;其實,Redis用了skiplist和hashtable兩種數據結構來實現zset,一方面hashtable能實現O(1)的查找,另外一方面skiplist實現了有序,可支持範圍查找;有集合序對象用的就比較普遍了,好比排行榜(只要是排序相關的列表均可以)、延遲任務隊列等。

3. 高可用

Redis的高可用,主要經過主從複製機制以及Sentinel集羣來實現。

  • 主從複製 分爲兩個階段,首先,當從服務器發起SYNC命令後,主服務器會生成最新的RDB文件發送給從服務器,並使用一個緩衝區來記錄今後刻開始主服務器執行的全部寫命令;待RDB文件傳輸完以後,再將該緩衝區的數據再發送給從服務器,這樣就完成了複製。舊的Redis版本有個缺陷是,若是在第二個階段發生失敗,須要從第一個階段從新開始同步,而這個階段的操做會消耗大量的CPU、內存和磁盤I/O以及網絡帶寬資源,太過耗費資源。因此從2.8版本開始,實現了部分重同步,經過主從服務器各維護一個複製偏移量來實現。
  • Sentinel 由一個或多個Sentinel實例組成的哨兵系統,能夠監視任意多個主從服務器,並完成Failover的操做。Sentinal實際上是一個運行在特殊模式下的Redis服務器,運行期間,會與各服務器創建網絡鏈接,以檢測服務器的狀態;同時會與其它Sentinel服務器建立鏈接,完成信息交換,好比發現某個主服務器心跳異常時,會互相詢問心跳結果,當超過必定數量時便可斷定爲客觀下線;一旦主服務器被斷定爲客觀下線狀態,那麼Sentinel集羣會經過raft協議選舉,選出一個Leader來執行Failover。
  • Failover 通常來講,會先選出優先級最高的從服務器,而後再從中選出複製偏移量最大的實例,做爲新的主服務器;最後將其它從和舊的主都切換爲新主的從。

當從服務器有2個或者多個時,Redis的主從架構能夠有兩種形式。一種是,全部的從服務器直接掛在主服務器上,這種模式的優勢是,全部從服務器複製的延遲相對較低,而缺點在於加大了主服務器的複製壓力;另外一種形式,是採用級聯的方式,S1從M複製,S2從S1複製,以此類推,這種模式的優勢是,將主服務器的複製壓力分攤到多個服務器上,而缺點在於越處於級聯下游的從實例,複製延遲就越大。

從主從複製模式能夠看出,Redis的數據只能保證最終一致,不能保證強一致性。

4. 可擴展

讀擴展,基於主從架構,能夠很好的平行擴展讀的能力。寫擴展,主要受限於主服務器的硬件資源的限制,一是單個實例內存容量受限,二是一個實例只使用到CPU一個核。下面討論基於多套主從架構Redis實例的集羣實現,目前主要有如下幾種方案:

  • 客戶端分片 實現方案,業務進程經過對key進行hash來分片,用Sentinel作failover。優勢:運維簡單,每一個實例獨立部署;可以使用lua腳本,業務進程執行的key均hash到同一個分片便可;缺點:一旦從新分片,因爲數據沒法自動遷移,部分數據須要回源;
  • Redis集羣 是官方提供的分佈式數據庫方案,經過分片實現數據共享,並提供複製和failover。按照16384個槽位進行分片,且實例之間共享分片視圖。優勢:當發生從新分片時,數據能夠自動遷移;缺點:客戶端須要升級到支持集羣協議的版本;客戶端須要感知分片實例,最壞的狀況,每一個key須要一次重定向;不支持lua腳本;不支持pipeline;
  • Codis 是由豌豆莢團隊開源的一款分佈式組件,它將分佈式的邏輯從Redis集羣剝離出來,交由幾個組件來完成,與數據的讀寫解耦。Codis proxy負責分片和聚合,dashboard做爲管理後臺,zookeeper作配置管理,Sentinel作failover。優勢:底層透明,客戶端兼容性好;從新分片時,數據可自動遷移;支持pipeline;支持lua腳本,業務進程保證執行的key均hash到同一個分片便可;缺點:運維較爲複雜;引入了中間層;

看不見的誤區

  • 鍵過大

Redis的key是string類型,最大能夠是512MB,那麼實際中是否是也能夠這樣用呢?答案是否認的,redis將key保存在一個全局的hashtable,若是key過大,一是佔用過多的內存,二是計算hash和字符串比較都會更耗時;通常建議key的大小不超過2kB。

  • Big key

或者說是big value,這會致使刪除key的操做比較耗時,會阻塞主線程。好比有些同窗喜歡用集合類的對象,動輒上百萬的元素。對於這類超大集合,通常有兩種優化方案,一是採起分片的方式,將每一個集合分片控制在較小的範圍內,好比小於1000個元素;二是起一個異步任務,對集合中的元素分批進行老化。

  • 全集合掃描

好比在業務代碼使用了keys*,hgetall,zrange(0, -1)等返回集合中全部元素,這些都屬於阻塞操做,通常考慮用scan,hscan等迭代操做代替。

  • 單個實例內存過大

內存過大有什麼問題呢?上文中在講到持久化的時候其實有說到,不管是生成RDB文件,仍是AOF重寫,都是要對整個實例的內存數據進行掃描,很是消耗CPU和磁盤資源;當使用Backgroud方式建立子進程時也會涉及到內存空間的拷貝,即使使用了COW機制,也會佔用至關的內存開銷。另外,在主從複製的第一階段,save、傳輸和加載RDB文件的開銷,也會隨着RDB文件的變大而變大。當單個實例達到瓶頸時,更好的解決方案應該是採用集羣方案。

  • 大量key同時過時

redis刪除過時鍵採用了惰性刪除和按期刪除相結合的策略,惰性刪除則是在每次GET/SET操做時去刪,按期刪除,則是在時間事件中,從整個key空間隨機取樣,直到過時鍵比率小於25%,若是同時有大量key過時的話,很可能致使主線程阻塞。通常能夠經過作散列來優化處理。

參考資料

相關文章
相關標籤/搜索