溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

架構(gòu)成長之路:分布式緩存架構(gòu)設(shè)計分析

發(fā)布時間:2020-04-22 00:31:49 來源:網(wǎng)絡(luò) 閱讀:506 作者:若小寒 欄目:編程語言

前言

在高并發(fā)場景下,需要通過緩存來減少數(shù)據(jù)庫的壓力,使得大量的訪問進來能夠命中緩存,只有少量的需要到數(shù)據(jù)庫層。由于緩存基于內(nèi)存,可支持的并發(fā)量遠遠大于基于硬盤的數(shù)據(jù)庫。所以對于高并發(fā)設(shè)計,緩存的設(shè)計是必不可少的一環(huán)。

一、為什么要使用緩存

為什么要使用緩存呢?源于人類的一個夢想,就是多快好省的建設(shè)社會主義。多快好?。亢芏嗫蛻舳歼@么要求,但是作為具體做技術(shù)的你,當然知道,好就不能快,多就沒法省。

可是沒辦法,客戶都這樣要求:

這個能不能便宜一點,你咋這么貴呀,你看人家都很便宜的。(您好,這種打折的房間比較靠里,是不能面向大海的)
你們的性能怎么這么差啊,用你這個系統(tǒng)跑的這么慢,你看人家廣告中說速度能達到多少多少。(您好,你如果買一個頂配的,我們也是有這種性能的)
你們服務(wù)不行啊,你就不能彬彬有禮,穿著整齊,送點水果瓜子啥的?(您好,我們蘭州拉面館沒有這項服務(wù),可以去對面的俏江南看一下)
這么貴的菜,一盤就這么一點點,都吃不飽,就不能上一大盤么。(您好,對面的蘭州拉面10塊錢一大碗)
怎么辦呢?勞動人民還是很有智慧的,就是聚焦核心需求,讓最最核心的部分享用好和快,而非核心的部門就多和省就可以了。
你可以大部分時間住在公司旁邊的出租屋里面,但是出去度假的一個星期,選一個面朝大海,春暖花開的五星級酒店。
你可以大部分時間都擠地鐵,擠公交,跋涉2個小時從北五環(huán)到南五環(huán),但是有急事的時候,你可以打車,想旅游的時候,可以租車。
你可以大部分時間都吃普通的餐館,而朋友來了,就去高級飯店里面搓一頓。

在計算機世界也是這樣樣子的,如圖所示。
架構(gòu)成長之路:分布式緩存架構(gòu)設(shè)計分析

越是快的設(shè)備,存儲量越小,越貴,而越是慢的設(shè)備,存儲量越大,越便宜。

對于一家電商來講,我們既希望存儲越來越多的數(shù)據(jù),因為數(shù)據(jù)將來就是資產(chǎn),就是財富,只有有了數(shù)據(jù),我們才知道用戶需要什么,同時又希望當我想訪問這些數(shù)據(jù)的時候,能夠快速的得到,雙十一拼的就是速度和用戶體驗,要讓用戶有流暢的感覺。

所以我們要講大量的數(shù)據(jù)都保存下來,放在便宜的存儲里面,同時將經(jīng)常訪問的,放在貴的,小的存儲里面,當然貴的快的往往比較資源有限,因而不能長時間被某些數(shù)據(jù)長期霸占,所以要大家輪著用,所以叫緩存,也就是暫時存著。

二、都有哪些類型的緩存

當一個應(yīng)用剛開始的時候,架構(gòu)比較簡單,往往就是一個Tomcat,后面跟著一個數(shù)據(jù)庫。
架構(gòu)成長之路:分布式緩存架構(gòu)設(shè)計分析

簡單的應(yīng)用,并發(fā)量不大的時候,當然沒有問題。

然而數(shù)據(jù)庫相當于我們應(yīng)用的中軍大帳,是我們整個架構(gòu)中最最關(guān)鍵的一部分,也是最不能掛,也最不能會被攻破的一部分,因而所有對數(shù)據(jù)庫的訪問都需要一道屏障來進行保護,常用的就是緩存。
我們以Tomcat為分界線,之外我們稱為接入層,接入層當然應(yīng)該有緩存,還有CDN。
Tomcat之后,我們稱為應(yīng)用層,應(yīng)用層也應(yīng)該有緩存,這是我們這一節(jié)討論的重點。
最簡單的方式就是Tomcat里面有一層緩存,常稱為本地緩存LocalCache。

這類的緩存常見的有Ehcache和Guava Cache,由于這類緩存在Tomcat本地,因而訪問速度是非??斓摹?/p>

