1. 程式人生 > 程式設計 >Redis由淺入深深深深深剖析

Redis由淺入深深深深深剖析

前言

常用的SQL資料庫的資料都是存在磁碟中的,雖然在資料庫底層也做了對應的快取來減少資料庫的IO壓力,但由於資料庫的快取一般是針對查詢的內容,而且粒度也比較小,一般只有表中的資料沒有發生變動的時候,資料庫的快取才會產生作用,但這並不能減少業務邏輯對資料庫的增刪改操作的IO壓力,因此快取技術應運而生,該技術實現了對熱點資料的快取記憶體,可以大大緩解後端資料庫的壓力。

主流應用架構

主流應用架構
客戶端在對資料庫發起請求時,先到快取層檢視是否有所需的資料,如果快取層存有客戶端所需的資料,則直接從快取層返回,否則進行穿透查詢,對資料庫進行查詢,如果在資料庫中查詢到該資料,則將該資料回寫到快取層,以便下次客戶端再次查詢能夠直接從快取層獲取資料。

快取中介軟體 -- Memcache和Redis的區別

  • Memcache:程式碼層類似Hash

    1.支援簡單資料型別
    2.不支援資料持久化儲存
    3.不支援主從
    4.不支援分片

  • Redis

    1.資料型別豐富
    2.支援資料磁碟持久化儲存
    3.支援主從
    4.支援分片

為什麼Redis能這麼快

Redis的效率很高,官方給出的資料是100000+QPS(query per second),這是因為:

1.Redis完全基於記憶體,絕大部分請求是純粹的記憶體操作,執行效率高。
2.Redis使用單程式單執行緒模型的(K,V)資料庫,將資料儲存在記憶體中,存取均不會受到硬碟IO的限制,因此其執行速度極快,另外單執行緒也能處理高併發請求,還可以避免頻繁上下文切換和鎖的競爭,如果想要多核執行也可以啟動多個例項。
3.資料結構簡單,對資料操作也簡單,Redis不使用表,不會強制使用者對各個關係進行關聯,不會有複雜的關係限制,其儲存結構就是鍵值對

,類似於HashMap,HashMap最大的優點就是存取的時間複雜度為O(1)。
4.Redis使用多路I/O複用模型,為非阻塞IO(非阻塞IO會另寫一篇解釋,可以先行百度)。


:Redis採用的I/O多路複用函式:epoll/kqueue/evport/select
選用策略:
1.因地制宜,優先選擇時間複雜度為O(1)的I/O多路複用函式作為底層實現。
2.由於select要遍歷每一個IO,所以其時間複雜度為O(n),通常被作為保底方案。
3.基於react設計模式監聽I/O事件。


Redis的資料型別

  • String

    最基本的資料型別,其值最大可儲存512M,二進位制安全(Redis的String可以包含任何二進位制資料,包含jpg物件等)。

    redis存String
    注:如果重複寫入key相同的鍵值對,後寫入的會將之前寫入的覆蓋。

  • Hash

    String元素組成的字典,適用於儲存物件。

    redis存Hash

  • List

    列表,按照String元素插入順序排序。其順序為後進先出。由於其具有棧的特性,所以可以實現如“最新訊息排行榜”這類的功能。

    redis存List

  • Set

    String元素組成的無序集合,通過雜湊表實現(增刪改查時間複雜度為O(1)),不允許重複。

    redis存Set
    另外,當我們使用smembers遍歷set中的元素時,其順序也是不確定的,是通過hash運算過後的結果。Redis還對集合提供了求交集、並集、差集等操作,可以實現如同共同關注,共同好友等功能。

  • Sorted Set

    通過分數來為集合中的成員進行從小到大的排序。

    redis存SortedSet

  • 更高階的Redis型別

    用於計數的HyperLogLog、用於支援儲存地理位置資訊的Geo。

