大家好,我是冰河~~
最近,有小夥伴私信我:冰哥,我最近出去面試,面試官問我如何設計快取能讓系統在百萬級別流量下仍能平穩執行,我當時沒回答上來。接著,面試官問我之前的專案是怎麼使用快取的,我說只是快取了一些資料。當時確實想不到快取還有哪些用處,估計這次面試是掛了。冰哥,你可以給我講講網際網路大廠專案是怎麼設計和使用快取的嗎?
另外,本文快取方案已經開源,開源地址如下,如果開源方案對你有點幫助或者啟發,歡迎在程式碼倉庫給個Star,讓更過的小夥伴看到它,互相學習,一起進步。
- GitHub:https://github.com/binghe001/spring-redis
- Gitee:https://gitee.com/binghe001/spring-redis
- GitCode:https://gitcode.net/binghe001/spring-redis
一、前言
透過這位小夥伴的自述,我們明顯感受到這位小夥伴對快取的認識還是停留在簡單的儲存資料上,沒有對使用快取背後的場景和實現邏輯進行深層次的思考。在網際網路大廠專案中,快取也是一種必不可少的元件,那使用快取僅僅是為了快取熱點資料,提升讀效能嗎?如果你對快取的認識只是停留在這裡,那就未免太淺顯了。
今天,我們就以高併發、大流量業務場景中最具代表性的 秒殺系統 為例,採用市面上大家都比較熟悉的技術,一起探究下 秒殺系統 背後是如何設計和使用快取的。
二、秒殺系統快取核心訴求
秒殺系統在承接瞬時高併發流量時,如果將流量直接打到資料庫,那資料庫很有可能因為扛不住瞬間的高併發流量而導致崩潰和當機。所以,需要對秒殺系統進行極致的快取設計,讓大部分流量走快取。同時,在設計快取架構方案時,為了進一步提升效能,將採用 本地快取+分散式快取的混合型快取 設計方案,讓本地快取抗大部分流量,分散式快取次之,資料庫再次之,如圖1所示
並且針對秒殺系統這種瞬時併發量高的場景,在設計快取時,需要注意的技巧:優先讀取本地快取資料,如果本地快取失效,則讀取分散式快取資料,並且在同一時刻,只能有一個執行緒更新本地快取,防止快取擊穿。沒有獲取到本地快取更新機會的其他執行緒,需要立即返回而不是原地等待。如果分散式快取失效時,在同一時刻,也只能有一個執行緒更新分散式快取,防止快取擊穿。沒有獲取到分散式快取更新機會的執行緒,也需要立即返回而不是原地等待。
另外,需要注意的是:我們提出了採用 本地快取+分散式快取的混合型快取設計方案,後文會著重對這種設計進行說明。
三、秒殺系統快取使用場景
秒殺系統屬於典型的讀多寫少的高併發系統,應對這種場景的一個有效措施就是使用快取,不管是單機JVM快取還是以Redis為例的分散式快取,其讀寫效能都會比資料庫高得多。所以,在秒殺系統中,為了應對高併發、大流量的業務場景,快取自然也就成為建設秒殺系統過程中必不可少的環節。
3.1 秒殺系統介面分析
在秒殺系統中,主要是對一些讀資料的介面設計快取策略,而在這些讀資料的介面中,獲取秒殺活動列表、獲取秒殺活動詳情、獲取秒殺商品列表和獲取秒殺商品詳情的介面流量比其他介面高。尤其是獲取秒殺商品列表和獲取秒殺商品詳情的介面QPS一般會高於獲取秒殺活動列表和秒殺活動詳情的介面,畢竟大部分使用者在秒殺開始前就已經進入到秒殺詳情頁,當然這也不是絕對的,還是要看秒殺系統對於這些介面的設計。
3.2 秒殺系統快取場景
儘管獲取秒殺商品列表和獲取秒殺商品詳情的介面QPS一般會高於獲取秒殺活動列表和秒殺活動詳情的介面,但是我們在設計快取時,需要對這些介面一視同仁,都要以嚴格的高標準來設計這些介面,不然稍有不慎,一個介面出現問題,就可能導致整場秒殺活動以失敗告終。秒殺系統快取的使用場景如圖2所示。
所以,在秒殺系統中,會對獲取秒殺活動列表、獲取秒殺活動詳情、獲取秒殺商品列表和獲取秒殺商品詳情的介面設計快取策略。
四、混合型快取設計
總體來說,在設計秒殺系統的快取過程中,會採用 本地快取+分散式快取的混合型快取 設計方案。其中,本地快取指的就是單機快取,比如JVM記憶體快取,單機Cache快取。分散式快取指的是以分散式的方式集中管理的快取,比如Memcached、Redis等,如圖3所示。
4.1 抗流量洪峰
良好的快取設計不僅僅能夠提升系統的總體效能,還能作為抗瞬時流量洪峰的有效防線。可以這麼說,如果整個秒殺系統前置的流量管控、流量清洗和限流等是秒殺系統流量洪峰的第一道防線,則本地快取就是抗流量洪峰的第二道防線,而分散式快取就是第三道防線,如圖4所示。
使用快取能夠抗一定的流量洪峰,經過前置的流量管控、流量清洗和限流等措施的第一道防線、本地快取的第二道防線、分散式快取的第三道防線,真正進入資料庫的流量就會比較小了。
4.2 快取叢集方案
從快取叢集模式的角度去分析,每臺伺服器甚至JVM例項都會擁有自己獨立的本地快取,在承載大併發流量時,,以本地快取為主,分散式快取次之,如圖5所示。
可以看到,從快取的叢集模式角度來看,每臺伺服器都會自己獨立本地快取,除了前置的流程管控、流量清洗和限流等措施構築的流量洪峰第一道防線外。本地快取會承接剩餘的大部分流量,構築成流量洪峰的第二道防線,而分散式快取則是流量洪峰的第三道防線。並且在快取的設計上,分散式快取的作用主要是協調和同步最新資料到本地快取。
也就是說,只有本地快取失效時,才會訪問分散式快取,將分散式快取中的資料更新到本地快取中,並且同一時刻只能有一個執行緒對本地快取進行更新操作,以避免多個執行緒併發更新本地快取。同樣的,如果分散式快取失效,則同一時刻只能有一個執行緒訪問資料庫來獲取對應的資料,並將其更新到分散式快取。
在叢集模式下,我們應該盡最大努力將流量攔截在本地快取,避免過多的請求訪問分散式快取,提高秒殺系統的效能,並且降低秒殺系統由於大量的遠端IO導致的各種風險。
4.3 快取互動流程
採用本地快取+分散式快取的混合型快取架構設計方案時,在讀取快取資料時,會優先讀取本地快取的資料,如果本地快取未開啟,或者已經失效,此時就會使用分散式快取,也就是說,優先讀取本地快取中的資料,如果本地快取未開啟或者快取資料失效,則讀取分散式快取中的資料,如圖6所示。
可以看到,只有在本地快取未開啟或者快取失效的情況下,才會去訪問分散式快取,讀取分散式快取中的資料,並且在同一個時刻只能有一個執行緒更新本地快取中的資料,這種方式可以最大限度減少遠端IO為秒殺系統帶來的風險。具體的流程如下所示。
(1)判斷本地快取是否開啟,如果開啟則進行第2步,否則進行第4步。
(2)判斷本地快取是否失效,如果未失效,則進行第3步,否則進行第4步。
(3)讀取本地快取資料,讀取快取流程結束。
(4)判斷分散式快取是否開啟,如果開啟則進行第5步,否則進行第7步。
(5)判斷分散式快取是否失效,如果未失效,則進行第6步,否則進行第7步。
(6)讀取分散式快取資料,同一時刻只有一個執行緒更新本地快取資料,讀取快取流程結束。
(7)讀取資料庫資料,同一時刻只有一個執行緒更新分散式快取資料,讀取快取流程結束。
這裡,有一個設計技巧需要大家注意:如果本地快取失效,並且某個執行緒沒有獲取到更新本地快取的機會,這個執行緒需要立即返回而不是在原地阻塞等待,這種方式可以最大限度的節省伺服器資源和執行緒切換的成本,尤其是像在秒殺系統這種承接瞬時高併發流量的系統中,這種設計能夠節省不少伺服器資源。這種執行緒未獲取到更新資料的機會而快速返回的機制,需要客戶端配合在適配處理,也就是說,客戶端對這種情況需要進行靜默處理,不要提示錯誤資訊,也不做其他處理,稍後重新呼叫介面進行重試即可。
4.4 混合型快取設計的優點
採用本地快取+分散式快取的混合型快取架構設計方案存在諸多的優點。其中,本地快取一個很大的優勢就在於不會發生遠端IO操作,效能更高,有利於服務的橫向伸縮,大部分請求會命中本地單機快取。這裡,我們可以從整體的請求鏈路上進行分析。
例如,當前請求鏈路上需要讀取5次分散式快取中的資料,這樣,如果秒殺系統承接了100萬的請求,則會產生500萬讀取分散式快取的IO操作。這成倍的IO風險對於秒殺系統來說,是絕對不能忽視的風險因素,如圖7所示。
可以看到,一次請求會訪問5次分散式快取,這在無形當中就增加了分散式快取的IO成本,這對秒殺系統來說,是不容忽視的風險項,稍有不慎,則系統可能會由於IO瓶頸引發各種事故,最終造成系統崩潰或者當機。所以,在設計秒殺系統時,一定要注意這種放大效應帶來的風險。所以,在高併發大流量的場景下,很有必要精心的設計本地快取。
五、快取重新整理機制
資料存放到快取中,並不是一成不變的,也不會永久存放到快取中。也就是說,存放到快取中的資料終歸是要失效或者過期的,也就是存放到快取中的資料會有相應的生命週期,為此需要以一定的策略對快取中的資料進行重新整理操作,以防止快取中的資料長時間過期而導致大部分流量直接打入資料庫。本節,就從本地快取和分散式快取兩個角度簡單聊聊快取的生命週期。
5.1 本地快取重新整理機制
假設本地快取基於Guava Cache實現,在設計本地快取時,本地快取的容量不宜過大,有效時長不宜過大,並且在設計本地快取時,可以基於版本號機制來實現快取的失效策略。
對於本地快取會實現兩種重新整理機制:
(1)主動重新整理
請求介面傳入的版本號如果大於本地快取中的版本號,說明本地快取已經失效,此時,就需要從分散式快取中重新獲取資料進行重新整理。
(2)被動重新整理
本地快取自動過期,被動從快取中移除,此時,需要從分散式快取中重新獲取資料進行重新整理。
5.2 分散式快取重新整理機制
假設分散式快取基於Redis實現,對於分散式快取來說,也需要設定快取的過期時間,不能讓快取資料永久性駐留到Redis中。相比於本地快取來說,分散式快取的過期時間要稍微長一些,並且分散式快取在重新整理機制上與本地快取略有不同。
(1)主動重新整理
業務資料變更驅動重新整理分散式快取資料。當業務資料發生變更時,會主動重新整理分散式快取中的資料。
(2)被動重新整理
可以基於Redis提供的快取過期策略,比如基於LRU、TTL等策略淘汰快取中的資料。後續在訪問分散式快取中的資料時,如果檢測到分散式快取中的資料已經過期,則會使用一個執行緒來重新整理分散式快取中的資料。
六、資料一致性
可以這麼說,只要系統中使用了快取,就或多或少會涉及到資料一致性的問題,在秒殺系統中,資料一致性的問題主要包括:本地快取與分散式快取資料一致性問題,快取與資料庫資料一致性問題。同時,在資料一致性保證方面,就包括強一致性保證和弱一致性保證。
6.1 強一致性保證
CAP理論為資料的強一致性奠定了理論基礎,但是CAP理論下的資料強一致性,很難做到既保證系統高效能的同時,又要保證資料的絕對一致。在秒殺系統的設計中,我們會將資料的強一致性保證交給資料庫和業務規則來實現,在業務規則層面結合資料庫來實現強一致。
例如,假設使用者在搶購秒殺商品中,快取中存在商品庫存,透過了快取中的校驗邏輯。在真正下單時,還要校驗資料庫中的商品庫存,如果此時資料庫中已經沒有商品剩餘庫存了,則終止下單邏輯,提示使用者商品已售罄。
6.2 弱一致性保證
強一致性保證交由業務規則和資料庫共同約束實現,快取層面的資料就可以實現為弱一致性。也就是說,在很小的一段時間內,允許快取中的資料存在延遲,允許快取中的資料與資料庫中的資料在短時間內的不一致,只要在可接受的時間範圍內最終達到一致即可。充分發揮快取的實際作用,即:快取資料,提供系統的讀寫效能和抗系統流量。
七、快取落地實現
在秒殺系統中本地快取和分散式快取相結合,能夠抗住進入秒殺系統內部的大部分流量。並且在技術選型上,假設本地快取預設基於Guava Cache實現,分散式快取預設基於Redis實現。並且本地快取不僅僅只是支援Guava Cache,分散式快取不僅僅只是支援Redis,在程式碼層面,都是面向介面程式設計,而非面向具體實現類程式設計,不管是本地快取還是分散式快取,都可以根據簡單的配置切換具體的實現方式。
7.1 擴充套件性描述
程式碼具備良好的擴充套件性,後續維護和升級的成本就比較低。相反,如果程式碼寫的雜亂無章,猶如“屎山”,那後期維護起來是相當痛苦的,誰也不想天天面對著一堆“屎山”,哪來有問題改哪裡。所以,從一開始寫的程式碼就要有良好的擴充套件性,方便後期的維護和升級。
假設秒殺系統整體基於SpringBoot+SpringCloud Alibaba技術棧實現,那如何寫程式碼具備良好的擴充套件性呢?總體的原則就是面向介面程式設計,而非面向具體的實現類程式設計,具體業務邏輯裡依賴的是介面,而非實現類,在介面不變的前提下,可以隨時切換具體的實現類,也可以隨時新增介面的實現類。業務中可以根據配置載入介面的某個具體實現類。
7.2 本地快取落地實現
本地快取的落地實現示意圖如圖8所示。
可以看到,具體秒殺業務中會依賴本地快取的介面,而非具體的實現類。本地快取的介面可以有多個實現類,在秒殺業務中可以根據具體的配置項指定要載入並使用哪個實現類,也可以根據具體的需求和業務場景隨時新增本地介面的實現類,大大提高了程式的擴充套件性。
7.3 分散式快取落地實現
分散式快取的落地實現示意圖如圖9所示。
可以看到,分散式快取在擴充套件性方面的設計與本地快取類似,同樣是秒殺系統在具體業務中依賴分散式快取的介面,而非分散式快取的具體實現類。分散式快取的介面可以有多個實現類,在秒殺業務中可以根據具體的配置項載入並例項化具體的實現類,也可以根據具體的需求和業務場景新增分散式快取介面的實現類,提高了實現分散式快取程式的擴充套件性。
八、總結
快取不僅僅可以用來儲存熱點資料,提升熱點資料的讀效能,還是業務系統中抗高併發、大流量的利器。以秒殺系統為例,採用本地快取+分散式快取的混合型快取方案時,如果整個秒殺系統前置的流量管控、流量清洗和限流等是秒殺系統流量洪峰的第一道防線,則本地快取就是抗流量洪峰的第二道防線,而分散式快取就是第三道防線,經過層層流量過濾,最終進入資料庫的流量就比較可控了。
同時,引入本地快取+分散式快取的混合型快取方案後,要考慮快取的重新整理機制,資料一致性問題,在程式碼落地的過程中,還要最大程度避免快取穿透、擊穿和雪崩問題,並實現程式碼的高度可擴充套件性。
在提供的開源方案中,已經解決了快取穿透、擊穿和雪崩問題,開源地址如下:
- GitHub:https://github.com/binghe001/spring-redis
- Gitee:https://gitee.com/binghe001/spring-redis
- GitCode:https://gitcode.net/binghe001/spring-redis
如果開源方案對你有點幫助或者啟發,歡迎在程式碼倉庫給個Star,讓更過的小夥伴看到它,互相學習,一起進步。
好了,今天就到這兒吧,我是冰河,我們下期見~~