但是本地緩存有個比較大的缺點,就是緩存是放在JVM里面的,會面臨Full GC的問題,一旦出現(xiàn)了FullGC,就會對應(yīng)用的性能和相應(yīng)時間產(chǎn)生影響,當然也可以嘗試jemalloc的分配方式。

還有一種方式,就是在Tomcat和Mysql中間加了一層Cache,我們常稱為分布式緩存。
架構(gòu)成長之路:分布式緩存架構(gòu)設(shè)計分析

分布式緩存常見的有Memcached和Redis,兩者各有優(yōu)缺點。

Memcached適合做簡單的key-value存儲,內(nèi)存使用率比較高,而且由于是多核處理,對于比較大的數(shù)據(jù),性能較好。

但是缺點也比較明顯,Memcached嚴格來講沒有集群機制,橫向擴展完全靠客戶端來實現(xiàn)。另外Memcached無法持久化,一旦掛了數(shù)據(jù)就都丟失了,如果想實現(xiàn)高可用,也是需要客戶端進行雙寫才可以。

所以可以看出Memcached真的是設(shè)計出來,簡簡單單為了做一個緩存的。
架構(gòu)成長之路:分布式緩存架構(gòu)設(shè)計分析

Redis的數(shù)據(jù)結(jié)構(gòu)就豐富的多了,單線程的處理所有的請求,對于比較大的數(shù)據(jù),性能稍微差一點。
架構(gòu)成長之路:分布式緩存架構(gòu)設(shè)計分析

Redis提供持久化的功能,包括RDB的全量持久化,或者AOF的增量持久化,從而使得Redis掛了,數(shù)據(jù)是有機會恢復(fù)的。

Redis提供成熟的主備同步,故障切換的功能,從而保證了高可用性。

所以很多地方管Redis稱為內(nèi)存數(shù)據(jù)庫,因為他的一些特性已經(jīng)有了數(shù)據(jù)庫的影子。

這也是很多人愿意用Redis的原因,集合了緩存和數(shù)據(jù)庫的優(yōu)勢,但是往往會濫用這些優(yōu)勢,從而忽略了架構(gòu)層面的設(shè)計,使得Redis集群有很大的風險。

很多情況下,會將Redis當做數(shù)據(jù)庫使用,開啟持久化和主備同步機制,以為就可以高枕無憂了。
架構(gòu)成長之路:分布式緩存架構(gòu)設(shè)計分析

然而Redis的持久化機制,全量持久化則往往需要額外較大的內(nèi)存,而在高并發(fā)場景下,內(nèi)存本來就很緊張,如果造成swap,就會影響性能。增量持久化也涉及到寫磁盤和fsync,也是會拖慢處理的速度,在平時還好,如果高并發(fā)場景下,仍然會影響吞吐量。

所以在架構(gòu)設(shè)計角度,緩存就是緩存,要意識到數(shù)據(jù)會隨時丟失的,要意識到緩存的存著的目的是攔截到數(shù)據(jù)庫的請求。如果為了保證緩存的數(shù)據(jù)不丟失,從而影響了緩存的吞吐量,甚至穩(wěn)定性,讓緩存響應(yīng)不過來,甚至掛掉,所有的請求擊穿到數(shù)據(jù)庫,就是更加嚴重的事情了。

如果非常需要進行持久化,可以考慮使用levelDB此類的,對于隨機寫入性能較好的key-value持久化存儲,這樣只有部分的確需要持久化的數(shù)據(jù),才進行持久化,而非無論什么數(shù)據(jù),通通往Redis里面扔,同時統(tǒng)一開啟了持久化。

三、基于緩存的架構(gòu)設(shè)計要點

所以基于緩存的設(shè)計:

1、多層次

這樣某一層的緩存掛了,還有另一層可以撐著,等待緩存的修復(fù),例如分布式緩存因為某種原因掛了,因為持久化的原因,同步機制的原因,內(nèi)存過大的原因等,修復(fù)需要一段時間,在這段時間內(nèi),至少本地緩存可以抗一陣,不至于一下子就擊穿數(shù)據(jù)庫。而且對于特別特別熱的數(shù)據(jù),熱到導(dǎo)致集中式的緩存處理不過來,網(wǎng)卡也被打滿的情況,由于本地緩存不需要遠程調(diào)用,也是分布在應(yīng)用層的,可以緩解這種問題。

2、分場景

到底要解決什么問題,可以選擇不同的緩存。是要存儲大的無格式的數(shù)據(jù),還是要存儲小的有格式的數(shù)據(jù),還是要存儲一定需要持久化的數(shù)據(jù)。具體的場景下一節(jié)詳細談。

