聊一聊作為高並行系統基石之一的快取,會用很簡單,用好才是技術活

2022-10-26 18:01:43

大家好,又見面了。


本文是筆者作為掘金技術社群簽約作者的身份輸出的快取專欄系列內容,將會通過系列專題,講清楚快取的方方面面。如果感興趣,歡迎關注以獲取後續更新。


在伺服器端開發中,快取常常被當做系統效能扛壓的不二之選。在實施方案上,快取使用策略雖有一定普適性,卻也並非完全絕對,需要結合實際的專案訴求與場景進行綜合權衡與考量,進而得出符合自己專案的最佳實踐。

快取使用的演進

現有這麼一個系統:

一個互動論壇系統,使用者登入系統之後,可以在論壇上檢視貼文列表、檢視貼文詳情、發表貼文、評論貼文、為貼文點贊等操作。

系統中所有的設定資料與業務資料均儲存在資料庫中。隨著業務的發展,註冊使用者量越來越多,然後整個系統的響應速度也越來越慢,使用者體驗越來越差,使用者逐漸出現流失。

本地快取的牛刀小試

為了挽救這一局面,開發人員需要介入去分析效能瓶頸並嘗試優化提升響應速度,並很快找到響應慢的瓶頸在資料庫的頻繁操作,於是想到了使用快取來解決問題。

於是,開發人員在專案中使用了基於介面維度的短期快取,對每個介面的請求引數(貼文ID)與響應內容快取一定的時間(比如1分鐘),對於相同的請求,如果匹配到快取則直接返回快取的結果即可,不用再次去執行查詢資料庫以及業務維度的運算邏輯。

JAVA中有很多的開源框架都有提供類似的能力支援,比如Ehcache或者Guava CacheCaffeine Cache等,可以通過簡單的新增註解的方式就實現上述需要的快取效果。比如使用Ehcache來實現介面介面快取的時候,程式碼使用方式如下(這裡先簡單的演示下,後續的系列檔案中會專門對這些框架進行深入的探討):

@Cacheable(value="UserDetailCache", key="#userId")
public UserDetail queryUserDetailById(String userId) {
    UserEntity userEntity = userMapper.queryByUserId(userId);
    return convertEntityToUserDetail(userEntity);
}

基上面的本地快取策略改動後重新上線,整體的響應效能上果然提升了很多。本地快取的策略雖然有效地提升了處理請求的速度,但新的問題也隨之浮現。有使用者反饋,社群內的貼文列表多次重新整理後會出現內容不一致的情況,有的貼文重新整理之後會從列表消失,多次重新整理後偶爾會出現。

其實這就是本地快取在叢集多節點場景下會遇到的一個很常見的快取漂移現象:

因為業務叢集存在多個節點,而快取是每個業務節點本地獨立構建的,所以才出現了更新場景導致的本地快取不一致的問題,進而表現為上述問題現象。

集中式快取的初露鋒芒

為了解決叢集內多個節點間執行寫操作之後,各節點本地快取不一致的問題,開發人員想到可以構建一個集中式快取,然後所有業務節點都讀取或者更新同一份快取資料,這樣就可以完美地解決節點間快取不一致的問題了。

業界成熟的集中式快取有很多,最出名的莫過於很多人都耳熟能詳的Redis,或者是在各種面試中常常被拿來與Redis進行比較的Memcached。也正是由於它們出色的自身效能表現,在當前的各種分散式系統中,Redis近乎已經成為了一種標配,常常與MySQL等持久化資料庫搭配使用,放在資料庫前面進行扛壓。比如下面圖中範例的一種最簡化版本的組網架構:

開發人員對快取進行了整改,將本地快取改為了Redis集中式快取。這樣一來:

  1. 快取不一致問題解決:解決了各個節點間資料不一致的問題。

  2. 單機記憶體容量限制解決:使用了Redis這種分散式的集中式快取,擴大了記憶體快取的容量範圍,可以順便將很多業務層面的資料全部載入到Redis中分片進行快取,效能也相比而言得到了提升。

似乎使用集中式快取已經是分散式系統中的最優解了,但是現實情況真的就這麼簡單麼?也不盡然

多級快取的珠聯璧合

在嚐到了集中式快取的甜頭之後,暖心的程式設計師們想到要徹底為資料庫減壓,將所有業務中需要頻繁使用的資料全部同步儲存到Redis中,然後業務使用的時候直接從Redis中獲取相關資料,大大地減少了資料庫的請求頻次。但是改完上線之後,發現有些處理流程中並沒有太大的效能提升。緣何如此?只因為對集中式快取的過分濫用!分析發現這些流程的處理需要涉及大量的互動與資料整合邏輯,一個流程需要存取近乎30次Redis!雖然Redis的單次請求處理效能極高,甚至可以達到微秒級別的響應速度,但是每個流程裡面幾十次的網路IO互動,導致頻繁的IO請求,以及執行緒的阻塞喚醒切換交替,使得系統線上程上下文切換層面浪費巨大

