大家好,又見面了。
本文是筆者作為掘金技術社群簽約作者的身份輸出的快取專欄系列內容,將會透過系列專題,講清楚快取的方方面面。如果感興趣,歡迎關注以獲取後續更新。
作為《深入理解快取原理與實戰設計》系列專欄,在前面的文章中,我們一起領略了Guava Cache、Caffeine、Ehcache等優秀的本地JVM級別本地快取框架的特性、原理與具體的使用方法。除卻本地快取之外,在當前分散式、微服務等架構盛行的時代,本地快取明顯無法滿足大型系統中的各種快取訴求,比如前面文章中反覆提及的快取漂移問題、以及單機快取無法逾越的記憶體容量瓶頸。作為應對之法,集中式快取被廣泛的使用在各中分散式系統中,而使用最廣泛的莫過於大家耳熟能詳的Redis
了。
提到Redis,大家應該都不會陌生,至少應該是有聽過這個名字。在中大型分散式系統中,Redis似乎成了一種標配,而說到集中快取,很多人腦海中第一閃過的也是Redis。Redis
是一個基於記憶體的非關係型資料庫(NoSQL),主要是儲存key-value型別的鍵值對資料,而value則支援多種不同的型別。由於其強悍的效能表現以及完善的可靠性與叢集擴充套件機制,使其俘獲了眾多開發人員的青睞,成為了高併發系統的制勝法寶。接下來的幾篇文章中呢,我們就一起聊一聊與Redis有關的內容,探討下Redis在集中式快取領域一枝獨秀的秘訣。
Redis的各種資料型別
作為快取元件,Redis的資料結構整體而言就是key-value
型別的鍵值對,但是Redis對於value型別的支援還是比較豐富的,提供了5種不同的資料結構,可以滿足大部分場景的使用訴求。
對幾種型別的結構特點與使用注意點梳理彙總如下:
型別 | 說明 | 支援功能 |
---|---|---|
string | 普通字串 | 字串的基礎增刪改查能力,如果是整數或者浮點數,還支援自增自減能力。 |
list | 連結串列內容,每個元素都是一個獨立的字串,內容可以相同 | 基礎增刪改查能力,從連結串列兩端插入或者彈出元素,按照下標獲取指定元素列表等等 |
set | 無序集合,每個元素都是一個獨立字串,元素之間不允許重複 | 基礎增刪改查能力,判斷元素是否存在,隨機獲取元素等等 |
hash | 無序的key-value鍵值對集合 | 基礎增刪改查能力,獲取所有的鍵值對 |
zset | 可以理解為一種比較特殊的hash結構,含有member和score兩個概念,對應到hash型別上分別是key與value的關係,其區別點在在於score是固定的double型別的value | 基礎增刪改查能力,支援根據score排序並獲取指定的排序個數的元素列表 |
實際的使用中,也會根據各自型別不同的特點,用來實現不同的業務訴求。
舉個例子:
一個系統內的通知公告檢視功能,可以將公告ID作為key,然後這邊通知公告的閱讀量作為score,在redis中儲存為zset型別,然後每次讀取詳情操作的都累加更新下對應的score值,這樣的話,就可以根據score進行降序排列,拉取到熱門新聞公告的排行榜。
Redis的百變應用場景
基於Redis提供的基礎能力,在專案中不同場景都有被廣泛的使用,下面列舉幾個常見的使用場景。
- 分散式鎖
在分散式系統裡面經常會需要用到分散式鎖,實現分散式鎖的方式有很多種,其中使用的比較廣泛的一種策略,就是基於Redis來實現的。之所以採用Redis來作為分散式鎖,可以有幾方面理由:
- redis足夠的快
- redis提供了
setnx + expire
的機制,完全契合分散式鎖的實現要點 Redisson
客戶端的流行,使得基於redis的分散式鎖更加簡單
- 資料庫扛壓層
藉助redis超高的處理效能,經常會被放置在資料庫的前面,用於資料扛壓場景使用。比如各種秒殺場景,可以將資料庫中的庫存資訊快取到redis中,然後利用redis來抗住秒殺期間洪水般的大併發量請求。
- 登入驗證碼儲存
這個場景也很常見,比如使用者傳送的簡訊驗證碼,一般都會要求5分鐘內有效。這種情況下,可以將驗證碼資訊儲存在redis中並設定5分鐘後自動過期。這樣的話就可以實現超時失效的功能,而無需業務層面去維護過期資訊。
- 全域性ID生成&全侷限流
在分散式系統中,Redis作為一個可以被所有節點訪問的集中節點,加上其具備的incrby
原子命令,使得在多個場景下發揮價值:
-
將其用作全域性唯一ID的生成,以保證各個節點之間生成的唯一ID不會衝突。
-
incrby可以實現全域性請求量的統計計數,結合expire一起可以實現定時重置計數器,進而實現限流能力。
- bitmap方式儲存每日簽到資料
其實,Redis還支援點陣圖(Bitmap
)格式進行資料儲存。前面我們說Redis支援五種資料結構裡面並沒有看到Bitmap型別的身影,其實Redis的bitmap資料最終儲存的是string型別,但是Redis為Bitmap操作提供了配套的操作介面,比如setbit
命令。
點陣圖的存在就是為了服務於海量資料的儲存場景的,比如系統裡面有10億使用者,現在需要記錄每個人每天的簽到情況,每天10億資料量,如果用普通String型別儲存,每天10億條資料量,時間一久任何的Redis也扛不住。而基於bitmap的方式儲存,則可以極大的降低整體資料量。關於redis的bitmap操作與使用,後面文章會展開闡述。
- 熱門榜單生成
基於Redis的zset
資料結構,可以將熱門值作為score進行儲存,這樣可以根據需要,按照score進行排序並拉取榜單資料。
後端面試中的常客
這篇文章中,我們改變下以往的文章行文敘事風格。我們先不直接切入到Redis的具體特性或功能點的實現原理與使用層面,而是先從面試場景作為切入口,透過幾個面試問題,來感受下Redis整體的“魅力”、引出Redis所具備的核心特性與常見使用注意事項。
因為Redis在專案中的廣泛使用,也讓其成為了後端面試中的熱門嘉賓。很多小夥伴應該在面試中都被問過與Redis有關的問題吧?當然有很多的八股文背誦一下就可以應付很多簡單的面試場景,但筆者作為面試官一般不太會直接去問八股文問題,經常會將問題稍作包裝之後再去問。
下面舉幾個例子。
Q1. 很多人都說Redis處理快是因為它是單執行緒的,Redis程式中真的只有一個執行緒嗎?為什麼常規專案中為了提升併發量都會採用執行緒池等方式來多執行緒處理,而Redis卻反其道而行之呢?
很多的面試八股文中都會提到說Redis是單執行緒的,這個說法其實不夠嚴謹,因為Redis中並非是只有一個執行緒,整個程式中還有一些額外的執行緒負責做一些輔助的其他事務,比如管理與客戶端的連線,比如佇列中訊息的維護等等。
Redis整體基於一種多路複用的機制來實現請求的接收與分配處理。整體簡化後的處理邏輯如下圖所示。
所以說,其實Redis僅僅是採用單執行緒來負責執行命令請求處理,而非整個Redis就是一個單執行緒的。回到最初的問題,為什麼Redis選擇採用單執行緒的方式來執行命令。在多執行緒程式設計的時候面臨問題主要有:
- 併發執行緒安全問題, 需要保證操作的先後順序,需要保證同一時刻只能有1個執行緒對某個物件進行寫操作 —— 需要構建完備的同步保護機制,會對整體效能造成影響。
- 多執行緒維護的系統額外開銷 —— CPU需要不停的在多個執行緒之間進行切換,由此會帶來一系列的額外開銷。
而由於Redis是一種key-value模型的資料結構模式,比如很多查詢操作都是O(1)
的時間複雜度,其操作執行速度非常快,所以這種情況下,結合I/O多路複用
模型一起,使用單執行緒的方式執行命令,反而可以達到比多執行緒更加優異的表現。
問題可以進一步引申,可以繼續聊一些其他問題。比如:
-
既然Redis是單執行緒的,那使用的時候有什麼需要注意的事項嗎?
不能執行耗時操作,會阻塞其餘請求命令的執行。 -
I/O多路複用是個什麼概念?它和BIO、NIO之間有什麼異同?
諸如此類的問題,都可以進一步的去展開考察。 -
當前計算機一般都是多核CPU,用單執行緒去執行的話,相當於其它幾個核就浪費了,那有什麼方式可以將其餘的幾個核也利用起來麼?
答案其實也不難,在一臺機器上同時去部署多個Redis程式,組成個叢集,就可以啦。
Q2. 如果我想要查詢一下生產環境的Redis中有多少以“User_”開頭的記錄數量,可以怎麼做?
這個問題其實是有一點小陷阱的。查詢以指定字首開頭的記錄,首先很多同學想到的就是keys
命令,但問題中有個約束是在生產環境中執行。所以這個問題看似簡單,其實需要結合如下幾點來綜合考慮:
- 通常情況下,生產環境中的資料量是非常大的、且請求併發量會比較高;
- Redis的
keys
命令是一個耗時操作,複雜度O(n)
,資料量越大執行速度越慢; - Redis的命令執行是單執行緒執行的。
基於上述幾點因素,如果在資料量較大的生產環境去執行keys
命令將會導致執行耗時特別長,而由於Redis是單執行緒執行命令,就會導致其餘請求命令被阻塞無法執行,這樣在一個高併發叢集內,很容易造成叢集內請求的大面積阻塞,影響系統的整體穩定性。
那麼keys命令不可以用,有什麼替代方案呢?可以使用scan
命令。
Q3. 假如有一批機器,記憶體都比較小(單機記憶體小於整體待快取資料量),用來搭建個Redis做熱點資料快取扛壓以降低資料庫的請求壓力。如果你來做的話,會有哪些應對思路呢?
這個問題就比較開放,而且答案也不唯一,考核的點也比較綜合。
首先來分析下題目,從題幹描述中可以捕捉到幾個資訊,以及對應的關聯知識點:
- 單機記憶體小於整體資料量,所以想要將所有資料全量載入到單機記憶體裡面是不可行的;
- 使用Redis的用途是扛壓來降低資料庫訪問壓力的,也就是允許部分請求穿透Redis打到資料庫上的,所以可以考慮將有限記憶體用來存放
熱點資料
,扛住大部分的流量; - 題目說有一批機器,就是說機器的數量不止一臺,所以可以考慮構建
叢集
的方式,擴充套件Redis叢集總記憶體大小,這樣以叢集的力量來快取全部的資料量。
所以說這個題目裡面其實涉及到了兩個考點:
- 熱點資料的概念、也即Redis的資料淘汰策略。
- Redis叢集擴充套件的相關概念。
更進一步,又可以引申出很多其它細節問題,比如:
-
Redis中的資料淘汰策略有哪些?
no-enviction、volatile-lru、volatile-ttl、volatile-random、allkeys-lru、allkeys-random -
Redis的資料淘汰策略與資料過期有啥區別?
資料過期是達到了設定的過期時間之後使資料不可用,而資料淘汰策略主要是在容量滿之後採取的被動應對策略。 -
Redis叢集中是如何決定一個記錄應該儲存在哪個節點上的?
關於一致性Hash相關的內容,以及如何解決資料傾斜問題
、節點擴容對快取命中情況的影響等等。
回頭看下,是不是其中蘊含的內容還是蠻多的?
這裡我們以面試場景中會被問及的幾個問題作為切入點,大概聊了下與Redis有關的一系列內容。當然這裡介紹的都比較淺顯,甚至只是列了下相關的知識點,主要是先讓大家先感受下Redis所包含與涉及的相關知識點。在後續的文章中,我們將逐步逐個地去剖析與介紹。
小結回顧
好啦,作為redis部分的第一篇內容,我們只是簡單的聊了下Redis
的基礎概念以及主要的特性介紹,同時透過幾個實際的面試題演示了下Redis整體內容的“博大精深”。而關於Redis的更多細化方向的展開闡述,我們將會在後續文章中逐步介紹。那麼你對Redis如何看呢?歡迎評論區一起交流下,期待和各位小夥伴們一起切磋、共同成長。
? 補充說明1 :
本文屬於《深入理解快取原理與實戰設計》系列專欄的內容之一。該專欄圍繞快取這個宏大命題進行展開闡述,全方位、系統性地深度剖析各種快取實現策略與原理、以及快取的各種用法、各種問題應對策略,並一起探討下快取設計的哲學。
如果有興趣,也歡迎關注此專欄。
? 補充說明2 :
- 關於本文中涉及的演示程式碼的完整示例,我已經整理並提交到github中,如果您有需要,可以自取:https://github.com/veezean/JavaBasicSkills
我是悟道,聊技術、又不僅僅聊技術~
如果覺得有用,請點贊 + 關注讓我感受到您的支援。也可以關注下我的公眾號【架構悟道】,獲取更及時的更新。
期待與你一起探討,一起成長為更好的自己。