更新時(shí)間:2022-10-14 來源:黑馬程序員 瀏覽量:
前言
提到緩存,想必每一位軟件工程師都不陌生,它是目前架構(gòu)設(shè)計(jì)中提高性能最直接的方式。
緩存技術(shù)存在于應(yīng)用場(chǎng)景的方方面面。從網(wǎng)站提高性能的角度分析,緩存可以放在瀏覽器,可以放在反向代理服務(wù)器,還可以放在應(yīng)用程序進(jìn)程內(nèi),同時(shí)可以放在分布式緩存系統(tǒng)中。
從用戶請(qǐng)求數(shù)據(jù)到數(shù)據(jù)返回,數(shù)據(jù)經(jīng)過了瀏覽器,CDN,Nginx代理緩存,應(yīng)用服務(wù)器,以及數(shù)據(jù)庫各個(gè)環(huán)節(jié)。每個(gè)環(huán)節(jié)都可以運(yùn)用緩存技術(shù)。
緩存的請(qǐng)求順序是:用戶請(qǐng)求 → HTTP 緩存 → CDN 緩存 → Nginx代理緩存 → 進(jìn)程內(nèi)緩存 → 分布式緩存。
在技術(shù)的架構(gòu)每個(gè)環(huán)節(jié)都可以加入緩存,我們看看每個(gè)環(huán)節(jié)是如何應(yīng)用緩存技術(shù)的。
2. HTTP緩存
通常 HTTP 緩存策略分為兩種:
- 強(qiáng)緩存
- 協(xié)商緩存。
從字面意思我們可以很直觀的看到它們的差別:
- 強(qiáng)緩存即強(qiáng)制直接使用緩存。
- 協(xié)商緩存就得和服務(wù)器協(xié)商確認(rèn)下這個(gè)緩存能不能用。
強(qiáng)緩存
強(qiáng)緩存不會(huì)向服務(wù)器發(fā)送請(qǐng)求,直接從緩存中讀取資源,在 chrome 控制臺(tái)的 network 選項(xiàng)中可以看到該請(qǐng)求返回 200 的狀態(tài)碼,并且`size`顯示`from disk cache`或`from memory cache`;
協(xié)商緩存
協(xié)商緩存會(huì)先向服務(wù)器發(fā)送一個(gè)請(qǐng)求,服務(wù)器會(huì)根據(jù)這個(gè)請(qǐng)求的 request header 的一些參數(shù)來判斷是否命中協(xié)商緩存,如果命中,則返回 304 狀態(tài)碼并帶上新的 response header 通知瀏覽器從緩存中讀取資源。
2.1 HTTP 緩存控制
在 HTTP 中,我們可以通過設(shè)置響應(yīng)頭以及請(qǐng)求頭來控制緩存策略。
強(qiáng)緩存可以通過設(shè)置`Expires`和`Cache-Control` 兩種響應(yīng)頭實(shí)現(xiàn)。如果同時(shí)存在,`Cache-Control`優(yōu)先級(jí)高于`Expires`。
Expires
Expires 響應(yīng)頭,它是 HTTP/1.0 的產(chǎn)物。代表該資源的過期時(shí)間,其值為一個(gè)絕對(duì)時(shí)間。它告訴瀏覽器在過期時(shí)間之前可以直接從瀏覽器緩存中存取數(shù)據(jù)。由于是個(gè)絕對(duì)時(shí)間,客戶端與服務(wù)端的時(shí)間時(shí)差或誤差等因素可能造成客戶端與服務(wù)端的時(shí)間不一致,將導(dǎo)致緩存命中的誤差。如果在`Cache-Control`響應(yīng)頭設(shè)置了 `max-age` 或者 `s-max-age` 指令,那么 `Expires` 會(huì)被忽略。
Expires: Wed, 21 Oct 2015 07:28:00 GMT
Cache-Control
`Cache-Control` 出現(xiàn)于 HTTP/1.1??梢酝ㄟ^指定多個(gè)指令來實(shí)現(xiàn)緩存機(jī)制。主要用表示資源緩存的最大有效時(shí)間。即在該時(shí)間端內(nèi),客戶端不需要向服務(wù)器發(fā)送請(qǐng)求。優(yōu)先級(jí)高于 Expires。其過期時(shí)間指令的值是相對(duì)時(shí)間,它解決了絕對(duì)時(shí)間的帶來的問題。
Cache-Control: max-age=315360000
`Cache-Control` 有很多屬性,不同的屬性代表的意義也不同。
可緩存性
- `public` 表明響應(yīng)可以被任何對(duì)象(包括:發(fā)送請(qǐng)求的客戶端,代理服務(wù)器,等等)緩存。
- `private` 表明響應(yīng)只能被單個(gè)用戶緩存,不能作為共享緩存(即代理服務(wù)器不能緩存它)
- `no-cache` 不使用強(qiáng)緩存,需要與服務(wù)器驗(yàn)協(xié)商緩存驗(yàn)證。
- `no-store` 緩存不應(yīng)存儲(chǔ)有關(guān)客戶端請(qǐng)求或服務(wù)器響應(yīng)的任何內(nèi)容,即不使用任何緩存。
過期
- `max-age=` 緩存存儲(chǔ)的最大周期,超過這個(gè)周期被認(rèn)為過期。
- `s-maxage=` 設(shè)置共享緩存。會(huì)覆蓋`max-age`和`expires`,私有緩存會(huì)忽略它
- `max-stale[=]` 客戶端愿意接收一個(gè)已經(jīng)過期的資源,可以設(shè)置一個(gè)可選的秒數(shù),表示響應(yīng)不能已經(jīng)過時(shí)超過該給定的時(shí)間。
- `min-fresh=` 客戶端希望在指定的時(shí)間內(nèi)獲取最新的響應(yīng)
重新驗(yàn)證和重新加載
- `must-revalidate` 如頁面過期,則去服務(wù)器進(jìn)行獲取。
- `proxy-revalidate` 與`must-revalidate` 作用相同,但是用于共享緩存。
其他
- `only-if-cached` 不進(jìn)行網(wǎng)絡(luò)請(qǐng)求,完全只使用緩存。
- `no-transform` 不得對(duì)資源進(jìn)行轉(zhuǎn)換和轉(zhuǎn)變。例如,不得對(duì)圖像格式進(jìn)行轉(zhuǎn)換。
協(xié)商緩存可以通過 `Last-Modified`/`If-Modified-Since`和`ETag`/`If-None-Match`這兩對(duì) Header 來控制。
2.2 Last-Modified、If-Modified-Since
`Last-Modified`與`If-Modified-Since` 的值都是 GMT 格式的時(shí)間字符串,代表的是文件的最后修改時(shí)間。
1.在服務(wù)器在響應(yīng)請(qǐng)求時(shí),會(huì)通過`Last-Modified`告訴瀏覽器資源的最后修改時(shí)間。
2. 瀏覽器再次請(qǐng)求服務(wù)器的時(shí)候,請(qǐng)求頭會(huì)包含`Last-Modified`字段,后面跟著在緩存中獲得的最后修改時(shí)間。
3. 服務(wù)端收到此請(qǐng)求頭發(fā)現(xiàn)有`if-Modified-Since`,則與被請(qǐng)求資源的最后修改時(shí)間進(jìn)行對(duì)比,如果一致則返回 304 和響應(yīng)報(bào)文頭,瀏覽器只需要從緩存中獲取信息即可。如果已經(jīng)修改,那么開始傳輸響應(yīng)一個(gè)整體,服務(wù)器返回:200 OK
<img src="images/image-20220718222822409.png" alt="image-20220718222822409" style="zoom:80%;" />
但是在服務(wù)器上經(jīng)常會(huì)出現(xiàn)這種情況,一個(gè)資源被修改了,但其實(shí)際內(nèi)容根本沒發(fā)生改變,會(huì)因?yàn)閌Last-Modified`時(shí)間匹配不上而返回了整個(gè)實(shí)體給客戶端(即使客戶端緩存里有個(gè)一模一樣的資源)。為了解決這個(gè)問題,HTTP/1.1 推出了`Etag`。Etag 優(yōu)先級(jí)高與`Last-Modified`。
2.3 Etag、If-None-Match
`Etag`都是服務(wù)器為每份資源生成的唯一標(biāo)識(shí),就像一個(gè)指紋,資源變化都會(huì)導(dǎo)致 ETag 變化,跟最后修改時(shí)間沒有關(guān)系,`ETag`可以保證每一個(gè)資源是唯一的。
在瀏覽器發(fā)起請(qǐng)求,瀏覽器的請(qǐng)求報(bào)文頭會(huì)包含 `If-None-Match` 字段,其值為上次返回的`Etag`發(fā)送給服務(wù)器,服務(wù)器接收到次報(bào)文后發(fā)現(xiàn) `If-None-Match` 則與被請(qǐng)求資源的唯一標(biāo)識(shí)進(jìn)行對(duì)比。如果相同說明資源沒有修改,則響應(yīng)返 304,瀏覽器直接從緩存中獲取數(shù)據(jù)信息。如果不同則說明資源被改動(dòng)過,則響應(yīng)整個(gè)資源內(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ù)器,通過中心平臺(tái)的負(fù)載均衡、調(diào)度及內(nèi)容分發(fā)等功能模塊,使用戶在請(qǐng)求所需訪問的內(nèi)容時(shí)能夠就近獲取,以此來降低網(wǎng)絡(luò)擁塞,提高資源對(duì)用戶的響應(yīng)速度。
本地存儲(chǔ)和瀏覽器緩存帶來的性能提升主要針對(duì)的是瀏覽器端已經(jīng)緩存了所需的資源,當(dāng)發(fā)生二次請(qǐng)求相同資源時(shí)便能夠進(jìn)行快速響應(yīng),避免重新發(fā)起請(qǐng)求或重新下載全部響應(yīng)資源。
這些方法對(duì)于首次資源請(qǐng)求的性能提升是無能為力的,若想提升首次請(qǐng)求資源的響應(yīng)速度,除了資源壓縮、圖片優(yōu)化等方式,還可借助CDN技術(shù)。
3.1 使用CDN網(wǎng)絡(luò)資源獲取過程
如果使用了CDN網(wǎng)絡(luò),則資源獲取的大致過程是這樣的。
1.由于DNS服務(wù)器將對(duì)CDN的域名解析權(quán)交給了CNAME指向的專用DNS服務(wù)器,所以對(duì)用戶輸入域名的解析最終是在CDN專用的DNS服務(wù)器上完成的。
2. 解析出的結(jié)果IP地址并非確定的CDN緩存服務(wù)器地址,而是CDN的負(fù)載均衡器的地址。
3. 瀏覽器會(huì)重新向該負(fù)載均衡器發(fā)起請(qǐng)求,經(jīng)過對(duì)用戶IP地址的距離、所請(qǐng)求資源內(nèi)容的位置及各個(gè)服務(wù)器復(fù)雜狀況的綜合計(jì)算,返回給用戶確定的緩存服務(wù)器IP地址。
4. 對(duì)目標(biāo)緩存服務(wù)器請(qǐng)求所需資源的過程。
這個(gè)過程也可能會(huì)發(fā)生所需資源未找到的情況,那么此時(shí)便會(huì)依次向其上一級(jí)緩存服務(wù)器繼續(xù)請(qǐng)求查詢,直至追溯到網(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代理緩存
用戶請(qǐng)求在達(dá)到應(yīng)用服務(wù)器之前,會(huì)先訪問 Nginx 負(fù)載均衡器,如果發(fā)現(xiàn)有緩存信息,直接返回給用戶。
如果沒有發(fā)現(xiàn)緩存信息,Nginx 回源到應(yīng)用服務(wù)器獲取信息。
另外,有一個(gè)緩存更新服務(wù),定期把應(yīng)用服務(wù)器中相對(duì)穩(wěn)定的信息更新到 Nginx 本地緩存中。
Nginx設(shè)置緩存有兩種方式:
- proxy_cache_path和proxy_cache
- Cache-Control和Pragma
對(duì)于站點(diǎn)中不經(jīng)常修改的靜態(tài)內(nèi)容(如圖片,JS,CSS),可以在服務(wù)器中設(shè)置expires過期時(shí)間,控制瀏覽器緩存,達(dá)到有效減小帶寬流量,降低服務(wù)器壓力的目的。
<img src="images/image-20220718224542963.png" alt="image-20220718224542963" style="zoom: 67%;" />
第一步:客戶端第一次向Nginx請(qǐng)求數(shù)據(jù)A;
第二步:當(dāng)Nginx發(fā)現(xiàn)緩存中沒有數(shù)據(jù)A時(shí),會(huì)向服務(wù)端請(qǐng)求數(shù)據(jù)A;
第三步:服務(wù)端接收到Nginx發(fā)來的請(qǐng)求,則返回?cái)?shù)據(jù)A到Nginx,并且緩存在Nginx;
第四步:Nginx返回?cái)?shù)據(jù)A給客戶端應(yīng)用;
第五步:客戶端第二次向Nginx請(qǐng)求數(shù)據(jù)A;
第六步:當(dāng)Nginx發(fā)現(xiàn)緩存中存在數(shù)據(jù)A時(shí),則不會(huì)請(qǐng)求服務(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客戶端請(qǐng)求。
如下配置可覆蓋這些默認(rèn)值:
- proxy_buffering默認(rèn)為on,若proxy_buffering設(shè)置為off,則NGINX不會(huì)緩存響應(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ù)器上部署著一個(gè)個(gè)應(yīng)用,這些應(yīng)用以進(jìn)程的方式運(yùn)行著,那么在進(jìn)程中的緩存是怎樣的呢?
進(jìn)程內(nèi)緩存又叫托管堆緩存,以 Java 為例,這部分緩存放在 JVM 的托管堆上面,同時(shí)會(huì)受到托管堆回收算法的影響。
由于其運(yùn)行在內(nèi)存中,對(duì)數(shù)據(jù)的響應(yīng)速度很快,通常我們會(huì)把熱點(diǎn)數(shù)據(jù)放在這里。
在進(jìn)程內(nèi)緩存沒有命中的時(shí)候,我們會(huì)去搜索進(jìn)程外的緩存或者分布式緩存。這種緩存的好處是沒有序列化和反序列化,是最快的緩存。缺點(diǎn)是緩存的空間不能太大,對(duì)垃圾回收器的性能有影響。
目前比較流行的實(shí)現(xiàn)有 Ehcache、GuavaCache、Caffeine。這些架構(gòu)可以很方便的把一些熱點(diǎn)數(shù)據(jù)放到進(jìn)程內(nèi)的緩存中。
這里我們需要關(guān)注幾個(gè)緩存的回收策略,具體的實(shí)現(xiàn)架構(gòu)的回收策略會(huì)有所不同,但大致的思路都是一致的:
- FIFO(First In First Out):先進(jìn)先出算法,最先放入緩存的數(shù)據(jù)最先被移除。
- LRU(Least Recently Used):最近最少使用算法,把最久沒有使用過的數(shù)據(jù)移除緩存。
- LFU(Least Frequently Used):最不常用算法,在一段時(shí)間內(nèi)使用頻率最小的數(shù)據(jù)被移除緩存。
在分布式架構(gòu)的今天,多應(yīng)用中如果采用進(jìn)程內(nèi)緩存會(huì)存在數(shù)據(jù)一致性的問題。
這里推薦兩個(gè)方案:
- 消息隊(duì)列方案
應(yīng)用在修改完自身緩存數(shù)據(jù)和數(shù)據(jù)庫數(shù)據(jù)之后,給消息隊(duì)列發(fā)送數(shù)據(jù)變化通知,其他應(yīng)用訂閱了消息通知,在收到通知的時(shí)候修改緩存數(shù)據(jù)。
- 定時(shí)任務(wù)修改方案
為了避免耦合,降低復(fù)雜性,對(duì)“實(shí)時(shí)一致性”不敏感的情況下。每個(gè)應(yīng)用都會(huì)啟動(dòng)一個(gè)定時(shí)任務(wù),定時(shí)從數(shù)據(jù)庫拉取最新的數(shù)據(jù),更新緩存。
進(jìn)程內(nèi)緩存有哪些使用場(chǎng)景呢?
- 場(chǎng)景一:只讀數(shù)據(jù),可以考慮在進(jìn)程啟動(dòng)時(shí)加載到內(nèi)存。當(dāng)然,把數(shù)據(jù)加載到類似 Redis 這樣的進(jìn)程外緩存服務(wù)也能解決這類問題。
- 場(chǎng)景二:高并發(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),通常會(huì)用分布式緩存的方式實(shí)現(xiàn)。
分布式緩存是與應(yīng)用分離的緩存服務(wù),最大的特點(diǎn)是,自身是一個(gè)獨(dú)立的應(yīng)用/服務(wù),與本地應(yīng)用隔離,多個(gè)應(yīng)用可直接共享一個(gè)或者多個(gè)緩存應(yīng)用/服務(wù)。
為了提高緩存的可用性,會(huì)在原有的緩存節(jié)點(diǎn)上加入 Master/Slave 的設(shè)計(jì)。當(dāng)緩存數(shù)據(jù)寫入 Master 節(jié)點(diǎn)的時(shí)候,會(huì)同時(shí)同步一份到 Slave 節(jié)點(diǎn)。
一旦 Master 節(jié)點(diǎn)失效,可以通過代理直接切換到 Slave 節(jié)點(diǎn),這時(shí) Slave 節(jié)點(diǎn)就變成了 Master 節(jié)點(diǎn),保證緩存的正常工作。
每個(gè)緩存節(jié)點(diǎn)還會(huì)提供緩存過期的機(jī)制,并且會(huì)把緩存內(nèi)容定期以快照的方式保存到文件上,方便緩存崩潰之后啟動(dòng)預(yù)熱加載。
6.1 緩存雪崩
當(dāng)緩存失效,緩存過期被清除,緩存更新的時(shí)候。請(qǐng)求是無法命中緩存的,這個(gè)時(shí)候請(qǐng)求會(huì)直接回源到數(shù)據(jù)庫。
如果上述情況頻繁發(fā)生或者同時(shí)發(fā)生的時(shí)候,就會(huì)造成大面積的請(qǐng)求直接到數(shù)據(jù)庫,造成數(shù)據(jù)庫訪問瓶頸。我們稱這種情況為緩存雪崩。
從如下兩方面來思考解決方案:
緩存方面:
- 避免緩存同時(shí)失效,不同的 key 設(shè)置不同的超時(shí)時(shí)間。
- 增加互斥鎖,對(duì)緩存的更新操作進(jìn)行加鎖保護(hù),保證只有一個(gè)線程進(jìn)行緩存更新。緩存一旦失效可以通過緩存快照的方式迅速重建緩存。對(duì)緩存節(jié)點(diǎn)增加主備機(jī)制,當(dāng)主緩存失效以后切換到備用緩存繼續(xù)工作。
設(shè)計(jì)方面,這里給出了幾點(diǎn)建議供大家參考:
- 熔斷機(jī)制:某個(gè)緩存節(jié)點(diǎn)不能工作的時(shí)候,需要通知緩存代理不要把請(qǐng)求路由到該節(jié)點(diǎn),減少用戶等待和請(qǐng)求時(shí)長。
- 限流機(jī)制:在接入層和代理層可以做限流,當(dāng)緩存服務(wù)無法支持高并發(fā)的時(shí)候,前端可以把無法響應(yīng)的請(qǐng)求放入到隊(duì)列或者丟棄。
- 隔離機(jī)制:緩存無法提供服務(wù)或者正在預(yù)熱重建的時(shí)候,把該請(qǐng)求放入隊(duì)列中,這樣該請(qǐng)求因?yàn)楸桓綦x就不會(huì)被路由到其他的緩存節(jié)點(diǎn)。
- 如此就不會(huì)因?yàn)檫@個(gè)節(jié)點(diǎn)的問題影響到其他節(jié)點(diǎn)。當(dāng)緩存重建以后,再從隊(duì)列中取出請(qǐng)求依次處理。
62. 緩存穿透
緩存一般是 Key,Value 方式存在,一個(gè) Key 對(duì)應(yīng)的 Value 不存在時(shí),請(qǐng)求會(huì)回源到數(shù)據(jù)庫。
假如對(duì)應(yīng)的 Value 一直不存在,則會(huì)頻繁的請(qǐng)求數(shù)據(jù)庫,對(duì)數(shù)據(jù)庫造成訪問壓力。如果有人利用這個(gè)漏洞攻擊,就麻煩了。
解決方法:如果一個(gè) Key 對(duì)應(yīng)的 Value 查詢返回為空,我們?nèi)匀话堰@個(gè)空結(jié)果緩存起來,如果這個(gè)值沒有變化下次查詢就不會(huì)請(qǐng)求數(shù)據(jù)庫了。
將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的 Bitmap 中,那么不存在的數(shù)據(jù)會(huì)被這個(gè) Bitmap 過濾器攔截掉,避免對(duì)數(shù)據(jù)庫的查詢壓力。
6.3 緩存擊穿
在數(shù)據(jù)請(qǐng)求的時(shí)候,某一個(gè)緩存剛好失效或者正在寫入緩存,同時(shí)這個(gè)緩存數(shù)據(jù)可能會(huì)在這個(gè)時(shí)間點(diǎn)被超高并發(fā)請(qǐng)求,成為“熱點(diǎn)”數(shù)據(jù)。
這就是緩存擊穿問題,這個(gè)和緩存雪崩的區(qū)別在于,這里是針對(duì)某一個(gè)緩存,前者是針對(duì)多個(gè)緩存。
解決方案:導(dǎo)致問題的原因是在同一時(shí)間讀/寫緩存,所以只有保證同一時(shí)間只有一個(gè)線程寫,寫完成以后,其他的請(qǐng)求再使用緩存就可以了。
比較常用的做法是使用 mutex(互斥鎖)。在緩存失效的時(shí)候,不是立即寫入緩存,而是先設(shè)置一個(gè) mutex(互斥鎖)。當(dāng)緩存被寫入完成以后,再放開這個(gè)鎖讓請(qǐng)求進(jìn)行訪問。