那麼,要想破局,最常規的手段便是嘗試降低對集中式快取(如Redis)的請求數量,降低網路IO互動次數。而如何來降低呢? —— 又回到了本地快取!集中式快取並非是分散式系統中提升效能的銀彈,但我們可以將本地快取與集中式快取結合起來使用,取長補短,實現效果最大化。如圖所示:

上圖演示的也即多級快取的策略。具體而言:

  • 對於一些變更頻率比較高的資料,採用集中式快取,這樣可以確保資料變更之後所有節點都可以實時感知到,確保資料一致;

  • 對於一些極少變更的資料(比如一些系統設定項)或者是一些對短期一致性要求不高的資料(比如使用者暱稱、簽名等)則採用本地快取,大大減少對遠端集中式快取的網路IO次數。

這樣一來,系統的響應效能又得到了進一步的提升。

通過對快取使用策略的一步步演進,我們可以感受到快取的恰當使用對系統效能的幫助作用。

無處不在的快取

快取存在的初衷,就是為了相容兩個處理速度不一致的場景對接適配的。在我們的日常生活中,也常常可以看到「快取」的影子。比如對於幾年前比較盛行的那種帶桶的淨水器(見下圖),由於淨水的功率比較小,導致實時過濾得到純淨水的水流特別的緩慢,使用者倒一杯水要等2分鐘,體驗太差,所以配了個蓄水桶,淨水機先慢慢的將淨化後的水儲存到桶中,然後使用者倒水的時候可以從桶裡快速的倒出,無需焦急等待 —— 這個蓄水桶,便是一個快取器

編碼源於生活,CPU快取記憶體設計就是這一生活實踐在計算機領域的原樣複製。快取可以說在軟體世界裡無處不在,除了我們自己的業務系統外,在網路傳輸作業系統中介軟體基礎框架中都可以看到快取的影子。如:

  1. 網路傳輸場景

比如ARP協定,基於ARP快取表進行IP與終端硬體MAC地址之間的快取對映。這樣與對端主機之間有通訊需求的時候,就可以在ARP快取中查詢到IP對應的對端裝置MAC地址,避免每次請求都需要去傳送ARP請求查詢MAC地址。

  1. MyBatis的多級快取

MyBatis作為JAVA體系中被廣泛使用的資料庫操作框架,其內部為了提升處理效率,構建了一級快取二級快取,大大減少了對SQL的重複執行次數。

  1. CPU中的快取

CPU記憶體之間有個臨時記憶體(快取記憶體),容量雖比記憶體小,但是處理速度卻遠快於普通記憶體。快取記憶體的機制,有效地解決了CPU運算速度記憶體讀寫速度不匹配的問題。

快取的使用場景

快取作為網際網路類軟體系統架構與實現中的基石般的存在,不僅僅是在系統扛壓或者介面處理速度提升等效能優化方案,在其他多個方面都可以發揮其獨一無二的關鍵價值。下面就讓我們一起來看看快取都可以用在哪些場景上,可以解決我們哪方面的痛點。

降低自身CPU消耗

如前面章節中提到的專案範例,快取最典型的使用場景就是用在系統的效能優化上。而在效能優化層面,一個經典的策略就是「空間換時間」。比如:

  • 在資料庫表中做一些欄位冗備

比如使用者表T_User和部門表T_Department,在T_User表中除了有個Department_Id欄位與T_Department表進行關聯之外,還額外在T_User表中儲存Department_Name值。這樣在很多需要展示使用者所屬部門資訊的時候就省去了多表關聯查詢的操作。

  • 對一些中間處理結果進行儲存

比如系統中的資料包表模組,需要對整個系統內所有的關聯業務資料進行計算統計,且需要多張表多來源資料之間的綜合彙總之後才能得到最終的結果,整個過程的計算非常的耗時。如果藉助快取,則可以將一些中間計算結果進行暫存,然後報表請求中基於中間結果進行二次簡單處理即可。這樣可以大大降低基於請求觸發的實時計算量。

在「空間換時間」實施策略中,快取是該策略的核心、也是被使用的最為廣泛的一種方案。藉助快取,可以將一些CPU耗時計算的處理結果進行快取複用,以降低重複計算工作量,達到降低CPU佔用的效果。

減少對外IO互動

上面介紹的使用快取是為了不斷降低請求處理時對自身CPU佔用,進而提升服務的處理效能。這裡我們介紹快取的另一典型使用場景,就是減少系統對外依賴請求頻次。即通過將一些從遠端請求回來的響應結果進行快取,後面直接使用此快取結果而無需再次發起網路IO請求互動。

