Redis緩存何以一枝獨秀?——從百變應用場景與熱門面試題中感受下Redis的核心特性與使用注意點

語言: CN / TW / HK

theme: healer-readable

本文為稀土掘金技術社區首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!


大家好,又見面了。

作為《深入理解緩存原理與實戰設計》系列專欄,在前面的文章中,我們一起領略了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來作為分佈式鎖,可以有幾方面理由:

  1. redis足夠的快
  2. redis提供了setnx + expire的機制,完全契合分佈式鎖的實現要點
  3. Redisson客户端的流行,使得基於redis的分佈式鎖更加簡單

  4. 數據庫扛壓層

藉助redis超高的處理性能,經常會被放置在數據庫的前面,用於數據扛壓場景使用。比如各種秒殺場景,可以將數據庫中的庫存信息緩存到redis中,然後利用redis來抗住秒殺期間洪水般的大併發量請求。

  • 登錄驗證碼存儲

這個場景也很常見,比如用户發送的短信驗證碼,一般都會要求5分鐘內有效。這種情況下,可以將驗證碼信息存儲在redis中並設定5分鐘後自動過期。這樣的話就可以實現超時失效的功能,而無需業務層面去維護過期信息。

  • 全局ID生成&全侷限流

在分佈式系統中,Redis作為一個可以被所有節點訪問的集中節點,加上其具備的incrby原子命令,使得在多個場景下發揮價值:

  1. 將其用作全局唯一ID的生成,以保證各個節點之間生成的唯一ID不會衝突。

  2. incrby可以實現全局請求量的統計計數,結合expire一起可以實現定時重置計數器,進而實現限流能力

  3. 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多路複用是個什麼概念?它和BIONIO之間有什麼異同? 諸如此類的問題,都可以進一步的去展開考察。

  • 當前計算機一般都是多核CPU,用單線程去執行的話,相當於其它幾個核就浪費了,那有什麼方式可以將其餘的幾個核也利用起來麼? 答案其實也不難,在一台機器上同時去部署多個Redis進程,組成個集羣,就可以啦。

Q2. 如果我想要查詢一下生產環境的Redis中有多少以“User_”開頭的記錄數量,可以怎麼做?

這個問題其實是有一點小陷阱的。查找以指定前綴開頭的記錄,首先很多同學想到的就是keys命令,但問題中有個約束是在生產環境中執行。所以這個問題看似簡單,其實需要結合如下幾點來綜合考慮:

  1. 通常情況下,生產環境中的數據量是非常大的、且請求併發量會比較高;
  2. Redis的keys命令是一個耗時操作,複雜度O(n),數據量越大執行速度越慢;
  3. Redis的命令執行是單線程執行的。

基於上述幾點因素,如果在數據量較大的生產環境去執行keys命令將會導致執行耗時特別長,而由於Redis是單線程執行命令,就會導致其餘請求命令被阻塞無法執行,這樣在一個高併發集羣內,很容易造成集羣內請求的大面積阻塞,影響系統的整體穩定性。

那麼keys命令不可以用,有什麼替代方案呢?可以使用scan命令。

Q3. 假如有一批機器,內存都比較小(單機內存小於整體待緩存數據量),用來搭建個Redis做熱點數據緩存扛壓以降低數據庫的請求壓力。如果你來做的話,會有哪些應對思路呢?

這個問題就比較開放,而且答案也不唯一,考核的點也比較綜合。

首先來分析下題目,從題幹描述中可以捕捉到幾個信息,以及對應的關聯知識點:

  1. 單機內存小於整體數據量,所以想要將所有數據全量加載到單機內存裏面是不可行的;
  2. 使用Redis的用途是扛壓來降低數據庫訪問壓力的,也就是允許部分請求穿透Redis打到數據庫上的,所以可以考慮將有限內存用來存放熱點數據,扛住大部分的流量;
  3. 題目説有一批機器,就是説機器的數量不止一台,所以可以考慮構建集羣的方式,擴展Redis集羣總內存大小,這樣以集羣的力量來緩存全部的數據量。

所以説這個題目裏面其實涉及到了兩個考點:

  1. 熱點數據的概念、也即Redis的數據淘汰策略。
  2. 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

我是悟道,聊技術、又不僅僅聊技術~

如果覺得有用,請點贊 + 關注讓我感受到您的支持。也可以關注下我的公眾號【架構悟道】,獲取更及時的更新。

期待與你一起探討,一起成長為更好的自己。