3、要分片

使得每一個緩存實例都不大,但是實例數(shù)目比較多,這樣一方面可以實現(xiàn)負載均衡,防止單個實例稱為瓶頸或者熱點,另一方面如果一個實例掛了,影響面會小很多,高可用性大大增強。分片的機制可以在客戶端實現(xiàn),可以使用中間件實現(xiàn),也可以使用Redis的Cluster的方式,分片的算法往往都是哈希取模,或者一致性哈希。

四、緩存的使用場景

當你的應(yīng)用扛不住,知道要使用緩存了,應(yīng)該怎么做呢?

場景1:和數(shù)據(jù)庫中的數(shù)據(jù)結(jié)構(gòu)保持一致,原樣緩存
這種場景是最常見的場景,也是很多架構(gòu)使用緩存的適合,最先涉及到的場景。

基本就是數(shù)據(jù)庫里面啥樣,我緩存也啥樣,數(shù)據(jù)庫里面有商品信息,緩存里面也放商品信息,唯一不同的是,數(shù)據(jù)庫里面是全量的商品信息,緩存里面是最熱的商品信息。

每當應(yīng)用要查詢商品信息的時候,先查緩存,緩存沒有就查數(shù)據(jù)庫,查出來的結(jié)果放入緩存,從而下次就查到了。

這個是緩存最最經(jīng)典的更新流程。這種方式簡單,直觀,很多緩存的庫都默認支持這種方式。

場景2:列表排序分頁場景的緩存

有時候我們需要獲得一些列表數(shù)據(jù),并對這些數(shù)據(jù)進行排序和分頁。

例如我們想獲取點贊最多的評論,或者最新的評論,然后列出來,一頁一頁的翻下去。

在這種情況下,緩存里面的數(shù)據(jù)結(jié)構(gòu)和數(shù)據(jù)庫里面完全不一樣。

如果完全使用數(shù)據(jù)庫進行實現(xiàn),則按照某種條件將所有的行查詢出來,然后按照某個字段進行排序,然后進行分頁,一頁一頁的展示。

但是當數(shù)據(jù)量比較大的時候,這種方式往往成為瓶頸,首先涉及的數(shù)據(jù)庫行數(shù)比較多,而且排序也是個很慢的活,盡管可能有索引,分頁也是翻頁到最后,越是慢。

在緩存里面,就沒必要每行一個key了,而是可以使用Redis的列表方式進行存儲,當然列表的長短是有限制的,肯定放不下數(shù)據(jù)庫里面這么多,但是大家會發(fā)現(xiàn)其實對于所有的列表,用戶往往沒有耐心看個十頁八頁的,例如百度上搜個東西,也是有排序和分頁的,但是你每次都往后翻了嗎,每頁就十條,就算是十頁,或者一百頁,也就一千條數(shù)據(jù),如果保持ID的話,完全放的下。

如果已經(jīng)排好序,放在Redis里面,那取出列表,翻頁就非??炝?。

可以后臺有一個線程,異步的初始化和刷新緩存,在緩存里面保存一個時間戳,當有更新的時候,刷新時間戳,異步任務(wù)發(fā)現(xiàn)時間戳改變了,就刷新緩存。

場景3:計數(shù)緩存
計數(shù)對于數(shù)據(jù)庫來講,是一個非常繁重的工作,需要查詢大量的行,最后得出計數(shù)的結(jié)論,當數(shù)據(jù)改變的時候,需要重新刷一遍,非常影響性能。

因此可以有一個計數(shù)服務(wù),后端是一個緩存,將計數(shù)作為結(jié)果放在緩存里面,當數(shù)據(jù)有改變的時候,調(diào)用計數(shù)服務(wù)增加或者減少計數(shù),而非通過異步數(shù)據(jù)庫count來更新緩存。

計數(shù)服務(wù)可以使用Redis進行單個計數(shù),或者hash表進行批量計數(shù)

場景4:重構(gòu)維度緩存

有時候數(shù)據(jù)庫里面保持的數(shù)據(jù)的維度是為了寫入方便,而非為了查詢方便的,然而同時查詢過程,也需要處理高并發(fā),因而需要為了查詢方便,將數(shù)據(jù)重新以另一個維度存儲一遍,或者說將多給數(shù)據(jù)庫的內(nèi)容聚合一下,再存儲一遍,從而不用每次查詢的時候都重新聚合,如果還是放在數(shù)據(jù)庫,比較難維護,放在緩存就好一些。

