Java開發想要Redis從入門到精通,不看看這篇文章怎麼行?

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

由於數據庫的緩存一般是針對查詢的內容,而且粒度也比較小,一般只有表中的數據沒有發生變動的時候,數據庫的緩存纔會產生作用。

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

主流應用架構

93b837b339b84024b1794fba1c12d8b7


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

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

緩存中間件 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 對象等)。

06ebdcfd273d47009a48805a68ea1676


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

Hash

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

2df5a24c3a4d48a3a17f55386550d6ac


List

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

6ed885bef2884efa8e93ead90c80591e


Set

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

0343b56c1ef64620baeeff304ea52bca


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

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


Sorted Set

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

f1355998f90143b1a0bdabe8f88ce814


更高級的 Redis 類型

用於計數的 HyperLogLog、用於支持存儲地理位置信息的 Geo。

從海量 Key 裏查詢出某一個固定前綴的 Key

假設 Redis 中有十億條 Key,如何從這麼多 Key 中找到固定前綴的 Key?

方法 1:使用 Keys [pattern]:查找所有符合給定模式 Pattern 的 Key

使用 Keys [pattern] 指令可以找到所有符合 Pattern 條件的 Key,但是 Keys 會一次性返回所有符合條件的 Key,所以會造成 Redis 的卡頓。

假設 Redis 此時正在生產環境下,使用該命令就會造成隱患,另外如果一次性返回所有 Key,對內存的消耗在某些條件下也是巨大的。

例:

9b95084ca34343ccb01fec75059daf3e


方法 2:使用 SCAN cursor [MATCH pattern] [COUNT count]

注:

  • cursor:遊標

  • MATCH pattern:查詢 Key 的條件

  • Count:返回的條數

SCAN 是一個基於遊標的迭代器,需要基於上一次的遊標延續之前的迭代過程。

SCAN 以 0 作爲遊標,開始一次新的迭代,直到命令返回遊標 0 完成一次遍歷。

此命令並不保證每次執行都返回某個給定數量的元素,甚至會返回 0 個元素,但只要遊標不是 0,程序都不會認爲 SCAN 命令結束,但是返回的元素數量大概率符合 Count 參數。另外,SCAN 支持模糊查詢。

例:

97da14c5fce942a6bb3ad7ed7669d79a


如何通過 Redis 實現分佈式鎖

分佈式鎖

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

分佈式鎖需要解決的問題如下:

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

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

  • 死鎖:獲取鎖的客戶端因爲某些原因而宕機繼而無法釋放鎖,其他客戶端再也無法獲取鎖而導致死鎖,此時需要有特殊機制來避免死鎖。

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

如何使用 Redis 實現分佈式鎖

使用 SETNX 實現,SETNX key value:如果 Key 不存在,則創建並賦值。

該命令時間複雜度爲 O(1),如果設置成功,則返回 1,否則返回 0。

2f018d75aa894cefba6de6f24cc6b8d3


由於 SETNX 指令操作簡單,且是原子性的,所以初期的時候經常被人們作爲分佈式鎖,我們在應用的時候,可以在某個共享資源區之前先使用 SETNX 指令,查看是否設置成功。

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

但是如果真的這麼做,就會存在一個問題,因爲 SETNX 是長久存在的,所以假設一個客戶端正在訪問資源,並且上鎖,那麼當這個客戶端結束訪問時,該鎖依舊存在,後來者也無法成功獲取鎖,這個該如何解決呢?

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

用法:

bd5e9fd943144e9db2089646f3ae57f7


7338ec5040ee4958935ae789a832d0e1


程序:

0e59c21307954438b19214b42f772eff


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

出現此問題的根本原因爲:原子性得不到滿足。

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

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

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

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

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

62d120f92e5b487c8f178e174e8f797c


注:SET 操作成功完成時纔會返回 OK,否則返回 nil。

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

e9ad2053b6b3408494f6814efb8914db


如何實現異步隊列

①使用 Redis 中的 List 作爲隊列

使用上文所說的 Redis 的數據結構中的 List 作爲隊列 Rpush 生產消息,LPOP 消費消息。

0ca6fe72caa14919a05aa3fe660bba83


此時我們可以看到,該隊列是使用 Rpush 生產隊列,使用 LPOP 消費隊列。

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

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

彌補:可以通過在應用層引入 Sleep 機制去調用 LPOP 重試。

②使用 BLPOP key [key…] timeout

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

1645c8c632f9410c92f85a3766a637b2


97a348a329e34fa4a6843f30f8b096d7


fa08da92fd0343d9aa612338b282cc9f


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

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

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

2631201341024944a3e6c47d47eb835f


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

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

Redis 持久化

什麼是持久化

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

由於 Redis 將數據存儲在內存而不是磁盤中,所以內存一旦斷電,Redis 中存儲的數據也隨即消失,這往往是用戶不期望的,所以 Redis 有持久化機制來保證數據的安全性。

Redis 如何做持久化

Redis 目前有兩種持久化方式,即 RDB 和 AOF,RDB 是通過保存某個時間點的全量數據快照實現數據的持久化,當恢復數據時,直接通過 RDB 文件中的快照,將數據恢復。

RDB(快照)持久化

RDB持久化會在某個特定的間隔保存那個時間點的全量數據的快照。

RDB 配置文件,redis.conf:

c99c1af1916e4e6bb2ce6b50c2f008fd


①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 的原理

25b55e1c70724debb8f336dab76f25cd


啓動:

  • 檢查是否存在子進程正在執行 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 文件中。

846637e7c7fd4758a962b5b37003eae5



日誌重寫解決 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 文件共存情況下的恢復流程如下圖:

3b8933936454425cbc1b37c3173bef84


從圖可知,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 的數據是趨於同步的,這就是最終一致性。

f36b659f04434a308872785517f0eaf6


全同步過程如下:

  • 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、1、2…2^32-1。

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

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

594b0589bbc64e18aed33e5fb88324f9


③Hash 環的數據傾斜問題

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

99ca3287ba1c430b9d9c60401a2a4e17


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

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

e0219c211d0344f4a515e9bf97f5a144


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

歡迎工作一到五年的Java工程師朋友們加入我的個人粉絲羣(Java技術劍:807987079)羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)

合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章