Redis從入門到精通,看這篇文章就夠了

經常使用的 SQL 數據庫的數據都是存在磁盤中的,雖然在數據庫底層也作了對應的緩存來減小數據庫的 IO 壓力。redis

因爲數據庫的緩存通常是針對查詢的內容,並且粒度也比較小,通常只有表中的數據沒有發生變更的時候,數據庫的緩存纔會產生做用。算法

但這並不能減小業務邏輯對數據庫的增刪改操做的 IO 壓力,所以緩存技術應運而生,該技術實現了對熱點數據的高速緩存,能夠大大緩解後端數據庫的壓力。數據庫

主流應用架構後端

ef5d20f8e0e14f3195307a5aa221fc1c


客戶端在對數據庫發起請求時,先到緩存層查看是否有所需的數據,若是緩存層存有客戶端所需的數據,則直接從緩存層返回,不然進行穿透查詢,對數據庫進行查詢。設計模式

若是在數據庫中查詢到該數據,則將該數據回寫到緩存層,以便下次客戶端再次查詢可以直接從緩存層獲取數據。緩存

緩存中間件 Memcache 和 Redis 的區別安全

Memcache 的代碼層相似 Hash,特色以下:服務器

  • 支持簡單數據類型數據結構

  • 不支持數據持久化存儲架構

  • 不支持主從

  • 不支持分片

Redis 特色以下:

  • 數據類型豐富

  • 支持數據磁盤持久化存儲

  • 支持主從

  • 支持分片

爲何 Redis 能這麼快

Redis 的效率很高,官方給出的數據是 100000+QPS,這是由於:

  • Redis 徹底基於內存,絕大部分請求是純粹的內存操做,執行效率高。

  • Redis 使用單進程單線程模型的(K,V)數據庫,將數據存儲在內存中,存取均不會受到硬盤 IO 的限制,所以其執行速度極快。另外單線程也能處理高併發請求,還能夠避免頻繁上下文切換和鎖的競爭,若是想要多核運行也能夠啓動多個實例。

  • 數據結構簡單,對數據操做也簡單,Redis 不使用表,不會強制用戶對各個關係進行關聯,不會有複雜的關係限制,其存儲結構就是鍵值對,相似於 HashMap,HashMap 最大的優勢就是存取的時間複雜度爲 O(1)。

  • Redis 使用多路 I/O 複用模型,爲非阻塞 IO。

注:Redis 採用的 I/O 多路複用函數:epoll/kqueue/evport/select。

選用策略:

  • 因地制宜,優先選擇時間複雜度爲 O(1) 的 I/O 多路複用函數做爲底層實現。

  • 因爲 Select 要遍歷每個 IO,因此其時間複雜度爲 O(n),一般被做爲保底方案。

  • 基於 React 設計模式監聽 I/O 事件。

Redis 的數據類型

String

最基本的數據類型,其值最大可存儲 512M,二進制安全(Redis 的 String 能夠包含任何二進制數據,包含 jpg 對象等)。

66dc525cd7e149428d858bdb2b3d3b4b


注:若是重複寫入 key 相同的鍵值對,後寫入的會將以前寫入的覆蓋。

Hash

String 元素組成的字典,適用於存儲對象。

d18b36dd14b047eca2b2180842de07b1


List

列表,按照 String 元素插入順序排序。其順序爲後進先出。因爲其具備棧的特性,因此能夠實現如「最新消息排行榜」這類的功能。

716d6af7498b41e1a21afa193a18c2de


Set

String 元素組成的無序集合,經過哈希表實現(增刪改查時間複雜度爲 O(1)),不容許重複。

c5229b4d1b8340ed9db785aceb454c1b


另外,當咱們使用 Smembers 遍歷 Set 中的元素時,其順序也是不肯定的,是經過 Hash 運算事後的結果。

Redis 還對集合提供了求交集、並集、差集等操做,能夠實現如同共同關注,共同好友等功能。

Sorted Set

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

deab8c6ff5614eec85260918587d3e2a


更高級的 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 實現分佈式鎖

分佈式鎖

分佈式鎖是控制分佈式系統之間共同訪問共享資源的一種鎖的實現。若是一個系統,或者不一樣系統的不一樣主機之間共享某個資源時,每每須要互斥,來排除干擾,知足數據一致性。