例如一個商品的所有的帖子和帖子的用戶,以及一個用戶發(fā)表過的所有的帖子就是屬于兩個維度。

這需要寫入一個維度的時候,同時異步通知,更新緩存中的另一個維度。

在這種場景下,數(shù)據(jù)量相對比較大,因而單純用內(nèi)存緩存memcached或者redis難以支撐,往往會選擇使用levelDB進行存儲,如果levelDB的性能跟不上,可以考慮在levelDB之前,再來一層memcached。

場景5:較大的詳情內(nèi)容數(shù)據(jù)緩存
對于評論的詳情,或者帖子的詳細內(nèi)容,屬于非結(jié)構(gòu)化的,而且內(nèi)容比較大,因而使用memcached比較好。

五、緩存三大矛盾問題

1、緩存實時性和一致性問題:當有了寫入后咋辦?

雖然使用了緩存,大家心里都有一個預(yù)期,就是實時性和一致性得不到完全的保證,畢竟數(shù)據(jù)保存了多份,數(shù)據(jù)庫一份,緩存中一份,當數(shù)據(jù)庫中因?qū)懭攵a(chǎn)生了新的數(shù)據(jù),往往緩存是不會和數(shù)據(jù)庫操作放在一個事務(wù)里面的,如何將新的數(shù)據(jù)更新到緩存里面,什么時候更新到緩存里面,不同的策略不一樣。

從用戶體驗角度,當然是越實時越好,用戶體驗越流暢,完全從這個角度出發(fā),就應(yīng)該有了寫入,馬上廢棄緩存,觸發(fā)一次數(shù)據(jù)庫的讀取,從而更新緩存。但是這和第三個問題,高并發(fā)就矛盾了,如果所有的都實時從數(shù)據(jù)庫里面讀取,高并發(fā)場景下,數(shù)據(jù)庫往往受不了。

2、緩存的穿透問題:當沒有讀到咋辦?

為什么會出現(xiàn)緩存讀取不到的情況呢?

第一:可能讀取的是冷數(shù)據(jù),原來從來沒有訪問過,所以需要到數(shù)據(jù)庫里面查詢一下,然后放入緩存,再返回給客戶。

第二:可能數(shù)據(jù)因為有了寫入,被實時的從緩存中刪除了,就如第一個問題中描述的那樣,為了保證實時性,當數(shù)據(jù)庫中的數(shù)據(jù)更新了之后,馬上刪除緩存中的數(shù)據(jù),導(dǎo)致這個時候的讀取讀不到,需要到數(shù)據(jù)庫里面查詢后,放入緩存,再返回給客戶。

第三:可能是緩存實效了,每個緩存數(shù)據(jù)都會有實效時間,過了一段時間沒有被訪問,就會失效,這個時候數(shù)據(jù)就訪問不到了,需要訪問數(shù)據(jù)庫后,再放入緩存。

第四:數(shù)據(jù)被換出,由于緩存內(nèi)存是有限的,當使用快滿了的時候,就會使用類似LRU策略,將不經(jīng)常使用的數(shù)據(jù)換出,所以也要訪問數(shù)據(jù)庫。

第五:后端確實也沒有,應(yīng)用訪問緩存沒有,于是查詢數(shù)據(jù)庫,結(jié)果數(shù)據(jù)庫里面也沒有,只好返回客戶為空,但是尷尬的是,每次出現(xiàn)這種情況的時候,都會面臨著一次數(shù)據(jù)庫的訪問,純屬浪費資源,常用的方法是,講這個key對應(yīng)的結(jié)果為空的事實也進行緩存,這樣緩存可以命中,但是命中后告訴客戶端沒有,減少了數(shù)據(jù)庫的壓力。

無論哪種原因?qū)е碌淖x取緩存讀不到的情況,該怎么辦?是個策略問題。

一種是同步訪問數(shù)據(jù)庫后,放入緩存,再返回給客戶,這樣實時性最好,但是給數(shù)據(jù)庫的壓力也最大。

另一種方式就是異步的訪問數(shù)據(jù)庫,暫且返回客戶一個fallback值,然后同時觸發(fā)一個異步更新,這樣下次就有了,這樣數(shù)據(jù)庫壓力小很多,但是用戶就訪問不到實時的數(shù)據(jù)了。

3、緩存對數(shù)據(jù)庫高并發(fā)訪問:都來訪問數(shù)據(jù)庫咋辦?

