最近,有小伙伴私信我:冰哥,我最近出去面試,面試官問(wèn)我如何設(shè)計(jì)緩存能讓系統(tǒng)在百萬(wàn)級(jí)別流量下仍能平穩(wěn)運(yùn)行,我當(dāng)時(shí)沒(méi)回答上來(lái)。接著,面試官問(wèn)我之前的項(xiàng)目是怎么使用緩存的,我說(shuō)只是緩存了一些數(shù)據(jù)。當(dāng)時(shí)確實(shí)想不到緩存還有哪些用處,估計(jì)這次面試是掛了。冰哥,你可以給我講講互聯(lián)網(wǎng)大廠項(xiàng)目是怎么設(shè)計(jì)和使用緩存的嗎?
一、前言
通過(guò)這位小伙伴的自述,我們明顯感受到這位小伙伴對(duì)緩存的認(rèn)識(shí)還是停留在簡(jiǎn)單的存儲(chǔ)數(shù)據(jù)上,沒(méi)有對(duì)使用緩存背后的場(chǎng)景和實(shí)現(xiàn)邏輯進(jìn)行深層次的思考。
在互聯(lián)網(wǎng)大廠項(xiàng)目中,緩存也是一種必不可少的組件,那使用緩存僅僅是為了緩存熱點(diǎn)數(shù)據(jù),提升讀性能嗎?如果你對(duì)緩存的認(rèn)識(shí)只是停留在這里,那就未免太淺顯了。
今天,我們就以高并發(fā)、大流量業(yè)務(wù)場(chǎng)景中最具代表性的 秒殺系統(tǒng) 為例,采用市面上大家都比較熟悉的技術(shù),一起探究下 秒殺系統(tǒng) 背后是如何設(shè)計(jì)和使用緩存的。
二、秒殺系統(tǒng)緩存核心訴求
秒殺系統(tǒng)在承接瞬時(shí)高并發(fā)流量時(shí),如果將流量直接打到數(shù)據(jù)庫(kù),那數(shù)據(jù)庫(kù)很有可能因?yàn)榭覆蛔∷查g的高并發(fā)流量而導(dǎo)致崩潰和宕機(jī)。所以,需要對(duì)秒殺系統(tǒng)進(jìn)行極致的緩存設(shè)計(jì),讓大部分流量走緩存。
同時(shí),在設(shè)計(jì)緩存架構(gòu)方案時(shí),為了進(jìn)一步提升性能,將采用 本地緩存+分布式緩存的混合型緩存 設(shè)計(jì)方案,讓本地緩存抗大部分流量,分布式緩存次之,數(shù)據(jù)庫(kù)再次之,如圖1所示
并且針對(duì)秒殺系統(tǒng)這種瞬時(shí)并發(fā)量高的場(chǎng)景,在設(shè)計(jì)緩存時(shí),需要注意的技巧:優(yōu)先讀取本地緩存數(shù)據(jù),如果本地緩存失效,則讀取分布式緩存數(shù)據(jù),并且在同一時(shí)刻,只能有一個(gè)線程更新本地緩存,防止緩存擊穿。
沒(méi)有獲取到本地緩存更新機(jī)會(huì)的其他線程,需要立即返回而不是原地等待。如果分布式緩存失效時(shí),在同一時(shí)刻,也只能有一個(gè)線程更新分布式緩存,防止緩存擊穿。沒(méi)有獲取到分布式緩存更新機(jī)會(huì)的線程,也需要立即返回而不是原地等待。
另外,需要注意的是:我們提出了采用 本地緩存+分布式緩存的混合型緩存設(shè)計(jì)方案,后文會(huì)著重對(duì)這種設(shè)計(jì)進(jìn)行說(shuō)明。
三、秒殺系統(tǒng)緩存使用場(chǎng)景
秒殺系統(tǒng)屬于典型的讀多寫少的高并發(fā)系統(tǒng),應(yīng)對(duì)這種場(chǎng)景的一個(gè)有效措施就是使用緩存,不管是單機(jī)JVM緩存還是以Redis為例的分布式緩存,其讀寫性能都會(huì)比數(shù)據(jù)庫(kù)高得多。所以,在秒殺系統(tǒng)中,為了應(yīng)對(duì)高并發(fā)、大流量的業(yè)務(wù)場(chǎng)景,緩存自然也就成為建設(shè)秒殺系統(tǒng)過(guò)程中必不可少的環(huán)節(jié)。
3.1 秒殺系統(tǒng)接口分析
在秒殺系統(tǒng)中,主要是對(duì)一些讀數(shù)據(jù)的接口設(shè)計(jì)緩存策略,而在這些讀數(shù)據(jù)的接口中,獲取秒殺活動(dòng)列表、獲取秒殺活動(dòng)詳情、獲取秒殺商品列表和獲取秒殺商品詳情的接口流量比其他接口高。
尤其是獲取秒殺商品列表和獲取秒殺商品詳情的接口QPS一般會(huì)高于獲取秒殺活動(dòng)列表和秒殺活動(dòng)詳情的接口,畢竟大部分用戶在秒殺開(kāi)始前就已經(jīng)進(jìn)入到秒殺詳情頁(yè),當(dāng)然這也不是絕對(duì)的,還是要看秒殺系統(tǒng)對(duì)于這些接口的設(shè)計(jì)。
3.2 秒殺系統(tǒng)緩存場(chǎng)景
盡管獲取秒殺商品列表和獲取秒殺商品詳情的接口QPS一般會(huì)高于獲取秒殺活動(dòng)列表和秒殺活動(dòng)詳情的接口,但是我們?cè)谠O(shè)計(jì)緩存時(shí),需要對(duì)這些接口一視同仁,都要以嚴(yán)格的高標(biāo)準(zhǔn)來(lái)設(shè)計(jì)這些接口,不然稍有不慎,一個(gè)接口出現(xiàn)問(wèn)題,就可能導(dǎo)致整場(chǎng)秒殺活動(dòng)以失敗告終。秒殺系統(tǒng)緩存的使用場(chǎng)景如圖2所示。
所以,在秒殺系統(tǒng)中,會(huì)對(duì)獲取秒殺活動(dòng)列表、獲取秒殺活動(dòng)詳情、獲取秒殺商品列表和獲取秒殺商品詳情的接口設(shè)計(jì)緩存策略。
四、混合型緩存設(shè)計(jì)
總體來(lái)說(shuō),在設(shè)計(jì)秒殺系統(tǒng)的緩存過(guò)程中,會(huì)采用 本地緩存+分布式緩存的混合型緩存 設(shè)計(jì)方案。其中,本地緩存指的就是單機(jī)緩存,比如JVM內(nèi)存緩存,單機(jī)Cache緩存。
分布式緩存指的是以分布式的方式集中管理的緩存,比如Memcached、Redis等,如圖3所示。
4.1 抗流量洪峰
良好的緩存設(shè)計(jì)不僅僅能夠提升系統(tǒng)的總體性能,還能作為抗瞬時(shí)流量洪峰的有效防線。
可以這么說(shuō),如果整個(gè)秒殺系統(tǒng)前置的流量管控、流量清洗和限流等是秒殺系統(tǒng)流量洪峰的第一道防線,則本地緩存就是抗流量洪峰的第二道防線,而分布式緩存就是第三道防線,如圖4所示。
使用緩存能夠抗一定的流量洪峰,經(jīng)過(guò)前置的流量管控、流量清洗和限流等措施的第一道防線、本地緩存的第二道防線、分布式緩存的第三道防線,真正進(jìn)入數(shù)據(jù)庫(kù)的流量就會(huì)比較小了。
4.2 緩存集群方案
從緩存集群模式的角度去分析,每臺(tái)服務(wù)器甚至JVM實(shí)例都會(huì)擁有自己獨(dú)立的本地緩存,在承載大并發(fā)流量時(shí),,以本地緩存為主,分布式緩存次之,如圖5所示。
可以看到,從緩存的集群模式角度來(lái)看,每臺(tái)服務(wù)器都會(huì)自己獨(dú)立本地緩存,除了前置的流程管控、流量清洗和限流等措施構(gòu)筑的流量洪峰第一道防線外。
本地緩存會(huì)承接剩余的大部分流量,構(gòu)筑成流量洪峰的第二道防線,而分布式緩存則是流量洪峰的第三道防線。并且在緩存的設(shè)計(jì)上,分布式緩存的作用主要是協(xié)調(diào)和同步最新數(shù)據(jù)到本地緩存。
也就是說(shuō),只有本地緩存失效時(shí),才會(huì)訪問(wèn)分布式緩存,將分布式緩存中的數(shù)據(jù)更新到本地緩存中,并且同一時(shí)刻只能有一個(gè)線程對(duì)本地緩存進(jìn)行更新操作,以避免多個(gè)線程并發(fā)更新本地緩存。
同樣的,如果分布式緩存失效,則同一時(shí)刻只能有一個(gè)線程訪問(wèn)數(shù)據(jù)庫(kù)來(lái)獲取對(duì)應(yīng)的數(shù)據(jù),并將其更新到分布式緩存。
在集群模式下,我們應(yīng)該盡最大努力將流量攔截在本地緩存,避免過(guò)多的請(qǐng)求訪問(wèn)分布式緩存,提高秒殺系統(tǒng)的性能,并且降低秒殺系統(tǒng)由于大量的遠(yuǎn)程IO導(dǎo)致的各種風(fēng)險(xiǎn)。
4.3 緩存交互流程
采用本地緩存+分布式緩存的混合型緩存架構(gòu)設(shè)計(jì)方案時(shí),在讀取緩存數(shù)據(jù)時(shí),會(huì)優(yōu)先讀取本地緩存的數(shù)據(jù),如果本地緩存未開(kāi)啟,或者已經(jīng)失效,此時(shí)就會(huì)使用分布式緩存,也就是說(shuō),優(yōu)先讀取本地緩存中的數(shù)據(jù),如果本地緩存未開(kāi)啟或者緩存數(shù)據(jù)失效,則讀取分布式緩存中的數(shù)據(jù),如圖6所示。
可以看到,只有在本地緩存未開(kāi)啟或者緩存失效的情況下,才會(huì)去訪問(wèn)分布式緩存,讀取分布式緩存中的數(shù)據(jù),并且在同一個(gè)時(shí)刻只能有一個(gè)線程更新本地緩存中的數(shù)據(jù),這種方式可以最大限度減少遠(yuǎn)程IO為秒殺系統(tǒng)帶來(lái)的風(fēng)險(xiǎn)。具體的流程如下所示。
(1)判斷本地緩存是否開(kāi)啟,如果開(kāi)啟則進(jìn)行第2步,否則進(jìn)行第4步。
(2)判斷本地緩存是否失效,如果未失效,則進(jìn)行第3步,否則進(jìn)行第4步。
(3)讀取本地緩存數(shù)據(jù),讀取緩存流程結(jié)束。
(4)判斷分布式緩存是否開(kāi)啟,如果開(kāi)啟則進(jìn)行第5步,否則進(jìn)行第7步。
(5)判斷分布式緩存是否失效,如果未失效,則進(jìn)行第6步,否則進(jìn)行第7步。
(6)讀取分布式緩存數(shù)據(jù),同一時(shí)刻只有一個(gè)線程更新本地緩存數(shù)據(jù),讀取緩存流程結(jié)束。
(7)讀取數(shù)據(jù)庫(kù)數(shù)據(jù),同一時(shí)刻只有一個(gè)線程更新分布式緩存數(shù)據(jù),讀取緩存流程結(jié)束。
這里,有一個(gè)設(shè)計(jì)技巧需要大家注意:如果本地緩存失效,并且某個(gè)線程沒(méi)有獲取到更新本地緩存的機(jī)會(huì),這個(gè)線程需要立即返回而不是在原地阻塞等待,這種方式可以最大限度的節(jié)省服務(wù)器資源和線程切換的成本。
尤其是像在秒殺系統(tǒng)這種承接瞬時(shí)高并發(fā)流量的系統(tǒng)中,這種設(shè)計(jì)能夠節(jié)省不少服務(wù)器資源。這種線程未獲取到更新數(shù)據(jù)的機(jī)會(huì)而快速返回的機(jī)制,需要客戶端配合在適配處理,也就是說(shuō),客戶端對(duì)這種情況需要進(jìn)行靜默處理,不要提示錯(cuò)誤信息,也不做其他處理,稍后重新調(diào)用接口進(jìn)行重試即可。
4.4 混合型緩存設(shè)計(jì)的優(yōu)點(diǎn)
采用本地緩存+分布式緩存的混合型緩存架構(gòu)設(shè)計(jì)方案存在諸多的優(yōu)點(diǎn)。其中,本地緩存一個(gè)很大的優(yōu)勢(shì)就在于不會(huì)發(fā)生遠(yuǎn)程IO操作,性能更高,有利于服務(wù)的橫向伸縮,大部分請(qǐng)求會(huì)命中本地單機(jī)緩存。這里,我們可以從整體的請(qǐng)求鏈路上進(jìn)行分析。
例如,當(dāng)前請(qǐng)求鏈路上需要讀取5次分布式緩存中的數(shù)據(jù),這樣,如果秒殺系統(tǒng)承接了100萬(wàn)的請(qǐng)求,則會(huì)產(chǎn)生500萬(wàn)讀取分布式緩
可以看到,一次請(qǐng)求會(huì)訪問(wèn)5次分布式緩存,這在無(wú)形當(dāng)中就增加了分布式緩存的IO成本,這對(duì)秒殺系統(tǒng)來(lái)說(shuō),是不容忽視的風(fēng)險(xiǎn)項(xiàng),稍有不慎,則系統(tǒng)可能會(huì)由于IO瓶頸引發(fā)各種事故,最終造成系統(tǒng)崩潰或者宕機(jī)。
所以,在設(shè)計(jì)秒殺系統(tǒng)時(shí),一定要注意這種放大效應(yīng)帶來(lái)的風(fēng)險(xiǎn)。所以,在高并發(fā)大流量的場(chǎng)景下,很有必要精心的設(shè)計(jì)本地緩存。
五、緩存刷新機(jī)制
數(shù)據(jù)存放到緩存中,并不是一成不變的,也不會(huì)永久存放到緩存中。也就是說(shuō),存放到緩存中的數(shù)據(jù)終歸是要失效或者過(guò)期的,也就是存放到緩存中的數(shù)據(jù)會(huì)有相應(yīng)的生命周期。
為此需要以一定的策略對(duì)緩存中的數(shù)據(jù)進(jìn)行刷新操作,以防止緩存中的數(shù)據(jù)長(zhǎng)時(shí)間過(guò)期而導(dǎo)致大部分流量直接打入數(shù)據(jù)庫(kù)。本節(jié),就從本地緩存和分布式緩存兩個(gè)角度簡(jiǎn)單聊聊緩存的生命周期。
5.1 本地緩存刷新機(jī)制
假設(shè)本地緩存基于Guava Cache實(shí)現(xiàn),在設(shè)計(jì)本地緩存時(shí),本地緩存的容量不宜過(guò)大,有效時(shí)長(zhǎng)不宜過(guò)大,并且在設(shè)計(jì)本地緩存時(shí),可以基于版本號(hào)機(jī)制來(lái)實(shí)現(xiàn)緩存的失效策略。
對(duì)于本地緩存會(huì)實(shí)現(xiàn)兩種刷新機(jī)制:
(1)主動(dòng)刷新
請(qǐng)求接口傳入的版本號(hào)如果大于本地緩存中的版本號(hào),說(shuō)明本地緩存已經(jīng)失效,此時(shí),就需要從分布式緩存中重新獲取數(shù)據(jù)進(jìn)行刷新。
(2)被動(dòng)刷新
本地緩存自動(dòng)過(guò)期,被動(dòng)從緩存中移除,此時(shí),需要從分布式緩存中重新獲取數(shù)據(jù)進(jìn)行刷新。
5.2 分布式緩存刷新機(jī)制
假設(shè)分布式緩存基于Redis實(shí)現(xiàn),對(duì)于分布式緩存來(lái)說(shuō),也需要設(shè)置緩存的過(guò)期時(shí)間,不能讓緩存數(shù)據(jù)永久性駐留到Redis中。相比于本地緩存來(lái)說(shuō),分布式緩存的過(guò)期時(shí)間要稍微長(zhǎng)一些,并且分布式緩存在刷新機(jī)制上與本地緩存略有不同。
(1)主動(dòng)刷新
業(yè)務(wù)數(shù)據(jù)變更驅(qū)動(dòng)刷新分布式緩存數(shù)據(jù)。當(dāng)業(yè)務(wù)數(shù)據(jù)發(fā)生變更時(shí),會(huì)主動(dòng)刷新分布式緩存中的數(shù)據(jù)。
(2)被動(dòng)刷新
可以基于Redis提供的緩存過(guò)期策略,比如基于LRU、TTL等策略淘汰緩存中的數(shù)據(jù)。后續(xù)在訪問(wèn)分布式緩存中的數(shù)據(jù)時(shí),如果檢測(cè)到分布式緩存中的數(shù)據(jù)已經(jīng)過(guò)期,則會(huì)使用一個(gè)線程來(lái)刷新分布式緩存中的數(shù)據(jù)。
六、數(shù)據(jù)一致性
可以這么說(shuō),只要系統(tǒng)中使用了緩存,就或多或少會(huì)涉及到數(shù)據(jù)一致性的問(wèn)題,在秒殺系統(tǒng)中,數(shù)據(jù)一致性的問(wèn)題主要包括:本地緩存與分布式緩存數(shù)據(jù)一致性問(wèn)題,緩存與數(shù)據(jù)庫(kù)數(shù)據(jù)一致性問(wèn)題。同時(shí),在數(shù)據(jù)一致性保證方面,就包括強(qiáng)一致性保證和弱一致性保證。
6.1 強(qiáng)一致性保證
CAP理論為數(shù)據(jù)的強(qiáng)一致性奠定了理論基礎(chǔ),但是CAP理論下的數(shù)據(jù)強(qiáng)一致性,很難做到既保證系統(tǒng)高性能的同時(shí),又要保證數(shù)據(jù)的絕對(duì)一致。在秒殺系統(tǒng)的設(shè)計(jì)中,我們會(huì)將數(shù)據(jù)的強(qiáng)一致性保證交給數(shù)據(jù)庫(kù)和業(yè)務(wù)規(guī)則來(lái)實(shí)現(xiàn),在業(yè)務(wù)規(guī)則層面結(jié)合數(shù)據(jù)庫(kù)來(lái)實(shí)現(xiàn)強(qiáng)一致。
例如,假設(shè)用戶在搶購(gòu)秒殺商品中,緩存中存在商品庫(kù)存,通過(guò)了緩存中的校驗(yàn)邏輯。在真正下單時(shí),還要校驗(yàn)數(shù)據(jù)庫(kù)中的商品庫(kù)存,如果此時(shí)數(shù)據(jù)庫(kù)中已經(jīng)沒(méi)有商品剩余庫(kù)存了,則終止下單邏輯,提示用戶商品已售罄。
6.2 弱一致性保證
強(qiáng)一致性保證交由業(yè)務(wù)規(guī)則和數(shù)據(jù)庫(kù)共同約束實(shí)現(xiàn),緩存層面的數(shù)據(jù)就可以實(shí)現(xiàn)為弱一致性。也就是說(shuō),在很小的一段時(shí)間內(nèi),允許緩存中的數(shù)據(jù)存在延遲,允許緩存中的數(shù)據(jù)與數(shù)據(jù)庫(kù)中的數(shù)據(jù)在短時(shí)間內(nèi)的不一致,只要在可接受的時(shí)間范圍內(nèi)最終達(dá)到一致即可。充分發(fā)揮緩存的實(shí)際作用,即:緩存數(shù)據(jù),提供系統(tǒng)的讀寫性能和抗系統(tǒng)流量。
七、緩存落地實(shí)現(xiàn)
在秒殺系統(tǒng)中本地緩存和分布式緩存相結(jié)合,能夠抗住進(jìn)入秒殺系統(tǒng)內(nèi)部的大部分流量。并且在技術(shù)選型上,假設(shè)本地緩存默認(rèn)基于Guava Cache實(shí)現(xiàn),分布式緩存默認(rèn)基于Redis實(shí)現(xiàn)。
并且本地緩存不僅僅只是支持Guava Cache,分布式緩存不僅僅只是支持Redis,在代碼層面,都是面向接口編程,而非面向具體實(shí)現(xiàn)類編程,不管是本地緩存還是分布式緩存,都可以根據(jù)簡(jiǎn)單的配置切換具體的實(shí)現(xiàn)方式。