從海量Key裡查詢出某一個固定字首的Key

  • 假設redis中有十億條key,如何從這麼多key中找到固定字首的key?

    • 方法1:使用KEYS [pattern]:查詢所有符合給定模式pattern的key

      使用keys [pattern]指令可以找到所有符合pattern條件的key,但是keys會一次性返回所有符合條件的key,所以會造成redis的卡頓,假設redis此時正在生產環境下,使用該命令就會造成隱患,另外如果一次性返回所有key,對記憶體的消耗在某些條件下也是巨大的。 例:
      keys test* //返回所有以test為字首的key
    • 方法2:使用SCAN cursor [MATCH pattern] [COUNT count]

      cursor:遊標 MATCH pattern:查詢key的條件 count:返回的條數 SCAN是一個基於遊標的迭代器,需要基於上一次的遊標延續之前的迭代過程。SCAN以0作為遊標,開始一次新的迭代,直到命令返回遊標0完成一次遍歷。此命令並不保證每次執行都返回某個給定數量的元素,甚至會返回0個元素,但只要遊標不是0,程式都不會認為SCAN命令結束,但是返回的元素數量大概率符合count引數。另外,SCAN支援模糊查詢。 例:
      SCAN 0 MATCH test* COUNT 10 //每次返回10條以test為字首的key

如何通過Redis實現分散式鎖

  • 分散式鎖

    分散式鎖是控制分散式系統之間共同訪問共享資源的一種鎖的實現。如果一個系統,或者不同系統的不同主機之間共享某個資源時,往往需要互斥,來排除幹擾,滿足資料一致性。
    分散式鎖需要解決的問題如下:
    1.互斥性:任意時刻只有一個客戶端獲取到鎖,不能有兩個客戶端同時獲取到鎖。
    2.安全性:鎖只能被持有該鎖的客戶端刪除,不能由其它客戶端刪除。
    3.死鎖:獲取鎖的客戶端因為某些原因而宕機繼而無法釋放鎖,其它客戶端再也無法獲取鎖而導致死鎖,此時需要有特殊機制來避免死鎖。
    4.容錯:當各個節點,如某個redis節點宕機的時候,客戶端仍然能夠獲取鎖或釋放鎖。

  • 如何使用redis實現分散式鎖

    • 使用SETNX實現

      SETNX key value:如果key不存在,則建立並賦值。該命令時間複雜度為O(1),如果設定成功,則返回1,否則返回0。

      redis分散式鎖
      由於SETNX指令操作簡單,且是原子性的,所以初期的時候經常被人們作為分散式鎖,我們在應用的時候,可以在某個共享資源區之前先使用SETNX指令,檢視是否設定成功,如果設定成功則說明前方沒有客戶端正在訪問該資源,如果設定失敗則說明有客戶端正在訪問該資源,那麼當前客戶端就需要等待。但是如果真的這麼做,就會存在一個問題,因為SETNX是長久存在的,所以假設一個客戶端正在訪問資源,並且上鎖,那麼當這個客戶端結束訪問時,該鎖依舊存在,後來者也無法成功獲取鎖,這個該如何解決呢?

      由於SETNX並不支援傳入EXPIRE引數,所以我們可以直接使用EXPIRE指令來對特定的key來設定過期時間。

      用法EXPIRE key seconds

      expire指令.png

      程式

      RedisService redisService = SpringUtils.getBean(RedisService.class);
      long status = redisService.setnx(key,"1");
      if(status == 1){
        redisService.expire(key,expire);
        doOcuppiedWork();
      }
      複製程式碼

      這段程式存在的問題:假設程式執行到第二行出現異常,那麼程式來不及設定過期時間就結束了,則key會一直存在,等同於鎖一直被持有無法釋放。出現此問題的根本原因為:原子性得不到滿足
      解決:從Redis2.6.12版本開始,我們就可以使用Set操作,將Setnx和expire融合在一起執行,具體做法如下。

      SET KEY value [EX seconds] [PX milliseconds] [NX|XX]
      複製程式碼

      EX second:設定鍵的過期時間為second
      PX millisecond:設定鍵的過期時間為millisecond毫秒
      NX:只在鍵不存在時,才對鍵進行設定操作。
      XX:只在鍵已經存在時,才對鍵進行設定操作。
      :SET操作成功完成時才會返回OK,否則返回nil。

      有了SET我們就可以在程式中使用類似下面的程式碼實現分散式鎖了:

      RedisService redisService = SpringUtils.getBean(RedisService.class);
      String result = redisService.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
      if("OK.equals(result)"){
        doOcuppiredWork();
      }
      複製程式碼