我們本來使用緩存,是來攔截直接訪問數(shù)據(jù)庫請求的,從而保證數(shù)據(jù)庫大本營永遠處于健康的狀態(tài)。但是如果一遇到不命中,就訪問數(shù)據(jù)庫的話,平時沒有什么問題,但是大促情況下,數(shù)據(jù)庫是受不了的。

一種情況是多個客戶端,并發(fā)狀態(tài)下,都不命中了,于是并發(fā)的都來訪問數(shù)據(jù)庫,其實只需要訪問一次就好,這種情況可以通過加鎖,只有一個到后端來實現(xiàn)。

另外就是即便采取了上述的策略,依然并發(fā)量非常大,后端的數(shù)據(jù)庫依然受不了,則需要通過降低實時性,將緩存攔在數(shù)據(jù)庫前面,暫且撐住,來解決。

六、解決緩存三大矛盾的刷新策略

1、實時策略

所謂的實時策略,是平時緩存使用的最常用的策略,也是保持實時性最好的策略。

讀取的過程,應(yīng)用程序先從cache取數(shù)據(jù),沒有得到,則從數(shù)據(jù)庫中取數(shù)據(jù),成功后,放到緩存中。如果命中,應(yīng)用程序從cache中取數(shù)據(jù),取到后返回。

寫入的過程,把數(shù)據(jù)存到數(shù)據(jù)庫中,成功后,再讓緩存失效,失效后下次讀取的時候,會被寫入緩存。那為什么不直接寫緩存呢?因為如果兩個線程同時更新數(shù)據(jù)庫,一個將數(shù)據(jù)庫改為10,一個將數(shù)據(jù)庫改為20,數(shù)據(jù)庫有自己的事務(wù)機制,可以保證如果20是后提交的,數(shù)據(jù)庫里面改為20,但是回過頭來寫入緩存的時候就沒有事務(wù)了,如果改為20的線程先更新緩存,改為10的線程后更新緩存,于是就會長時間出現(xiàn)緩存中是10,但是數(shù)據(jù)庫中是20的現(xiàn)象。

這種方式實時性好,用戶體驗好,是默認應(yīng)該使用的策略。

2、異步策略

所謂異步策略,就是當讀取的時候讀不到的時候,不直接訪問數(shù)據(jù)庫,而是返回一個fallback數(shù)據(jù),然后往消息隊列里面放入一個數(shù)據(jù)加載的事件,在背后有一個任務(wù),收到事件后,會異步的讀取數(shù)據(jù)庫,由于有隊列的作用,可以實現(xiàn)消峰,緩沖對數(shù)據(jù)庫的訪問,甚至可以將多個隊列中的任務(wù)合并請求,合并更新緩存,提高了效率。

當更新的時候,異步策略總是先更新數(shù)據(jù)庫和緩存中的一個,然后異步的更新另一個。

一是先更新數(shù)據(jù)庫,然后異步更新緩存。當數(shù)據(jù)庫更新后,同樣生成一個異步消息,放入消息隊列中,等待背后的任務(wù)通過消息進行緩存更新,同樣可以實現(xiàn)消峰和任務(wù)合并。缺點就是實時性比較差,估計要過一段時間才能看到更新,好處是數(shù)據(jù)持久性可以得到保證。

一是先更新緩存,然后異步更新數(shù)據(jù)庫。這種方式讀取和寫入都用緩存,將緩存完全擋在了數(shù)據(jù)庫的前面,把緩存當成了數(shù)據(jù)庫在用。所以一般會使用有持久化機制和主備的redis,但是仍然不能保證緩存不丟數(shù)據(jù),所以這種情況適用于并發(fā)量大,但是數(shù)據(jù)沒有那么關(guān)鍵的情況,好處是實時性好。

在實時策略扛不住大促的時候,可以根據(jù)場景,切換到上面的兩種模式的一個,算是降級策略。

3、定時策略

如果并發(fā)量實在太大,數(shù)據(jù)量也大的情況,異步都難以滿足,可以降級為定時刷新的策略,這種情況下,應(yīng)用只訪問緩存,不訪問數(shù)據(jù)庫,更新頻率也不高,而且用戶要求也不高,例如詳情,評論等。

這種情況下,由于數(shù)據(jù)量比較大,建議將一整塊數(shù)據(jù)拆分成幾部分進行緩存,而且區(qū)分更新頻繁的和不頻繁的,這樣不用每次更新的時候,所有的都更新,只更新一部分。并且緩存的時候,可以進行數(shù)據(jù)的預(yù)整合,因為實時性不高,讀取預(yù)整合的數(shù)據(jù)更快。
###最后
大家覺得不錯可以點個贊在關(guān)注下,以后還會分享更多文章!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI