教育行業(yè)A股IPO第一股(股票代碼 003032)

全國咨詢/投訴熱線:400-618-4000

Java多級緩存是為了解決什么的?

更新時間:2022年12月02日14時52分 來源:傳智教育 瀏覽次數(shù):

  1. 前言

  提到緩存,想必每一位軟件工程師都不陌生,它是目前架構(gòu)設(shè)計中提高性能最直接的方式。

  緩存技術(shù)存在于應(yīng)用場景的方方面面。從網(wǎng)站提高性能的角度分析,緩存可以放在瀏覽器,可以放在反向代理服務(wù)器,還可以放在應(yīng)用程序進(jìn)程內(nèi),同時可以放在分布式緩存系統(tǒng)中。

  從用戶請求數(shù)據(jù)到數(shù)據(jù)返回,數(shù)據(jù)經(jīng)過了瀏覽器,CDN,Nginx代理緩存,應(yīng)用服務(wù)器,以及數(shù)據(jù)庫各個環(huán)節(jié)。每個環(huán)節(jié)都可以運(yùn)用緩存技術(shù)。

  

1665715719534_1.jpg

  緩存的請求順序是:用戶請求 → HTTP 緩存 → CDN 緩存 → Nginx代理緩存 → 進(jìn)程內(nèi)緩存 → 分布式緩存。

  在技術(shù)的架構(gòu)每個環(huán)節(jié)都可以加入緩存,我們看看每個環(huán)節(jié)是如何應(yīng)用緩存技術(shù)的。

  2. HTTP緩存

  通常 HTTP 緩存策略分為兩種:

  - 強(qiáng)緩存

  - 協(xié)商緩存。

  從字面意思我們可以很直觀的看到它們的差別:

  - 強(qiáng)緩存即強(qiáng)制直接使用緩存。

  - 協(xié)商緩存就得和服務(wù)器協(xié)商確認(rèn)下這個緩存能不能用。

  強(qiáng)緩存

  強(qiáng)緩存不會向服務(wù)器發(fā)送請求,直接從緩存中讀取資源,在 chrome 控制臺的 network 選項(xiàng)中可以看到該請求返回 200 的狀態(tài)碼,并且`size`顯示`from disk cache`或`from memory cache`;

  協(xié)商緩存

  協(xié)商緩存會先向服務(wù)器發(fā)送一個請求,服務(wù)器會根據(jù)這個請求的 request header 的一些參數(shù)來判斷是否命中協(xié)商緩存,如果命中,則返回 304 狀態(tài)碼并帶上新的 response header 通知瀏覽器從緩存中讀取資源。

  2.1 HTTP 緩存控制

  在 HTTP 中,我們可以通過設(shè)置響應(yīng)頭以及請求頭來控制緩存策略。

  強(qiáng)緩存可以通過設(shè)置`Expires`和`Cache-Control` 兩種響應(yīng)頭實(shí)現(xiàn)。如果同時存在,`Cache-Control`優(yōu)先級高于`Expires`。

  Expires

  Expires 響應(yīng)頭,它是 HTTP/1.0 的產(chǎn)物。代表該資源的過期時間,其值為一個絕對時間。它告訴瀏覽器在過期時間之前可以直接從瀏覽器緩存中存取數(shù)據(jù)。由于是個絕對時間,客戶端與服務(wù)端的時間時差或誤差等因素可能造成客戶端與服務(wù)端的時間不一致,將導(dǎo)致緩存命中的誤差。如果在`Cache-Control`響應(yīng)頭設(shè)置了 `max-age` 或者 `s-max-age` 指令,那么 `Expires` 會被忽略。

Expires: Wed, 21 Oct 2015 07:28:00 GMT

  Cache-Control

  `Cache-Control` 出現(xiàn)于 HTTP/1.1??梢酝ㄟ^指定多個指令來實(shí)現(xiàn)緩存機(jī)制。主要用表示資源緩存的最大有效時間。即在該時間端內(nèi),客戶端不需要向服務(wù)器發(fā)送請求。優(yōu)先級高于 Expires。其過期時間指令的值是相對時間,它解決了絕對時間的帶來的問題。