如何實現非同步佇列

  • 使用Redis中的List作為佇列

    使用上文所說的Redis的資料結構中的List作為佇列 Rpush生產訊息,LPOP消費訊息。

    使用Redis作為非同步佇列
    此時我們可以看到,該佇列是使用rpush生產佇列,使用lpop消費佇列。在這個生產者-消費者佇列裡,當lpop沒有訊息時,證明該佇列中沒有元素,並且生產者還沒有來得及生產新的資料
    缺點:lpop不會等待佇列中有值之後再消費,而是直接進行消費。
    彌補:可以通過在應用層引入Sleep機制去呼叫LPOP重試。

  • 使用BLPOP key [key...] timeout

    BLPOP key [key ...] timeout:阻塞直到佇列有訊息或者超時。

    兩個客戶端模擬A

    兩個客戶端模擬B

    兩個客戶端模擬C
    缺點:按照此種方法,我們生產後的資料只能提供給各個單一消費者消費

    能否實現生產一次就能讓多個消費者消費呢?

  • pub/sub:主題訂閱者模式

    傳送者(pub)傳送訊息,訂閱者(sub)接收訊息。 訂閱者可以訂閱任意數量的頻道

    釋出訂閱者模式
    pub/sub模式的缺點
    訊息的釋出是無狀態的,無法保證可達。對於釋出者來說,訊息是“即發即失”的,此時如果某個消費者在生產者釋出訊息時下線,重新上線之後,是無法接收該訊息的,要解決該問題需要使用專業的訊息佇列,如kafka...此處不再贅述。

Redis持久化

  • 什麼是持久化

    持久化,即將資料持久儲存,而不因斷電或其它各種複雜外部環境影響資料的完整性。由於Redis將資料儲存在記憶體而不是磁碟中,所以記憶體一旦斷電,Redis中儲存的資料也隨即消失,這往往是使用者不期望的,所以Redis有持久化機制來保證資料的安全性。

  • Redis如何做持久化

    Redis目前有兩種持久化方式,即RDBAOF,RDB是通過儲存某個時間點的全量資料快照實現資料的持久化,當恢復資料時,直接通過rdb檔案中的快照,將資料恢復。

  • RDB(快照)持久化:儲存某個時間點的全量資料快照

    RDB持久化會在某個特定的間隔儲存那個時間點的全量資料的快照。 RDB配置檔案: redis.conf:

      save 900 1 #在900s內如果有1條資料被寫入,則產生一次快照。
      save 300 10 #在300s內如果有10條資料被寫入,則產生一次快照
      save 60 10000 #在60s內如果有10000條資料被寫入,則產生一次快照
      stop-writes-on-bgsave-error yes 
      #stop-writes-on-bgsave-error :
      如果為yes則表示,當備份程式出錯的時候,
      主程式就停止進行接受新的寫入操作,這樣是為了保護持久化的資料一致性的問題。
    複製程式碼
    • RDB的建立與載入

      SAVE:阻塞Redis的伺服器程式,直到RDB檔案被建立完畢。SAVE命令很少被使用,因為其會阻塞主執行緒來保證快照的寫入,由於Redis是使用一個主執行緒來接收所有客戶端請求,這樣會阻塞所有客戶端請求。
      BGSAVE:該指令會Fork出一個子程式來建立RDB檔案,不阻塞伺服器程式,子程式接收請求並建立RDB快照,父程式繼續接收客戶端的請求。子程式在完成檔案的建立時會向父程式傳送訊號,父程式在接收客戶端請求的過程中,在一定的時間間隔通過輪詢來接收子程式的訊號。我們也可以通過使用lastsave指令來檢視bgsave是否執行成功,lastsave可以返回最後一次執行成功bgsave的時間。

    • 自動化觸發RDB持久化的方式

      1.根據redis.conf配置裡的SAVE m n 定時觸發(實際上使用的是BGSAVE)
      2.主從複製時,主節點自動觸發。
      3.執行Debug Reload
      4.執行Shutdown且沒有開啟AOF持久化。

    • BGSAVE的原理

      啟動
      1.檢查是否存在子程式正在執行AOF或者RDB的持久化任務。如果有則返回false。
      2.呼叫Redis原始碼中的rdbSaveBackground方法,方法中執行fork()產生子程式執行rdb操作。

      rdb原理
      3.關於fork()中的Copy-On-Write
      fork()在linux中建立子程式採用Copy-On-Write(寫時拷貝技術),即如果有多個呼叫者同時要求相同資源(如記憶體或磁碟上的資料儲存),他們會共同獲取相同的指標指向相同的資源,直到某個呼叫者試圖修改資源的內容時,系統才會真正複製一份專用副本給呼叫者,而其它呼叫者所見到的最初的資源仍然保持不變

    • RDB持久化方式的缺點

      1.記憶體資料全量同步,資料量大的狀況下,會由於I/O而嚴重影響效能。
      2.可能會因為Redis宕機而丟失從當前至最近一次快照期間的資料。

  • AOF(Append-Only-File)持久化:儲存寫狀態

    AOF持久化是通過儲存Redis的寫狀態來記錄資料庫的。相對RDB來說,RDB持久化是通過備份資料庫的狀態來記錄資料庫,而AOF持久化是備份資料庫接收到的指令。
    1.AOF記錄除了查詢以外的所有變更資料庫狀態的指令。
    2.以增量的形式追加儲存到AOF檔案中。

  • 開啟AOF持久化

    1.開啟redis.conf配置檔案,將appendonly屬性改為yes。
    2.修改appendfsync屬性,該屬性可以接收三種引數,分別是always,everysec,no,always表示總是即時將緩衝區內容寫入AOF檔案當中,everysec表示每隔一秒將緩衝區內容寫入AOF檔案,no表示將寫入檔案操作交由作業系統決定,一般來說,作業系統考慮效率問題,會等待緩衝區被填滿再將緩衝區資料寫入AOF檔案中。

      appendonly yes
    
      #appendsync always
      appendfsync everysec
      # appendfsync no
    複製程式碼
  • 日誌重寫解決AOF檔案不斷增大的問題

    隨著寫操作的不斷增加,AOF檔案會越來越大。假設遞增一個計數器100次,如果使用RDB持久化方式,我們只要儲存最終結果100即可,而AOF持久化方式需要記錄下這100次遞增操作的指令,而事實上要恢復這條記錄,只需要執行一條命令就行,所以那一百條命令實際可以精簡為一條。Redis支援這樣的功能,在不中斷前臺服務的情況下,可以重寫AOF檔案,同樣使用到了COW(寫時拷貝)。重寫過程如下:
    1.呼叫fork(),建立一個子程式。
    2.子程式把新的AOF寫到一個臨時檔案裡,不依賴原來的AOF檔案。
    3.主程式持續將新的變動同時寫到記憶體和原來的AOF裡。
    4.主程式獲取子程式重寫AOF的完成訊號,往新AOF同步增量變動。
    5.使用新的AOF檔案替換掉舊的AOF檔案。

  • AOF和RDB的優缺點

    RDB優點:全量資料快照,檔案小,恢復快。
    RDB缺點:無法儲存最近一次快照之後的資料。
    AOF優點:可讀性高,適合儲存增量資料,資料不易丟失。
    AOF缺點:檔案體積大,恢復時間長。

  • RDB-AOF混合持久化方式

    redis4.0之後推出了此種持久化方式,RDB作為全量備份,AOF作為增量備份,並且將此種方式作為預設方式使用。
    在上述兩種方式中,RDB方式是將全量資料寫入RDB檔案,這樣寫入的特點是檔案小,恢復快,但無法儲存最近一次快照之後的資料,AOF則將redis指令存入檔案中,這樣又會造成檔案體積大,恢復時間長等弱點。
    RDB-AOF方式下,持久化策略首先將快取中資料以RDB方式全量寫入檔案,再將寫入後新增的資料以AOF的方式追加在RDB資料的後面,在下一次做RDB持久化的時候將AOF的資料重新以RDB的形式寫入檔案。這種方式既可以提高讀寫和恢復效率,也可以減少檔案大小,同時可以保證資料的完整性。在此種策略的持久化過程中,子程式會通過管道從父程式讀取增量資料,在以RDB格式儲存全量資料時,也會通過管道讀取資料,同時不會造成管道阻塞。可以說,在此種方式下的持久化檔案,前半段是RDB格式的全量資料,後半段是AOF格式的增量資料。此種方式是目前較為推薦的一種持久化方式。

Redis資料的恢復

  • RDB和AOF檔案共存情況下的恢復流程

    RDB和AOF共存
    從圖可知,Redis啟動時會先檢查AOF是否存在,如果AOF存在則直接載入AOF,如果不存在AOF,則直接載入RDB檔案。

Pineline

Pipeline和Linux的管道類似,它可以讓Redis批量執行指令。
Redis基於請求/響應模型,單個請求處理需要一一應答。如果需要同時執行大量命令,則每條命令都需要等待上一條命令執行完畢後才能繼續執行,這中間不僅僅多了RTT,還多次使用了系統IO。Pipeline由於可以批量執行指令,所以可以節省多次IO和請求響應往返的時間。但是如果指令之間存在依賴關係,則建議分批傳送指令。

