更新時間:2022-10-14 來源:黑馬程序員 瀏覽量:
前言
提到緩存,想必每一位軟件工程師都不陌生,它是目前架構(gòu)設(shè)計中提高性能最直接的方式。
緩存技術(shù)存在于應(yīng)用場景的方方面面。從網(wǎng)站提高性能的角度分析,緩存可以放在瀏覽器,可以放在反向代理服務(wù)器,還可以放在應(yīng)用程序進程內(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é)都可以運用緩存技術(shù)。
緩存的請求順序是:用戶請求 → HTTP 緩存 → CDN 緩存 → Nginx代理緩存 → 進程內(nèi)緩存 → 分布式緩存。
在技術(shù)的架構(gòu)每個環(huán)節(jié)都可以加入緩存,我們看看每個環(huán)節(jié)是如何應(yīng)用緩存技術(shù)的。
2. HTTP緩存
通常 HTTP 緩存策略分為兩種:
- 強緩存
- 協(xié)商緩存。
從字面意思我們可以很直觀的看到它們的差別:
- 強緩存即強制直接使用緩存。
- 協(xié)商緩存就得和服務(wù)器協(xié)商確認下這個緩存能不能用。
強緩存
強緩存不會向服務(wù)器發(fā)送請求,直接從緩存中讀取資源,在 chrome 控制臺的 network 選項中可以看到該請求返回 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)頭以及請求頭來控制緩存策略。
強緩存可以通過設(shè)置`Expires`和`Cache-Control` 兩種響應(yīng)頭實現(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。可以通過指定多個指令來實現(xiàn)緩存機制。主要用表示資源緩存的最大有效時間。即在該時間端內(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` 不使用強緩存,需要與服務(wù)器驗協(xié)商緩存驗證。
- `no-store` 緩存不應(yīng)存儲有關(guān)客戶端請求或服務(wù)器響應(yīng)的任何內(nèi)容,即不使用任何緩存。
過期
- `max-age=` 緩存存儲的最大周期,超過這個周期被認為過期。
- `s-maxage=` 設(shè)置共享緩存。會覆蓋`max-age`和`expires`,私有緩存會忽略它
- `max-stale[=]` 客戶端愿意接收一個已經(jīng)過期的資源,可以設(shè)置一個可選的秒數(shù),表示響應(yīng)不能已經(jīng)過時超過該給定的時間。
- `min-fresh=` 客戶端希望在指定的時間內(nèi)獲取最新的響應(yīng)
重新驗證和重新加載
- `must-revalidate` 如頁面過期,則去服務(wù)器進行獲取。
- `proxy-revalidate` 與`must-revalidate` 作用相同,但是用于共享緩存。
其他
- `only-if-cached` 不進行網(wǎng)絡(luò)請求,完全只使用緩存。
- `no-transform` 不得對資源進行轉(zhuǎn)換和轉(zhuǎ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`,則與被請求資源的最后修改時間進行對比,如果一致則返回 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)這種情況,一個資源被修改了,但其實際內(nèi)容根本沒發(fā)生改變,會因為`Last-Modified`時間匹配不上而返回了整個實體給客戶端(即使客戶端緩存里有個一模一樣的資源)。為了解決這個問題,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)識進行對比。如果相同說明資源沒有修改,則響應(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ù)器,通過中心平臺的負載均衡、調(diào)度及內(nèi)容分發(fā)等功能模塊,使用戶在請求所需訪問的內(nèi)容時能夠就近獲取,以此來降低網(wǎng)絡(luò)擁塞,提高資源對用戶的響應(yīng)速度。
本地存儲和瀏覽器緩存帶來的性能提升主要針對的是瀏覽器端已經(jīng)緩存了所需的資源,當(dāng)發(fā)生二次請求相同資源時便能夠進行快速響應(yīng),避免重新發(fā)起請求或重新下載全部響應(yīng)資源。
這些方法對于首次資源請求的性能提升是無能為力的,若想提升首次請求資源的響應(yīng)速度,除了資源壓縮、圖片優(yōu)化等方式,還可借助CDN技術(shù)。
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的負載均衡器的地址。
3. 瀏覽器會重新向該負載均衡器發(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ò)的核心功能包括兩點:
緩存與回源
緩存指的是將所需的靜態(tài)資源文件復(fù)制一份到CDN緩存服務(wù)器上;
回源指的是如果未在CDN緩存服務(wù)器上查找到目標(biāo)資源,或CDN緩存服務(wù)器上的緩存資源已經(jīng)過期,則重新追溯到網(wǎng)站根服務(wù)器獲取相關(guān)資源的過程。
4. Nginx代理緩存
用戶請求在達到應(yīng)用服務(wù)器之前,會先訪問 Nginx 負載均衡器,如果發(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
對于站點中不經(jīng)常修改的靜態(tài)內(nèi)容(如圖片,JS,CSS),可以在服務(wù)器中設(shè)置expires過期時間,控制瀏覽器緩存,達到有效減小帶寬流量,降低服務(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)用。
默認情況下,NGINX尊重Cache-Control源服務(wù)器的標(biāo)頭。它不緩存響應(yīng)Cache-Control設(shè)置為Private,No-Cache或No-Store或Set-Cookie在響應(yīng)頭。NGINX只緩存GET和HEAD客戶端請求。
如下配置可覆蓋這些默認值:
- proxy_buffering默認為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. 進程緩存
通過了客戶端,CDN,Nginx代理緩存,我們終于來到了應(yīng)用服務(wù)器。應(yīng)用服務(wù)器上部署著一個個應(yīng)用,這些應(yīng)用以進程的方式運行著,那么在進程中的緩存是怎樣的呢?
進程內(nèi)緩存又叫托管堆緩存,以 Java 為例,這部分緩存放在 JVM 的托管堆上面,同時會受到托管堆回收算法的影響。
由于其運行在內(nèi)存中,對數(shù)據(jù)的響應(yīng)速度很快,通常我們會把熱點數(shù)據(jù)放在這里。
在進程內(nèi)緩存沒有命中的時候,我們會去搜索進程外的緩存或者分布式緩存。這種緩存的好處是沒有序列化和反序列化,是最快的緩存。缺點是緩存的空間不能太大,對垃圾回收器的性能有影響。
目前比較流行的實現(xiàn)有 Ehcache、GuavaCache、Caffeine。這些架構(gòu)可以很方便的把一些熱點數(shù)據(jù)放到進程內(nèi)的緩存中。
這里我們需要關(guān)注幾個緩存的回收策略,具體的實現(xiàn)架構(gòu)的回收策略會有所不同,但大致的思路都是一致的:
- FIFO(First In First Out):先進先出算法,最先放入緩存的數(shù)據(jù)最先被移除。
- LRU(Least Recently Used):最近最少使用算法,把最久沒有使用過的數(shù)據(jù)移除緩存。
- LFU(Least Frequently Used):最不常用算法,在一段時間內(nèi)使用頻率最小的數(shù)據(jù)被移除緩存。
在分布式架構(gòu)的今天,多應(yīng)用中如果采用進程內(nèi)緩存會存在數(shù)據(jù)一致性的問題。
這里推薦兩個方案:
- 消息隊列方案
應(yīng)用在修改完自身緩存數(shù)據(jù)和數(shù)據(jù)庫數(shù)據(jù)之后,給消息隊列發(fā)送數(shù)據(jù)變化通知,其他應(yīng)用訂閱了消息通知,在收到通知的時候修改緩存數(shù)據(jù)。
- 定時任務(wù)修改方案
為了避免耦合,降低復(fù)雜性,對“實時一致性”不敏感的情況下。每個應(yīng)用都會啟動一個定時任務(wù),定時從數(shù)據(jù)庫拉取最新的數(shù)據(jù),更新緩存。
進程內(nèi)緩存有哪些使用場景呢?
- 場景一:只讀數(shù)據(jù),可以考慮在進程啟動時加載到內(nèi)存。當(dāng)然,把數(shù)據(jù)加載到類似 Redis 這樣的進程外緩存服務(wù)也能解決這類問題。
- 場景二:高并發(fā),可以考慮使用進程內(nèi)緩存,例如:秒殺。
6. 分布式緩存
說完進程內(nèi)緩存,自然就過度到進程外緩存了。
與進程內(nèi)緩存不同,進程外緩存在應(yīng)用運行的進程之外,它擁有更大的緩存容量,并且可以部署到不同的物理節(jié)點,通常會用分布式緩存的方式實現(xiàn)。
分布式緩存是與應(yīng)用分離的緩存服務(wù),最大的特點是,自身是一個獨立的應(yīng)用/服務(wù),與本地應(yīng)用隔離,多個應(yīng)用可直接共享一個或者多個緩存應(yīng)用/服務(wù)。
為了提高緩存的可用性,會在原有的緩存節(jié)點上加入 Master/Slave 的設(shè)計。當(dāng)緩存數(shù)據(jù)寫入 Master 節(jié)點的時候,會同時同步一份到 Slave 節(jié)點。
一旦 Master 節(jié)點失效,可以通過代理直接切換到 Slave 節(jié)點,這時 Slave 節(jié)點就變成了 Master 節(jié)點,保證緩存的正常工作。
每個緩存節(jié)點還會提供緩存過期的機制,并且會把緩存內(nèi)容定期以快照的方式保存到文件上,方便緩存崩潰之后啟動預(yù)熱加載。
6.1 緩存雪崩
當(dāng)緩存失效,緩存過期被清除,緩存更新的時候。請求是無法命中緩存的,這個時候請求會直接回源到數(shù)據(jù)庫。
如果上述情況頻繁發(fā)生或者同時發(fā)生的時候,就會造成大面積的請求直接到數(shù)據(jù)庫,造成數(shù)據(jù)庫訪問瓶頸。我們稱這種情況為緩存雪崩。
從如下兩方面來思考解決方案:
緩存方面:
- 避免緩存同時失效,不同的 key 設(shè)置不同的超時時間。
- 增加互斥鎖,對緩存的更新操作進行加鎖保護,保證只有一個線程進行緩存更新。緩存一旦失效可以通過緩存快照的方式迅速重建緩存。對緩存節(jié)點增加主備機制,當(dāng)主緩存失效以后切換到備用緩存繼續(xù)工作。
設(shè)計方面,這里給出了幾點建議供大家參考:
- 熔斷機制:某個緩存節(jié)點不能工作的時候,需要通知緩存代理不要把請求路由到該節(jié)點,減少用戶等待和請求時長。
- 限流機制:在接入層和代理層可以做限流,當(dāng)緩存服務(wù)無法支持高并發(fā)的時候,前端可以把無法響應(yīng)的請求放入到隊列或者丟棄。
- 隔離機制:緩存無法提供服務(wù)或者正在預(yù)熱重建的時候,把該請求放入隊列中,這樣該請求因為被隔離就不會被路由到其他的緩存節(jié)點。
- 如此就不會因為這個節(jié)點的問題影響到其他節(jié)點。當(dāng)緩存重建以后,再從隊列中取出請求依次處理。
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ù)可能會在這個時間點被超高并發(fā)請求,成為“熱點”數(shù)據(jù)。
這就是緩存擊穿問題,這個和緩存雪崩的區(qū)別在于,這里是針對某一個緩存,前者是針對多個緩存。
解決方案:導(dǎo)致問題的原因是在同一時間讀/寫緩存,所以只有保證同一時間只有一個線程寫,寫完成以后,其他的請求再使用緩存就可以了。
比較常用的做法是使用 mutex(互斥鎖)。在緩存失效的時候,不是立即寫入緩存,而是先設(shè)置一個 mutex(互斥鎖)。當(dāng)緩存被寫入完成以后,再放開這個鎖讓請求進行訪問。