分佈式鎖須要解決的問題以下:

  • 互斥性:任意時刻只有一個客戶端獲取到鎖,不能有兩個客戶端同時獲取到鎖。

  • 安全性:鎖只能被持有該鎖的客戶端刪除,不能由其餘客戶端刪除。

  • 死鎖:獲取鎖的客戶端由於某些緣由而宕機繼而沒法釋放鎖,其餘客戶端再也沒法獲取鎖而致使死鎖,此時須要有特殊機制來避免死鎖。

  • 容錯:當各個節點,如某個 Redis 節點宕機的時候,客戶端仍然可以獲取鎖或釋放鎖。

如何使用 Redis 實現分佈式鎖

使用 SETNX 實現,SETNX key value:若是 Key 不存在,則建立並賦值。

該命令時間複雜度爲 O(1),若是設置成功,則返回 1,不然返回 0。

5f6929e921bd47bca283b277373dc5eb


因爲 SETNX 指令操做簡單,且是原子性的,因此初期的時候常常被人們做爲分佈式鎖,咱們在應用的時候,能夠在某個共享資源區以前先使用 SETNX 指令,查看是否設置成功。

若是設置成功則說明前方沒有客戶端正在訪問該資源,若是設置失敗則說明有客戶端正在訪問該資源,那麼當前客戶端就須要等待。

可是若是真的這麼作,就會存在一個問題,由於 SETNX 是長久存在的,因此假設一個客戶端正在訪問資源,而且上鎖,那麼當這個客戶端結束訪問時,該鎖依舊存在,後來者也沒法成功獲取鎖,這個該如何解決呢?

因爲 SETNX 並不支持傳入 EXPIRE 參數,因此咱們能夠直接使用 EXPIRE 指令來對特定的 Key 來設置過時時間。

用法:

EXPIRE key seconds

985256127c5d4531aa064ac7303a5433


程序:

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

這段程序存在的問題:假設程序運行到第二行出現異常,那麼程序來不及設置過時時間就結束了,則 Key 會一直存在,等同於鎖一直被持有沒法釋放。

出現此問題的根本緣由爲:原子性得不到知足。

解決:從 Redis 2.6.12 版本開始,咱們就可使用 Set 操做,將 SETNX 和 EXPIRE 融合在一塊兒執行,具體作法以下:

  • EX second:設置鍵的過時時間爲 Second 秒。

  • PX millisecond:設置鍵的過時時間爲 MilliSecond 毫秒。

  • NX:只在鍵不存在時,纔對鍵進行設置操做。

  • XX:只在鍵已經存在時,纔對鍵進行設置操做。