Redis的同步機制

  • 主從同步原理

    Redis一般是使用一個Master節點來進行寫操作,而若干個Slave節點進行讀操作,Master和Slave分別代表了一個個不同的RedisServer例項,另外定期的資料備份操作也是單獨選擇一個Slave去完成,這樣可以最大程度發揮Redis的效能,為的是保證資料的弱一致性最終一致性。另外,Master和Slave的資料不是一定要即時同步的,但是在一段時間後Master和Slave的資料是趨於同步的,這就是最終一致性

    Redis主從同步

    • 全同步過程

      1.Slave傳送sync命令到Master。
      2.Master啟動一個後臺程式,將Redis中的資料快照儲存到檔案中。
      3.Master將儲存資料快照期間接收到的寫命令快取起來。
      4.Master完成寫檔案操作後,將該檔案傳送給Slave。
      5.使用新的AOF檔案替換掉舊的AOF檔案。
      6.Master將這期間收集的增量寫命令傳送給Slave端。
    • 增量同步過程

      1.Master接收到使用者的操作指令,判斷是否需要傳播到Slave。
      2.將操作記錄追加到AOF檔案。
      3.將操作傳播到其它Slave:1.對齊主從庫;2.往響應快取寫入指令。
      4.將快取中的資料傳送給Slave。
  • Redis Sentinel(哨兵)

    主從模式弊端:當Master宕機後,Redis叢集將不能對外提供寫入操作。Redis Sentinel可解決這一問題。
    解決主從同步Master宕機後的主從切換問題
    1.監控:檢查主從伺服器是否執行正常。
    2.提醒:通過API向管理員或者其它應用程式傳送故障通知。
    3.自動故障遷移:主從切換(在Master宕機後,將其中一個Slave轉為Master,其他的Slave從該節點同步資料)。

Redis叢集

  • 原理:如何從海量資料裡快速找到所需?

    • 分片

      按照某種規則去劃分資料,分散儲存在多個節點上。通過將資料分到多個Redis伺服器上,來減輕單個Redis伺服器的壓力。

    • 一致性Hash演演算法

      既然要將資料進行分片,那麼通常的做法就是獲取節點的Hash值,然後根據節點數求模,但這樣的方法有明顯的弊端,當Redis節點數需要動態增加或減少的時候,會造成大量的Key無法被命中。所以Redis中引入了一致性Hash演演算法。該演演算法對2^32 取模,將Hash值空間組成虛擬的圓環,整個圓環按順時針方向組織,每個節點依次為0、1、2...2^32-1,之後將每個伺服器進行Hash運算,確定伺服器在這個Hash環上的地址,確定了伺服器地址後,對資料使用同樣的Hash演演算法,將資料定位到特定的Redis伺服器上。如果定位到的地方沒有Redis伺服器例項,則繼續順時針尋找,找到的第一臺伺服器即該資料最終的伺服器位置。

      一致性Hash演演算法

  • Hash環的資料傾斜問題

    Hash環在伺服器節點很少的時候,容易遇到伺服器節點不均勻的問題,這會造成資料傾斜,資料傾斜指的是被快取的物件大部分集中在Redis叢集的其中一臺或幾臺伺服器上。

    資料傾斜
    如上圖,一致性Hash演演算法運算後的資料大部分被存放在A節點上,而B節點只存放了少量的資料,久而久之A節點將被撐爆。
    針對這一問題,可以引入虛擬節點解決。簡單地說,就是為每一個伺服器節點計算多個Hash,每個計算結果位置都放置一個此伺服器節點,稱為虛擬節點,可以在伺服器IP或者主機名後放置一個編號實現。
    虛擬節點
    例如上圖:將NodeA和NodeB兩個節點分為Node A#1-A#3 NodeB#1-B#3。

結語

這篇準(tou)備(lan)了相當久的時間,因為有些東西總感覺自己拿不準不敢往上寫,差點自閉,就算現在發出來了也感覺有很多地方是需要改動的。如果有同學覺得哪裡寫的不對勁的,評論區或者私聊我...嗯,我不要你覺得,我要我覺得。

本文圖片來自網路,侵刪。

歡迎大家訪問我的個人部落格:Object's Blog