對於伺服器端而言,通過構建快取的方式來減少自身對外的IO請求,主要有幾個考量出發點:

  1. 自身效能層面考慮,減少對外IO操作,降低了對外介面的響應時延,也對伺服器端自身處理效能有一定提升。

  2. 對端服務穩定性層面考慮,避免對端服務負載過大。很多時候呼叫方和被呼叫方系統的承壓能力是不匹配的,甚至有些被呼叫方系統可能是不承壓的。為了避免將對端服務壓垮,需要呼叫方快取請求結果,降低IO請求。

  3. 自身可靠性層面而言,將一些遠端服務請求到的結果快取起來,即使遠端服務出現故障,自身業務依舊可以基於快取資料進行正常業務處理,起到一個兜底作用提升自身的抗風險能力

在分散式系統服務治理範疇內,服務註冊管理服務是必不可少的,比如SpringCloud家族的Eureka,或者是Alibaba開源的Nacos。它們對於快取的利用,可以說是對上面所提幾點的完美闡述。

Nacos為例:

除了上述的因素之外,對一些行動端APP或者H5介面而言,快取的使用還有一個層面的考慮,即降低使用者的流量消耗,通過將一些資源類資料快取到本地,避免反覆去下載,給使用者省點流量,也可以提升使用者的使用體驗(介面渲染速度快,減少出現白屏等待的情況)。

提升使用者個性化體驗

快取除了在系統效能提升或系統可靠性兜底等場景發揮價值外,在APP或者web類使用者側產品中,還經常被用於儲存一些臨時非永久的個性化使用習慣設定或者身份資料,以提升使用者的個性化使用體驗。

  • 快取cookiesession等身份鑑權資訊,這樣就可以避免使用者每次存取都需要進行身份驗證。

  • 記住一些使用者上次操作習慣,比如使用者在一個頁面上將列表分頁查詢設定為100條/頁,則後續在系統記憶體取其它列表頁面時,都沿用這一設定。

  • 快取使用者的一些本地設定,這個主要是APP端常用的功能,可以在快取中儲存些與當前裝置繫結的設定資訊,僅對當前裝置有效。比如同一個賬號登入某個APP,使用者希望在手機端可以顯示深色主題,而PAD端則顯示淺色主體,這種基於裝置的個性化設定,可以快取到裝置本身即可。

業務與快取的整合模式

如前所述,我們可以在不同的方面使用快取來輔助達成專案在某些方面的訴求。而根據使用場景的不同,在結合快取進行業務邏輯實現的時候,也會存在不同的架構模式,典型的會有旁路型快取穿透型快取非同步型快取三種。

旁路型快取

旁路型快取模式中,業務自行負責與快取以及資料庫之間的互動,可以自由決定快取未命中場景的處理策略,更加契合大部分業務場景的客製化化訴求。

由於業務模組自行實現快取與資料庫之間的資料寫入與更新的邏輯,實際實現的時候需要注意下在高並行場景的資料一致性問題,以及可能會出現的快取擊穿快取穿透快取雪崩等問題的防護。

旁路型快取是實際業務中最常使用的一種架構模式,在後面的內容中,我們還會不斷的涉及到旁路快取中相關的內容。

穿透型快取

穿透型快取在實際業務中使用的較少,主要是應用在一些快取類的中介軟體中,或者在一些大型系統中專門的資料管理模組中使用。

一般情況下,業務使用快取的時候,會是先嚐試讀取快取,在嘗試讀取DB,而使用穿透型快取架構時,會有專門模組將這些動作封裝成黑盒的,業務模組不會與資料庫進行直接互動。如下圖所示:

這種模式對業務而言是比較友好的,業務只需呼叫快取介面即可,無需自行實現快取與DB之間的互動策略。

非同步型快取

還有一種快取的使用模式,可以看作是穿透型快取的演進異化版本,其使用場景也相對較少,即非同步型快取。其主要策略就是業務側請求的實時讀寫互動都是基於快取進行,任何資料的讀寫也完全基於快取進行操作。此外,單獨實現一個資料持久化操作(獨立執行緒或者程序中執行),用於將快取中變更的資料寫入到資料庫中。

這種情況,實時業務讀寫請求完全基於快取進行,而將資料庫僅僅作為一個資料持久化儲存的備份盤。由於實時業務請求僅與快取進行互動,所以在效能上可以得到更好的表現。但是這種模式也存在一個致命的問題:資料可靠性!因為是非同步操作,所以在下一次資料寫入DB前,會有一段時間資料僅存在於快取中,一旦快取服務宕機,這部分資料將會丟失。所以這種模式僅適用於對資料一致性要求不是特別高的場景。

快取的優秀實踐

快取持久化儲存的一個很大的不同點就是快取的定位應該是一種輔助角色,是一種錦上添花般的存在。