Cache-Control: max-age=315360000

  `Cache-Control` 有很多屬性,不同的屬性代表的意義也不同。

  可緩存性

  - `public` 表明響應(yīng)可以被任何對象(包括:發(fā)送請求的客戶端,代理服務(wù)器,等等)緩存。

  - `private` 表明響應(yīng)只能被單個用戶緩存,不能作為共享緩存(即代理服務(wù)器不能緩存它)

  - `no-cache` 不使用強(qiáng)緩存,需要與服務(wù)器驗(yàn)協(xié)商緩存驗(yàn)證。

  - `no-store` 緩存不應(yīng)存儲有關(guān)客戶端請求或服務(wù)器響應(yīng)的任何內(nèi)容,即不使用任何緩存。

  過期

  - `max-age=` 緩存存儲的最大周期,超過這個周期被認(rèn)為過期。

  - `s-maxage=` 設(shè)置共享緩存。會覆蓋`max-age`和`expires`,私有緩存會忽略它

  - `max-stale[=]` 客戶端愿意接收一個已經(jīng)過期的資源,可以設(shè)置一個可選的秒數(shù),表示響應(yīng)不能已經(jīng)過時超過該給定的時間。

  - `min-fresh=` 客戶端希望在指定的時間內(nèi)獲取最新的響應(yīng)

  重新驗(yàn)證和重新加載

  - `must-revalidate` 如頁面過期,則去服務(wù)器進(jìn)行獲取。

  - `proxy-revalidate` 與`must-revalidate` 作用相同,但是用于共享緩存。

  其他

  - `only-if-cached` 不進(jìn)行網(wǎng)絡(luò)請求,完全只使用緩存。

  - `no-transform` 不得對資源進(jìn)行轉(zhuǎn)換和轉(zhuǎn)變。例如,不得對圖像格式進(jìn)行轉(zhuǎn)換。

  協(xié)商緩存可以通過 `Last-Modified`/`If-Modified-Since`和`ETag`/`If-None-Match`這兩對 Header 來控制。

  2.2 Last-Modified、If-Modified-Since

  `Last-Modified`與`If-Modified-Since` 的值都是 GMT 格式的時間字符串,代表的是文件的最后修改時間。

       1.在服務(wù)器在響應(yīng)請求時,會通過`Last-Modified`告訴瀏覽器資源的最后修改時間。

  2. 瀏覽器再次請求服務(wù)器的時候,請求頭會包含`Last-Modified`字段,后面跟著在緩存中獲得的最后修改時間。

  3. 服務(wù)端收到此請求頭發(fā)現(xiàn)有`if-Modified-Since`,則與被請求資源的最后修改時間進(jìn)行對比,如果一致則返回 304 和響應(yīng)報文頭,瀏覽器只需要從緩存中獲取信息即可。如果已經(jīng)修改,那么開始傳輸響應(yīng)一個整體,服務(wù)器返回:200 OK

<img src="images/image-20220718222822409.png" alt="image-20220718222822409" style="zoom:80%;" />

  但是在服務(wù)器上經(jīng)常會出現(xiàn)這種情況,一個資源被修改了,但其實(shí)際內(nèi)容根本沒發(fā)生改變,會因?yàn)閌Last-Modified`時間匹配不上而返回了整個實(shí)體給客戶端(即使客戶端緩存里有個一模一樣的資源)。為了解決這個問題,HTTP/1.1 推出了`Etag`。Etag 優(yōu)先級高與`Last-Modified`。

  2.3 Etag、If-None-Match

  `Etag`都是服務(wù)器為每份資源生成的唯一標(biāo)識,就像一個指紋,資源變化都會導(dǎo)致 ETag 變化,跟最后修改時間沒有關(guān)系,`ETag`可以保證每一個資源是唯一的。

  在瀏覽器發(fā)起請求,瀏覽器的請求報文頭會包含 `If-None-Match` 字段,其值為上次返回的`Etag`發(fā)送給服務(wù)器,服務(wù)器接收到次報文后發(fā)現(xiàn) `If-None-Match` 則與被請求資源的唯一標(biāo)識進(jìn)行對比。如果相同說明資源沒有修改,則響應(yīng)返 304,瀏覽器直接從緩存中獲取數(shù)據(jù)信息。如果不同則說明資源被改動過,則響應(yīng)整個資源內(nèi)容,返回狀態(tài)碼 200。

  3. CDN緩存

  CDN:Content Delivery Network,即內(nèi)容分發(fā)網(wǎng)絡(luò),它是構(gòu)建在現(xiàn)有網(wǎng)絡(luò)基礎(chǔ)上的虛擬智能網(wǎng)絡(luò),依靠部署在各地的邊緣服務(wù)器,通過中心平臺的負(fù)載均衡、調(diào)度及內(nèi)容分發(fā)等功能模塊,使用戶在請求所需訪問的內(nèi)容時能夠就近獲取,以此來降低網(wǎng)絡(luò)擁塞,提高資源對用戶的響應(yīng)速度。

  本地存儲和瀏覽器緩存帶來的性能提升主要針對的是瀏覽器端已經(jīng)緩存了所需的資源,當(dāng)發(fā)生二次請求相同資源時便能夠進(jìn)行快速響應(yīng),避免重新發(fā)起請求或重新下載全部響應(yīng)資源。

  這些方法對于首次資源請求的性能提升是無能為力的,若想提升首次請求資源的響應(yīng)速度,除了資源壓縮、圖片優(yōu)化等方式,還可借助CDN技術(shù)。

  

1665715997767_2.jpg

  3.1 使用CDN網(wǎng)絡(luò)資源獲取過程

  如果使用了CDN網(wǎng)絡(luò),則資源獲取的大致過程是這樣的。

       1.由于DNS服務(wù)器將對CDN的域名解析權(quán)交給了CNAME指向的專用DNS服務(wù)器,所以對用戶輸入域名的解析最終是在CDN專用的DNS服務(wù)器上完成的。

  2. 解析出的結(jié)果IP地址并非確定的CDN緩存服務(wù)器地址,而是CDN的負(fù)載均衡器的地址。

  3. 瀏覽器會重新向該負(fù)載均衡器發(fā)起請求,經(jīng)過對用戶IP地址的距離、所請求資源內(nèi)容的位置及各個服務(wù)器復(fù)雜狀況的綜合計算,返回給用戶確定的緩存服務(wù)器IP地址。

  4. 對目標(biāo)緩存服務(wù)器請求所需資源的過程。

  這個過程也可能會發(fā)生所需資源未找到的情況,那么此時便會依次向其上一級緩存服務(wù)器繼續(xù)請求查詢,直至追溯到網(wǎng)站的根服務(wù)器并將資源拉取到本地。

  3.2 CDN網(wǎng)絡(luò)的核心功能包括兩點(diǎn):

  緩存與回源

  緩存指的是將所需的靜態(tài)資源文件復(fù)制一份到CDN緩存服務(wù)器上;

  回源指的是如果未在CDN緩存服務(wù)器上查找到目標(biāo)資源,或CDN緩存服務(wù)器上的緩存資源已經(jīng)過期,則重新追溯到網(wǎng)站根服務(wù)器獲取相關(guān)資源的過程。

  4. Nginx代理緩存

  用戶請求在達(dá)到應(yīng)用服務(wù)器之前,會先訪問 Nginx 負(fù)載均衡器,如果發(fā)現(xiàn)有緩存信息,直接返回給用戶。

  如果沒有發(fā)現(xiàn)緩存信息,Nginx 回源到應(yīng)用服務(wù)器獲取信息。

  另外,有一個緩存更新服務(wù),定期把應(yīng)用服務(wù)器中相對穩(wěn)定的信息更新到 Nginx 本地緩存中。

  Nginx設(shè)置緩存有兩種方式:

  - proxy_cache_path和proxy_cache

  - Cache-Control和Pragma

  對于站點(diǎn)中不經(jīng)常修改的靜態(tài)內(nèi)容(如圖片,JS,CSS),可以在服務(wù)器中設(shè)置expires過期時間,控制瀏覽器緩存,達(dá)到有效減小帶寬流量,降低服務(wù)器壓力的目的。

<img src="images/image-20220718224542963.png" alt="image-20220718224542963" style="zoom: 67%;" />

  第一步:客戶端第一次向Nginx請求數(shù)據(jù)A;

  第二步:當(dāng)Nginx發(fā)現(xiàn)緩存中沒有數(shù)據(jù)A時,會向服務(wù)端請求數(shù)據(jù)A;

  第三步:服務(wù)端接收到Nginx發(fā)來的請求,則返回數(shù)據(jù)A到Nginx,并且緩存在Nginx;

  第四步:Nginx返回數(shù)據(jù)A給客戶端應(yīng)用;

  第五步:客戶端第二次向Nginx請求數(shù)據(jù)A;

  第六步:當(dāng)Nginx發(fā)現(xiàn)緩存中存在數(shù)據(jù)A時,則不會請求服務(wù)端;

  第七步:Nginx把緩存中的數(shù)據(jù)A返回給客戶端應(yīng)用。

  默認(rèn)情況下,NGINX尊重Cache-Control源服務(wù)器的標(biāo)頭。它不緩存響應(yīng)Cache-Control設(shè)置為Private,No-Cache或No-Store或Set-Cookie在響應(yīng)頭。NGINX只緩存GET和HEAD客戶端請求。

  如下配置可覆蓋這些默認(rèn)值:

  - proxy_buffering默認(rèn)為on,若proxy_buffering設(shè)置為off,則NGINX不會緩存響應(yīng)。

  - proxy_ignore_headers可以配置忽略Cache-Control:

location /images/ {
    proxy_cache my_cache;
    proxy_ignore_headers Cache-Control;
    proxy_cache_valid any 30m;
    # ...
}

  5. 進(jìn)程緩存

  通過了客戶端,CDN,Nginx代理緩存,我們終于來到了應(yīng)用服務(wù)器。應(yīng)用服務(wù)器上部署著一個個應(yīng)用,這些應(yīng)用以進(jìn)程的方式運(yùn)行著,那么在進(jìn)程中的緩存是怎樣的呢?

  進(jìn)程內(nèi)緩存又叫托管堆緩存,以 Java 為例,這部分緩存放在 JVM 的托管堆上面,同時會受到托管堆回收算法的影響。

  由于其運(yùn)行在內(nèi)存中,對數(shù)據(jù)的響應(yīng)速度很快,通常我們會把熱點(diǎn)數(shù)據(jù)放在這里。

  在進(jìn)程內(nèi)緩存沒有命中的時候,我們會去搜索進(jìn)程外的緩存或者分布式緩存。這種緩存的好處是沒有序列化和反序列化,是最快的緩存。缺點(diǎn)是緩存的空間不能太大,對垃圾回收器的性能有影響。

  目前比較流行的實(shí)現(xiàn)有 Ehcache、GuavaCache、Caffeine。這些架構(gòu)可以很方便的把一些熱點(diǎn)數(shù)據(jù)放到進(jìn)程內(nèi)的緩存中。

  這里我們需要關(guān)注幾個緩存的回收策略,具體的實(shí)現(xiàn)架構(gòu)的回收策略會有所不同,但大致的思路都是一致的:

  - FIFO(First In First Out):先進(jìn)先出算法,最先放入緩存的數(shù)據(jù)最先被移除。

  - LRU(Least Recently Used):最近最少使用算法,把最久沒有使用過的數(shù)據(jù)移除緩存。

  - LFU(Least Frequently Used):最不常用算法,在一段時間內(nèi)使用頻率最小的數(shù)據(jù)被移除緩存。

  在分布式架構(gòu)的今天,多應(yīng)用中如果采用進(jìn)程內(nèi)緩存會存在數(shù)據(jù)一致性的問題。

  這里推薦兩個方案:

  - 消息隊(duì)列方案

  應(yīng)用在修改完自身緩存數(shù)據(jù)和數(shù)據(jù)庫數(shù)據(jù)之后,給消息隊(duì)列發(fā)送數(shù)據(jù)變化通知,其他應(yīng)用訂閱了消息通知,在收到通知的時候修改緩存數(shù)據(jù)。

  - 定時任務(wù)修改方案

  為了避免耦合,降低復(fù)雜性,對“實(shí)時一致性”不敏感的情況下。每個應(yīng)用都會啟動一個定時任務(wù),定時從數(shù)據(jù)庫拉取最新的數(shù)據(jù),更新緩存。

  進(jìn)程內(nèi)緩存有哪些使用場景呢?

  - 場景一:只讀數(shù)據(jù),可以考慮在進(jìn)程啟動時加載到內(nèi)存。當(dāng)然,把數(shù)據(jù)加載到類似 Redis 這樣的進(jìn)程外緩存服務(wù)也能解決這類問題。

  - 場景二:高并發(fā),可以考慮使用進(jìn)程內(nèi)緩存,例如:秒殺。

  6. 分布式緩存

  說完進(jìn)程內(nèi)緩存,自然就過度到進(jìn)程外緩存了。

  與進(jìn)程內(nèi)緩存不同,進(jìn)程外緩存在應(yīng)用運(yùn)行的進(jìn)程之外,它擁有更大的緩存容量,并且可以部署到不同的物理節(jié)點(diǎn),通常會用分布式緩存的方式實(shí)現(xiàn)。

  分布式緩存是與應(yīng)用分離的緩存服務(wù),最大的特點(diǎn)是,自身是一個獨(dú)立的應(yīng)用/服務(wù),與本地應(yīng)用隔離,多個應(yīng)用可直接共享一個或者多個緩存應(yīng)用/服務(wù)。

  為了提高緩存的可用性,會在原有的緩存節(jié)點(diǎn)上加入 Master/Slave 的設(shè)計。當(dāng)緩存數(shù)據(jù)寫入 Master 節(jié)點(diǎn)的時候,會同時同步一份到 Slave 節(jié)點(diǎn)。

  一旦 Master 節(jié)點(diǎn)失效,可以通過代理直接切換到 Slave 節(jié)點(diǎn),這時 Slave 節(jié)點(diǎn)就變成了 Master 節(jié)點(diǎn),保證緩存的正常工作。

  每個緩存節(jié)點(diǎn)還會提供緩存過期的機(jī)制,并且會把緩存內(nèi)容定期以快照的方式保存到文件上,方便緩存崩潰之后啟動預(yù)熱加載。

  6.1 緩存雪崩

  當(dāng)緩存失效,緩存過期被清除,緩存更新的時候。請求是無法命中緩存的,這個時候請求會直接回源到數(shù)據(jù)庫。

  如果上述情況頻繁發(fā)生或者同時發(fā)生的時候,就會造成大面積的請求直接到數(shù)據(jù)庫,造成數(shù)據(jù)庫訪問瓶頸。我們稱這種情況為緩存雪崩。

  從如下兩方面來思考解決方案:

  緩存方面:

  - 避免緩存同時失效,不同的 key 設(shè)置不同的超時時間。

  - 增加互斥鎖,對緩存的更新操作進(jìn)行加鎖保護(hù),保證只有一個線程進(jìn)行緩存更新。緩存一旦失效可以通過緩存快照的方式迅速重建緩存。對緩存節(jié)點(diǎn)增加主備機(jī)制,當(dāng)主緩存失效以后切換到備用緩存繼續(xù)工作。

  設(shè)計方面,這里給出了幾點(diǎn)建議供大家參考:

  - 熔斷機(jī)制:某個緩存節(jié)點(diǎn)不能工作的時候,需要通知緩存代理不要把請求路由到該節(jié)點(diǎn),減少用戶等待和請求時長。

  - 限流機(jī)制:在接入層和代理層可以做限流,當(dāng)緩存服務(wù)無法支持高并發(fā)的時候,前端可以把無法響應(yīng)的請求放入到隊(duì)列或者丟棄。

  - 隔離機(jī)制:緩存無法提供服務(wù)或者正在預(yù)熱重建的時候,把該請求放入隊(duì)列中,這樣該請求因?yàn)楸桓綦x就不會被路由到其他的緩存節(jié)點(diǎn)。

  - 如此就不會因?yàn)檫@個節(jié)點(diǎn)的問題影響到其他節(jié)點(diǎn)。當(dāng)緩存重建以后,再從隊(duì)列中取出請求依次處理。

  62. 緩存穿透

  緩存一般是 Key,Value 方式存在,一個 Key 對應(yīng)的 Value 不存在時,請求會回源到數(shù)據(jù)庫。

  假如對應(yīng)的 Value 一直不存在,則會頻繁的請求數(shù)據(jù)庫,對數(shù)據(jù)庫造成訪問壓力。如果有人利用這個漏洞攻擊,就麻煩了。

  解決方法:如果一個 Key 對應(yīng)的 Value 查詢返回為空,我們?nèi)匀话堰@個空結(jié)果緩存起來,如果這個值沒有變化下次查詢就不會請求數(shù)據(jù)庫了。

  將所有可能存在的數(shù)據(jù)哈希到一個足夠大的 Bitmap 中,那么不存在的數(shù)據(jù)會被這個 Bitmap 過濾器攔截掉,避免對數(shù)據(jù)庫的查詢壓力。

  6.3 緩存擊穿

  在數(shù)據(jù)請求的時候,某一個緩存剛好失效或者正在寫入緩存,同時這個緩存數(shù)據(jù)可能會在這個時間點(diǎn)被超高并發(fā)請求,成為“熱點(diǎn)”數(shù)據(jù)。

  這就是緩存擊穿問題,這個和緩存雪崩的區(qū)別在于,這里是針對某一個緩存,前者是針對多個緩存。

  解決方案:導(dǎo)致問題的原因是在同一時間讀/寫緩存,所以只有保證同一時間只有一個線程寫,寫完成以后,其他的請求再使用緩存就可以了。

  比較常用的做法是使用 mutex(互斥鎖)。在緩存失效的時候,不是立即寫入緩存,而是先設(shè)置一個 mutex(互斥鎖)。當(dāng)緩存被寫入完成以后,再放開這個鎖讓請求進(jìn)行訪問。

0 分享到:
和我們在線交談!