在服務端開發中,緩存常常被當做系統性能扛壓的不二之選。在實施方案上,緩存使用策略雖有一定普適性,卻也並非完全絕對,需要結合實際的項目訴求與場景進行綜合權衡與考量,進而得出符合自己項目的最佳實踐。 ...
大家好,又見面了。
本文是筆者作為掘金技術社區簽約作者的身份輸出的緩存專欄系列內容,將會通過系列專題,講清楚緩存的方方面面。如果感興趣,歡迎關註以獲取後續更新。
在服務端開發中,緩存常常被當做系統性能扛壓的不二之選。在實施方案上,緩存使用策略雖有一定普適性,卻也並非完全絕對,需要結合實際的項目訴求與場景進行綜合權衡與考量,進而得出符合自己項目的最佳實踐。
緩存使用的演進
現有這麼一個系統:
一個互動論壇系統,用戶登錄系統之後,可以在論壇上查看帖子列表、查看帖子詳情、發表帖子、評論帖子、為帖子點贊等操作。
系統中所有的配置數據與業務數據均存儲在資料庫
中。隨著業務的發展,註冊用戶量越來越多,然後整個系統的響應速度也越來越慢,用戶體驗越來越差,用戶逐漸出現流失。
本地緩存的牛刀小試
為了輓救這一局面,開發人員需要介入去分析性能瓶頸並嘗試優化提升響應速度,並很快找到響應慢的瓶頸在資料庫的頻繁操作,於是想到了使用緩存
來解決問題。
於是,開發人員在項目中使用了基於介面維度的短期緩存,對每個介面的請求參數
(帖子ID)與響應內容
緩存一定的時間(比如1分鐘),對於相同的請求,如果匹配到緩存則直接返回緩存的結果即可,不用再次去執行查詢資料庫以及業務維度的運算邏輯。
JAVA
中有很多的開源框架都有提供類似的能力支持,比如Ehcache
或者Guava Cache
、Caffeine 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集中式緩存。這樣一來:
-
緩存不一致問題解決:解決了各個節點間數據不一致的問題。
-
單機記憶體容量限制解決:使用了Redis這種分散式的集中式緩存,擴大了記憶體緩存的容量範圍,可以順便將很多業務層面的數據全部載入到Redis中分片進行緩存,性能也相比而言得到了提升。
似乎使用集中式緩存已經是分散式系統中的最優解了,但是現實情況真的就這麼簡單麽?也不盡然!
多級緩存的珠聯璧合
在嘗到了集中式緩存的甜頭之後,暖心的程式員們想到要徹底為資料庫減壓,將所有業務中需要頻繁使用的數據全部同步存儲到Redis
中,然後業務使用的時候直接從Redis中獲取相關數據,大大地減少了資料庫的請求頻次。但是改完上線之後,發現有些處理流程中並沒有太大的性能提升。緣何如此?只因為對集中式緩存
的過分濫用!分析發現這些流程的處理需要涉及大量的交互與數據整合邏輯,一個流程需要訪問近乎30
次Redis!雖然Redis的單次請求處理性能極高,甚至可以達到微秒級別的響應速度,但是每個流程裡面幾十次的網路IO
交互,導致頻繁的IO請求
,以及線程的阻塞
與喚醒
切換交替,使得系統線上程上下文切換層面浪費巨大。
那麼,要想破局,最常規的手段便是嘗試降低對集中式緩存(如Redis)的請求數量,降低網路IO交互次數。而如何來降低呢? —— 又回到了本地緩存!集中式緩存並非是分散式系統中提升性能的銀彈,但我們可以將本地緩存與集中式緩存結合起來使用,取長補短,實現效果最大化。如圖所示:
上圖演示的也即多級緩存的策略。具體而言:
-
對於一些變更頻率比較高的數據,採用
集中式緩存
,這樣可以確保數據變更之後所有節點都可以實時感知到,確保數據一致; -
對於一些極少變更的數據(比如一些系統配置項)或者是一些對短期一致性要求不高的數據(比如用戶昵稱、簽名等)則採用
本地緩存
,大大減少對遠端集中式緩存的網路IO次數。
這樣一來,系統的響應性能又得到了進一步的提升。
通過對緩存使用策略的一步步演進,我們可以感受到緩存的恰當使用對系統性能的幫助作用。
無處不在的緩存
緩存存在的初衷,就是為了相容兩個處理速度不一致的場景對接適配的。在我們的日常生活中,也常常可以看到“緩存”的影子。比如對於幾年前比較盛行的那種帶桶的凈水器(見下圖),由於凈水的功率比較小,導致實時過濾得到純凈水的水流特別的緩慢,用戶倒一杯水要等2分鐘
,體驗太差,所以配了個蓄水桶,凈水機先慢慢的將凈化後的水存儲到桶中,然後用戶倒水的時候可以從桶里快速的倒出,無需焦急等待 —— 這個蓄水桶,便是一個緩存器。
編碼源於生活,CPU
的高速緩存設計就是這一生活實踐在電腦領域的原樣複製。緩存可以說在軟體世界里無處不在,除了我們自己的業務系統外,在網路傳輸
、操作系統
、中間件
、基礎框架
中都可以看到緩存的影子。如:
- 網路傳輸場景。
比如ARP協議
,基於ARP緩存表進行IP
與終端硬體MAC
地址之間的緩存映射。這樣與對端主機之間有通信需求的時候,就可以在ARP緩存中查找到IP對應的對端設備MAC地址,避免每次請求都需要去發送ARP請求查詢MAC地址。
- MyBatis的多級緩存。
MyBatis
作為JAVA
體系中被廣泛使用的資料庫操作框架,其內部為了提升處理效率,構建了一級緩存與二級緩存,大大減少了對SQL
的重覆執行次數。
- CPU中的緩存。
CPU
與記憶體
之間有個臨時存儲器(高速緩存),容量雖比記憶體小,但是處理速度卻遠快於普通記憶體。高速緩存的機制,有效地解決了CPU運算速度
與記憶體讀寫速度
不匹配的問題。
緩存的使用場景
緩存作為互聯網類軟體系統架構與實現中的基石般的存在,不僅僅是在系統扛壓或者介面處理速度提升等性能優化方案,在其他多個方面都可以發揮其獨一無二的關鍵價值。下麵就讓我們一起來看看緩存都可以用在哪些場景上,可以解決我們哪方面的痛點。
降低自身CPU消耗
如前面章節中提到的項目實例,緩存最典型的使用場景就是用在系統的性能優化上。而在性能優化層面,一個經典的策略就是“空間換時間”。比如:
- 在資料庫表中做一些欄位冗備。
比如用戶表T_User
和部門表T_Department
,在T_User
表中除了有個Department_Id
欄位與T_Department
表進行關聯之外,還額外在T_User
表中存儲Department_Name
值。這樣在很多需要展示用戶所屬部門信息的時候就省去了多表關聯查詢的操作。
- 對一些中間處理結果進行存儲。
比如系統中的數據報表模塊,需要對整個系統內所有的關聯業務數據進行計算統計,且需要多張表多來源數據之間的綜合彙總之後才能得到最終的結果,整個過程的計算非常的耗時。如果藉助緩存,則可以將一些中間計算結果進行暫存,然後報表請求中基於中間結果進行二次簡單處理即可。這樣可以大大降低基於請求觸發的實時計算量。
在“空間換時間
”實施策略中,緩存是該策略的核心、也是被使用的最為廣泛的一種方案。藉助緩存,可以將一些CPU
耗時計算的處理結果進行緩存復用,以降低重覆計算工作量,達到降低CPU
占用的效果。
減少對外IO交互
上面介紹的使用緩存是為了不斷降低請求處理時對自身CPU占用,進而提升服務的處理性能。這裡我們介紹緩存的另一典型使用場景,就是減少系統對外依賴
的請求頻次。即通過將一些從遠端請求回來的響應結果進行緩存,後面直接使用此緩存結果而無需再次發起網路IO請求交互。
對於服務端而言,通過構建緩存的方式來減少自身對外的IO請求,主要有幾個考量出發點:
-
從自身性能層面考慮,減少對外
IO操作
,降低了對外介面的響應時延
,也對服務端自身處理性能有一定提升。 -
從對端服務穩定性層面考慮,避免對端服務
負載過大
。很多時候調用方和被調用方系統的承壓能力是不匹配的,甚至有些被調用方系統可能是不承壓的。為了避免將對端服務壓垮,需要調用方緩存請求結果,降低IO
請求。 -
從自身可靠性層面而言,將一些遠端服務請求到的結果緩存起來,即使遠端服務出現故障,自身業務依舊可以基於緩存數據進行正常業務處理,起到一個
兜底作用
,提升自身的抗風險能力。
在分散式系統服務治理範疇內,服務註冊管理服務是必不可少的,比如SpringCloud
家族的Eureka
,或者是Alibaba
開源的Nacos
。它們對於緩存的利用,可以說是對上面所提幾點的完美闡述。
以Nacos
為例:
除了上述的因素之外,對一些移動端APP
或者H5
界面而言,緩存的使用還有一個層面的考慮,即降低用戶的流量消耗,通過將一些資源類數據緩存到本地,避免反覆去下載,給用戶省點流量,也可以提升用戶的使用體驗(界面渲染速度快,減少出現白屏等待的情況)。
提升用戶個性化體驗
緩存除了在系統性能提升或系統可靠性兜底等場景發揮價值外,在APP
或者web
類用戶側產品中,還經常被用於存儲一些臨時非永久的個性化使用習慣配置或者身份數據,以提升用戶的個性化使用體驗。
- 緩存
cookie
、session
等身份鑒權信息,這樣就可以避免用戶每次訪問都需要進行身份驗證。
-
記住一些用戶上次
操作習慣
,比如用戶在一個頁面上將列表分頁查詢設置為100
條/頁,則後續在系統內訪問其它列表頁面時,都沿用這一設置。 -
緩存用戶的一些
本地設置
,這個主要是APP
端常用的功能,可以在緩存中保存些與當前設備綁定的設置信息,僅對當前設備有效。比如同一個賬號登錄某個APP,用戶希望在手機端可以顯示深色主題,而PAD端則顯示淺色主體,這種基於設備的個性化設置,可以緩存到設備本身即可。
業務與緩存的集成模式
如前所述,我們可以在不同的方面使用緩存來輔助達成項目在某些方面的訴求。而根據使用場景的不同,在結合緩存進行業務邏輯實現的時候,也會存在不同的架構模式,典型的會有旁路型緩存
、穿透型緩存
與非同步型緩存
三種。
旁路型緩存
在旁路型緩存模式中,業務自行負責與緩存以及資料庫之間的交互,可以自由決定緩存未命中場景的處理策略,更加契合大部分業務場景的定製化訴求。
由於業務模塊自行實現緩存與資料庫之間的數據寫入與更新的邏輯,實際實現的時候需要註意下在高併發場景的數據一致性
問題,以及可能會出現的緩存擊穿
、緩存穿透
、緩存雪崩
等問題的防護。
旁路型緩存是實際業務中最常使用的一種架構模式,在後面的內容中,我們還會不斷的涉及到旁路緩存中相關的內容。
穿透型緩存
穿透型緩存在實際業務中使用的較少,主要是應用在一些緩存類的中間件中,或者在一些大型系統中專門的數據管理模塊中使用。
一般情況下,業務使用緩存的時候,會是先嘗試讀取緩存,在嘗試讀取DB
,而使用穿透型緩存架構時,會有專門模塊將這些動作封裝成黑盒的,業務模塊不會與資料庫進行直接交互。如下圖所示:
這種模式對業務而言是比較友好的,業務只需調用緩存介面即可,無需自行實現緩存與DB之間的交互策略。
非同步型緩存
還有一種緩存的使用模式,可以看作是穿透型緩存的演進異化版本,其使用場景也相對較少,即非同步型緩存。其主要策略就是業務側請求的實時讀寫交互都是基於緩存進行,任何數據的讀寫也完全基於緩存進行操作。此外,單獨實現一個數據持久化操作(獨立線程或者進程中執行),用於將緩存中變更的數據寫入到資料庫中。
這種情況,實時業務讀寫請求完全基於緩存進行,而將資料庫僅僅作為一個數據持久化存儲的備份盤。由於實時業務請求僅與緩存進行交互,所以在性能上可以得到更好的表現。但是這種模式也存在一個致命的問題:數據可靠性!因為是非同步操作,所以在下一次數據寫入DB前,會有一段時間數據僅存在於緩存中,一旦緩存服務宕機,這部分數據將會丟失。所以這種模式僅適用於對數據一致性要求不是特別高的場景。
緩存的優秀實踐
緩存
與持久化存儲
的一個很大的不同點就是緩存的定位應該是一種輔助角色,是一種錦上添花般的存在。
緩存
也是一把雙刃劍,基於緩存可以大幅提升我們的系統併發與承壓能力,但稍不留神也可能會讓我們的系統陷入滅頂之災。所以我們在決定使用緩存的時候,需要知曉緩存設計與使用的一些關鍵要點,才可以讓我們在使用的時候更加游刃有餘。
可刪除重建
可刪除重建,這是緩存與持久化存儲最大的一個差別。緩存的定位一定是為了輔助業務處理而生的,也就是說緩存有則使用,沒有也不會影響到我們具體的業務運轉。此外,即使我們的緩存數據除了問題,我們也可以將其刪除重建。
這一點在APP
類的產品中體現的會比較明顯。比如對於微信APP
的緩存,就有明確的提示說緩存可以刪除而不會影響其功能使用:
同樣地,我們也可以去放心的清理瀏覽器
的緩存,而不用擔心清理之後我們瀏覽器或者網頁的功能會出現異常(最多就是需要重新下載或者重建緩存數據,速度會有一些慢)。
相同的邏輯,在服務端構建的一些緩存,也應該具備此特性。比如基於記憶體的緩存,當業務進程重啟後,應該有途徑可以將緩存重建出來(比如從MySQL
中載入數據然後構建緩存,或者是緩存從0開始
基於請求觸發而構建)。
有兜底屏障
緩存作為高併發類系統中的核心組件,負責抗住大部分的併發請求,一旦緩存組件出問題,往往對整個系統會造成毀滅性的打擊。所以我們的緩存在實現的時候必須要有充足且完備的兜底與自恢復機制。需要做到以下幾點:
-
關註下緩存數據量超出承受範圍的處理策略,比如定好數據的
淘汰機制
。 -
避免緩存集中失效,比如批量載入數據到緩存的時候
隨機打散
過期時間,避免同一時間大批量緩存失效引發緩存雪崩問題。 -
有效地冷數據預熱載入機制,以及熱點數據防過期機制,避免出現大量對冷數據的請求無法命中緩存或者熱點數據突然失效,導致
緩存擊穿
問題。 -
合理的防身自保手段,比如採用
布隆過濾器
機制,避免被惡意請求攻陷,導致緩存穿透類的問題。
緩存的可靠性與兜底策略設計,是一個巨集大且寬泛的命題,在本系列專欄後續的文章中,我們會逐個深入的探討。
關註緩存的一致性保證
在高併發類的系統中進行數據更新的時候,緩存與資料庫的數據一致性
問題,是一個永遠無法繞過的話題。對於基於旁路型緩存的大部分業務而言,數據更新操作,一般可以組合出幾種不同的處理策略:
-
先更新緩存,再更新資料庫
-
先更新資料庫, 再更新緩存
-
先刪除緩存,再更新資料庫
-
先更新資料庫,再刪除緩存
由於大部分資料庫都支持事務
,而幾乎所有的緩存操作都不具有事務性。所以在一些寫操作併發不是特別高且一致性要求不是特別強烈的情況下,可以簡單的藉助資料庫的事務進行控制。比如先更新資料庫再更新緩存,如果緩存更新失敗則回滾資料庫事務。
然而在一些併發請求特別高的時候,基於事務控制來保證數據一致性往往會對性能造成影響,且事務隔離級別
設置的越高影響越大,所以也可以採用一些其它輔助策略,來替代事務的控制,如重試機制
、或非同步補償機制
、或多者結合方式等。
比如下圖所示的這種策略:
上圖的數據更新處理策略,可以有效地保證數據的最終一致性,降低極端情況可能出現數據不一致的概率,並兜底增加了數據不一致時的自恢復能力。
數據一致性保證作為緩存的另一個重要命題,我們會在本系列專欄後續的文章中專門進行深入的剖析。
總結回顧
本篇文章的內容中,我們對緩存的各個方面進行了一個簡單的闡述與瞭解,也可以看出緩存對於一個軟體系統的重要價值。通過對緩存的合理、充分利用,可以大大的增強我們的系統承壓性能
、提升產品的用戶體驗
。
緩存作為高併發系統中的神兵利器
被廣泛使用,堪稱高併發系統的基石之一。而緩存的內容還遠遠不止我們本篇文檔中所介紹的這些、它是一個非常巨集大的命題。
為了能夠將緩存的方方面面徹底的講透、講全,在接下來的一段時間里,我會以系列專欄的形式,從不同的角度對緩存的方方面面進行探討。不僅僅著眼於如何去使用緩存、也一起聊聊緩存設計中的一些哲學理念
—— 這一點是我覺得更有價值的一點,因為這些理念對提升我們的軟體架構認知、完善我們的軟體設計思維有很大的指導與借鑒意義。
所以,如果你有興趣,歡迎關註本系列專欄(深入理解緩存原理與實戰設計),我會以我一貫的行文風格,用最簡單的語言講透複雜的邏輯,期待一起切磋、共同成長。
我是悟道,聊技術、又不僅僅聊技術~
如果覺得有用,請點贊 + 關註讓我感受到您的支持。也可以關註下我的公眾號【架構悟道】,獲取更及時的更新。
期待與你一起探討,一起成長為更好的自己。
本文來自博客園,作者:架構悟道,歡迎關註公眾號[架構悟道]持續獲取更多乾貨,轉載請註明原文鏈接:https://www.cnblogs.com/softwarearch/p/16828094.html