快取也是一把雙刃劍,基於快取可以大幅提升我們的系統並行承壓能力,但稍不留神也可能會讓我們的系統陷入滅頂之災。所以我們在決定使用快取的時候,需要知曉快取設計與使用的一些關鍵要點,才可以讓我們在使用的時候更加遊刃有餘。

可刪除重建

可刪除重建,這是快取與持久化儲存最大的一個差別。快取的定位一定是為了輔助業務處理而生的,也就是說快取有則使用,沒有也不會影響到我們具體的業務運轉。此外,即使我們的快取資料除了問題,我們也可以將其刪除重建。

這一點在APP類的產品中體現的會比較明顯。比如對於微信APP的快取,就有明確的提示說快取可以刪除而不會影響其功能使用:

同樣地,我們也可以去放心的清理瀏覽器的快取,而不用擔心清理之後我們瀏覽器或者網頁的功能會出現異常(最多就是需要重新下載或者重建快取資料,速度會有一些慢)。

相同的邏輯,在伺服器端構建的一些快取,也應該具備此特性。比如基於記憶體的快取,當業務程序重啟後,應該有途徑可以將快取重建出來(比如從MySQL中載入資料然後構建快取,或者是快取從0開始基於請求觸發而構建)。

有兜底屏障

快取作為高並行類系統中的核心元件,負責抗住大部分的並行請求,一旦快取元件出問題,往往對整個系統會造成毀滅性的打擊。所以我們的快取在實現的時候必須要有充足且完備的兜底自恢復機制。需要做到以下幾點:

  • 關注下快取資料量超出承受範圍的處理策略,比如定好資料的淘汰機制

  • 避免快取集中失效,比如批次載入資料到快取的時候隨機打散過期時間,避免同一時間大批次快取失效引發快取雪崩問題。

  • 有效地冷資料預熱載入機制,以及熱點資料防過期機制,避免出現大量對冷資料的請求無法命中快取或者熱點資料突然失效,導致快取擊穿問題。

  • 合理的防身自保手段,比如採用布隆過濾器機制,避免被惡意請求攻陷,導致快取穿透類的問題。

快取的可靠性與兜底策略設計,是一個宏大且寬泛的命題,在本系列專欄後續的文章中,我們會逐個深入的探討。

關注快取的一致性保證

在高並行類的系統中進行資料更新的時候,快取與資料庫的資料一致性問題,是一個永遠無法繞過的話題。對於基於旁路型快取的大部分業務而言,資料更新操作,一般可以組合出幾種不同的處理策略:

  • 先更新快取,再更新資料庫

  • 先更新資料庫, 再更新快取

  • 先刪除快取,再更新資料庫

  • 先更新資料庫,再刪除快取

由於大部分資料庫都支援事務,而幾乎所有的快取操作都不具有事務性。所以在一些寫操作並行不是特別高且一致性要求不是特別強烈的情況下,可以簡單的藉助資料庫的事務進行控制。比如先更新資料庫再更新快取,如果快取更新失敗則回滾資料庫事務。

然而在一些並行請求特別高的時候,基於事務控制來保證資料一致性往往會對效能造成影響,且事務隔離級別設定的越高影響越大,所以也可以採用一些其它輔助策略,來替代事務的控制,如重試機制、或非同步補償機制、或多者結合方式等。

比如下圖所示的這種策略:

上圖的資料更新處理策略,可以有效地保證資料的最終一致性,降低極端情況可能出現資料不一致的概率,並兜底增加了資料不一致時的自恢復能力。

資料一致性保證作為快取的另一個重要命題,我們會在本系列專欄後續的文章中專門進行深入的剖析。

總結回顧

本篇文章的內容中,我們對快取的各個方面進行了一個簡單的闡述與瞭解,也可以看出快取對於一個軟體系統的重要價值。通過對快取的合理、充分利用,可以大大的增強我們的系統承壓效能、提升產品的使用者體驗

快取作為高並行系統中的神兵利器被廣泛使用,堪稱高並行系統的基石之一。而快取的內容還遠遠不止我們本篇檔案中所介紹的這些、它是一個非常宏大的命題。

為了能夠將快取的方方面面徹底的講透、講全,在接下來的一段時間裡,我會以系列專欄的形式,從不同的角度對快取的方方面面進行探討。不僅僅著眼於如何去使用快取、也一起聊聊快取設計中的一些哲學理念 —— 這一點是我覺得更有價值的一點,因為這些理念對提升我們的軟體架構認知、完善我們的軟體設計思維有很大的指導與借鑑意義。

所以,如果你有興趣,歡迎關注本系列專欄(深入理解快取原理與實戰設計),我會以我一貫的行文風格,用最簡單的語言講透複雜的邏輯,期待一起切磋、共同成長。

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

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

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