動態

詳情 返回 返回

互聯網大廠的緩存策略:抵抗超高併發的秘密武器,已開源! - 動態 詳情

大家好,我是冰河~~

最近,有小夥伴私信我:冰哥,我最近出去面試,面試官問我如何設計緩存能讓系統在百萬級別流量下仍能平穩運行,我當時沒回答上來。接着,面試官問我之前的項目是怎麼使用緩存的,我説只是緩存了一些數據。當時確實想不到緩存還有哪些用處,估計這次面試是掛了。冰哥,你可以給我講講互聯網大廠項目是怎麼設計和使用緩存的嗎?

本文緩存方案已經開源,開源地址如下,如果開源方案對你有點幫助或者啓發,歡迎在代碼倉庫給個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,讓更過的小夥伴看到它,互相學習,一起進步。

好了,今天就到這兒吧,我是冰河,我們下期見~~

user avatar u_15988698 頭像 ucrx2py9 頭像 huli_5f06b98ab5a44 頭像 syntaxerror 頭像 wenweneryadedahuoji 頭像 kuanrongdeshanyang 頭像 winnn 頭像 youyudeshuanggang 頭像 jueqiangdeqianbi 頭像 ourbmc 頭像
點贊 10 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.