SET KEY value [EX seconds] [PX milliseconds] [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 消費消息。

22d9d4f430814de0aeb7156dc6834578


此時咱們能夠看到,該隊列是使用 Rpush 生產隊列,使用 LPOP 消費隊列。

在這個生產者-消費者隊列裏,當 LPOP 沒有消息時,證實該隊列中沒有元素,而且生產者尚未來得及生產新的數據。

缺點:LPOP 不會等待隊列中有值以後再消費,而是直接進行消費。

彌補:能夠經過在應用層引入 Sleep 機制去調用 LPOP 重試。

②使用 BLPOP key [key…] timeout

BLPOP key [key …] timeout:阻塞直到隊列有消息或者超時。

7753511f1702459e92e5c2663e08b541


3f864102d9a64b739fe574cd500c8a76


4925d1d63e5249aa8a1d4e1634a7c085


缺點:按照此種方法,咱們生產後的數據只能提供給各個單一消費者消費。可否實現生產一次就能讓多個消費者消費呢?

③Pub/Sub:主題訂閱者模式

發送者(Pub)發送消息,訂閱者(Sub)接收消息。訂閱者能夠訂閱任意數量的頻道。

c9705b246e4b4186821901c6286dfe5a


Pub/Sub模式的缺點:消息的發佈是無狀態的,沒法保證可達。對於發佈者來講,消息是「即發即失」的。

此時若是某個消費者在生產者發佈消息時下線,從新上線以後,是沒法接收該消息的,要解決該問題須要使用專業的消息隊列,如 Kafka…此處再也不贅述。

Redis 持久化

什麼是持久化

持久化,即將數據持久存儲,而不因斷電或其餘各類複雜外部環境影響數據的完整性。

因爲 Redis 將數據存儲在內存而不是磁盤中,因此內存一旦斷電,Redis 中存儲的數據也隨即消失,這每每是用戶不指望的,因此 Redis 有持久化機制來保證數據的安全性。

Redis 如何作持久化

Redis 目前有兩種持久化方式,即 RDB 和 AOF,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 持久化的方式

自動化觸發RDB持久化的方式以下:

  • 根據 redis.conf 配置裏的 SAVE m n 定時觸發(實際上使用的是 BGSAVE)。

  • 主從複製時,主節點自動觸發。

  • 執行 Debug Reload。

  • 執行 Shutdown 且沒有開啓 AOF 持久化。

③BGSAVE 的原理

be419ccf681d494499da9a7bdeba8a89


啓動:

  • 檢查是否存在子進程正在執行 AOF 或者 RDB 的持久化任務。若是有則返回 false。

  • 調用 Redis 源碼中的 rdbSaveBackground 方法,方法中執行 fork() 產生子進程執行 RDB 操做。

  • 關於 fork() 中的 Copy-On-Write。

fork() 在 Linux 中建立子進程採用 Copy-On-Write(寫時拷貝技術),即若是有多個調用者同時要求相同資源(如內存或磁盤上的數據存儲)。

他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統纔會真正複製一份專用副本給調用者,而其餘調用者所見到的最初的資源仍然保持不變。

④RDB 持久化方式的缺點

RDB 持久化方式的缺點以下:

  • 內存數據全量同步,數據量大的情況下,會因爲 I/O 而嚴重影響性能。

  • 可能會由於 Redis 宕機而丟失從當前至最近一次快照期間的數據。

AOF 持久化:保存寫狀態

AOF 持久化是經過保存 Redis 的寫狀態來記錄數據庫的。

相對 RDB 來講,RDB 持久化是經過備份數據庫的狀態來記錄數據庫,而 AOF 持久化是備份數據庫接收到的指令:

  • AOF 記錄除了查詢之外的全部變動數據庫狀態的指令。

  • 以增量的形式追加保存到 AOF 文件中。

開啓 AOF 持久化

①打開 redis.conf 配置文件,將 appendonly 屬性改成 yes。

②修改 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(寫時拷貝)。

重寫過程以下:

  • 調用 fork(),建立一個子進程。

  • 子進程把新的 AOF 寫到一個臨時文件裏,不依賴原來的 AOF 文件。

  • 主進程持續將新的變更同時寫到內存和原來的 AOF 裏。

  • 主進程獲取子進程重寫 AOF 的完成信號,往新 AOF 同步增量變更。

  • 使用新的 AOF 文件替換掉舊的 AOF 文件。

AOF 和 RDB 的優缺點

AOF 和 RDB 的優缺點以下:

  • RDB 優勢:全量數據快照,文件小,恢復快。

  • RDB 缺點:沒法保存最近一次快照以後的數據。

  • AOF 優勢:可讀性高,適合保存增量數據,數據不易丟失。

  • AOF 缺點:文件體積大,恢復時間長。

RDB-AOF 混合持久化方式

Redis 4.0 以後推出了此種持久化方式,RDB 做爲全量備份,AOF 做爲增量備份,而且將此種方式做爲默認方式使用。

在上述兩種方式中,RDB 方式是將全量數據寫入 RDB 文件,這樣寫入的特色是文件小,恢復快,但沒法保存最近一次快照以後的數據,AOF 則將 Redis 指令存入文件中,這樣又會形成文件體積大,恢復時間長等弱點。

在 RDB-AOF 方式下,持久化策略首先將緩存中數據以 RDB 方式全量寫入文件,再將寫入後新增的數據以 AOF 的方式追加在 RDB 數據的後面,在下一次作 RDB 持久化的時候將 AOF 的數據從新以 RDB 的形式寫入文件。

這種方式既能夠提升讀寫和恢復效率,也能夠減小文件大小,同時能夠保證數據的完整性。

在此種策略的持久化過程當中,子進程會經過管道從父進程讀取增量數據,在以 RDB 格式保存全量數據時,也會經過管道讀取數據,同時不會形成管道阻塞。

能夠說,在此種方式下的持久化文件,前半段是 RDB 格式的全量數據,後半段是 AOF 格式的增量數據。此種方式是目前較爲推薦的一種持久化方式。

Redis 數據的恢復

RDB 和 AOF 文件共存狀況下的恢復流程以下圖:

569e3ffa97b54fdcb423748b0e6e0ea0


從圖可知,Redis 啓動時會先檢查 AOF 是否存在,若是 AOF 存在則直接加載 AOF,若是不存在 AOF,則直接加載 RDB 文件。

Pineline

Pipeline 和 Linux 的管道相似,它可讓 Redis 批量執行指令。

Redis 基於請求/響應模型,單個請求處理須要一一應答。若是須要同時執行大量命令,則每條命令都須要等待上一條命令執行完畢後才能繼續執行,這中間不只僅多了 RTT,還屢次使用了系統 IO。

Pipeline 因爲能夠批量執行指令,因此能夠節省屢次 IO 和請求響應往返的時間。可是若是指令之間存在依賴關係,則建議分批發送指令。

Redis 的同步機制

主從同步原理

Redis 通常是使用一個 Master 節點來進行寫操做,而若干個 Slave 節點進行讀操做,Master 和 Slave 分別表明了一個個不一樣的 Redis Server 實例。

另外按期的數據備份操做也是單獨選擇一個 Slave 去完成,這樣能夠最大程度發揮 Redis 的性能,爲的是保證數據的弱一致性和最終一致性。

另外,Master 和 Slave 的數據不是必定要即時同步的,可是在一段時間後 Master 和 Slave 的數據是趨於同步的,這就是最終一致性。

0e345f3d68bd4c18b509581c754ba08d


全同步過程以下:

  • Slave 發送 Sync 命令到 Master。

  • Master 啓動一個後臺進程,將 Redis 中的數據快照保存到文件中。

  • Master 將保存數據快照期間接收到的寫命令緩存起來。

  • Master 完成寫文件操做後,將該文件發送給 Slave。

  • 使用新的 AOF 文件替換掉舊的 AOF 文件。

  • Master 將這期間收集的增量寫命令發送給 Slave 端。

增量同步過程以下:

  • Master 接收到用戶的操做指令,判斷是否須要傳播到 Slave。

  • 將操做記錄追加到 AOF 文件。

  • 將操做傳播到其餘 Slave:對齊主從庫;往響應緩存寫入指令。

  • 將緩存中的數據發送給 Slave。

Redis Sentinel(哨兵)

主從模式弊端:當 Master 宕機後,Redis 集羣將不能對外提供寫入操做。Redis Sentinel 可解決這一問題。

解決主從同步 Master 宕機後的主從切換問題:

  • 監控:檢查主從服務器是否運行正常。

  • 提醒:經過 API 向管理員或者其它應用程序發送故障通知。

  • 自動故障遷移:主從切換(在 Master 宕機後,將其中一個 Slave 轉爲 Master,其餘的 Slave 從該節點同步數據)。

Redis 集羣

如何從海量數據裏快速找到所需?

①分片

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

②一致性 Hash 算法

既然要將數據進行分片,那麼一般的作法就是獲取節點的 Hash 值,而後根據節點數求模。

但這樣的方法有明顯的弊端,當 Redis 節點數須要動態增長或減小的時候,會形成大量的 Key 沒法被命中。因此 Redis 中引入了一致性 Hash 算法。

該算法對 2^32 取模,將 Hash 值空間組成虛擬的圓環,整個圓環按順時針方向組織,每一個節點依次爲 0、一、2…2^32-1。

以後將每一個服務器進行 Hash 運算,肯定服務器在這個 Hash 環上的地址,肯定了服務器地址後,對數據使用一樣的 Hash 算法,將數據定位到特定的 Redis 服務器上。

若是定位到的地方沒有 Redis 服務器實例,則繼續順時針尋找,找到的第一臺服務器即該數據最終的服務器位置。

49501433bd87473f84de8cbfdca2ad92


③Hash 環的數據傾斜問題

Hash 環在服務器節點不多的時候,容易遇到服務器節點不均勻的問題,這會形成數據傾斜,數據傾斜指的是被緩存的對象大部分集中在 Redis 集羣的其中一臺或幾臺服務器上。

40ec8a9fe7174058ab59a20013a9148c


如上圖,一致性 Hash 算法運算後的數據大部分被存放在 A 節點上,而 B 節點只存放了少許的數據,長此以往 A 節點將被撐爆。

針對這一問題,能夠引入虛擬節點解決。簡單地說,就是爲每個服務器節點計算多個 Hash,每一個計算結果位置都放置一個此服務器節點,稱爲虛擬節點,能夠在服務器 IP 或者主機名後放置一個編號實現。

b63bd3419bc847f68fafb3e64ffc41dd


例如上圖:將 NodeA 和 NodeB 兩個節點分爲 Node A#1-A#3,NodeB#1-B#3。

讀者福利

點擊領取

image.pngimage.png

相關文章
相關標籤/搜索