在互聯網高速發展的今天,緩存技術被普遍地應用。不管業內仍是業外,只要是提到性能問題,你們都會脫口而出「用緩存解決」。前端
這種說法帶有片面性,甚至是隻知其一;不知其二,可是做爲專業人士的咱們,須要對緩存有更深、更廣的瞭解。算法
緩存技術存在於應用場景的方方面面。從瀏覽器請求,到反向代理服務器,從進程內緩存到分佈式緩存。其中緩存策略,算法也是層出不窮,今天就帶你們走進緩存。數據庫
緩存對於每一個開發者來講是至關熟悉了,爲了提升程序的性能咱們會去加緩存,可是在什麼地方加緩存,如何加緩存呢?編程
假設一個網站,須要提升性能,緩存能夠放在瀏覽器,能夠放在反向代理服務器,還能夠放在應用程序進程內,同時能夠放在分佈式緩存系統中。後端
從用戶請求數據到數據返回,數據通過了瀏覽器,CDN,代理服務器,應用服務器,以及數據庫各個環節。每一個環節均可以運用緩存技術。瀏覽器
從瀏覽器/客戶端開始請求數據,經過 HTTP 配合 CDN 獲取數據的變動狀況,到達代理服務器(Nginx)能夠經過反向代理獲取靜態資源。緩存
再往下來到應用服務器能夠經過進程內(堆內)緩存,分佈式緩存等遞進的方式獲取數據。若是以上全部緩存都沒有命中數據,纔會回源到數據庫。服務器
緩存的請求順序是:用戶請求 → HTTP 緩存 → CDN 緩存 → 代理服務器緩存 → 進程內緩存 → 分佈式緩存 → 數據庫。網絡
看來在技術的架構每一個環節均可以加入緩存,看看每一個環節是如何應用緩存技術的。數據結構
當用戶經過瀏覽器請求服務器的時候,會發起 HTTP 請求,若是對每次 HTTP 請求進行緩存,那麼能夠減小應用服務器的壓力。
當第一次請求的時候,瀏覽器本地緩存庫沒有緩存數據,會從服務器取數據,而且放到瀏覽器的緩存庫中,下次再進行請求的時候會根據緩存的策略來讀取本地或者服務的信息。
通常信息的傳遞經過 HTTP 請求頭 Header 來傳遞。目前比較常見的緩存方式有兩種,分別是:
強制緩存
對比緩存
當瀏覽器本地緩存庫保存了緩存信息,在緩存數據未失效的狀況下,能夠直接使用緩存數據。不然就須要從新獲取數據。
這種緩存機制看上去比較直接,那麼如何判斷緩存數據是否失效呢?這裏須要關注 HTTP Header 中的兩個字段 Expires 和 Cache-Control。
Expires 爲服務端返回的過時時間,客戶端第一次請求服務器,服務器會返回資源的過時時間。若是客戶端再次請求服務器,會把請求時間與過時時間作比較。
若是請求時間小於過時時間,那麼說明緩存沒有過時,則能夠直接使用本地緩存庫的信息。
反之,說明數據已通過期,必須從服務器從新獲取信息,獲取完畢又會更新最新的過時時間。
這種方式在 HTTP 1.0 用的比較多,到了 HTTP 1.1 會使用 Cache-Control 替代。
Cache-Control 中有個 max-age 屬性,單位是秒,用來表示緩存內容在客戶端的過時時間。
例如:max-age 是 60 秒,當前緩存沒有數據,客戶端第一次請求完後,將數據放入本地緩存。
那麼在 60 秒之內客戶端再發送請求,都不會請求應用服務器,而是從本地緩存中直接返回數據。若是兩次請求相隔時間超過了 60 秒,那麼就須要經過服務器獲取數據。
須要對比先後兩次的緩存標誌來判斷是否使用緩存。瀏覽器第一次請求時,服務器會將緩存標識與數據一塊兒返回,瀏覽器將兩者備份至本地緩存庫中。瀏覽器再次請求時,將備份的緩存標識發送給服務器。
服務器根據緩存標識進行判斷,若是判斷數據沒有發生變化,把判斷成功的 304 狀態碼發給瀏覽器。
這時瀏覽器就可使用緩存的數據來。服務器返回的就只是 Header,不包含 Body。
下面介紹兩種標識規則:
在客戶端第一次請求的時候,服務器會返回資源最後的修改時間,記做 Last-Modified。客戶端將這個字段連同資源緩存起來。
Last-Modified 被保存之後,在下次請求時會以 Last-Modified-Since 字段被髮送。
當客戶端再次請求服務器時,會把 Last-Modified 連同請求的資源一塊兒發給服務器,這時 Last-Modified 會被命名爲 If-Modified-Since,存放的內容都是同樣的。
服務器收到請求,會把 If-Modified-Since 字段與服務器上保存的 Last-Modified 字段做比較:
若服務器上的 Last-Modified 最後修改時間大於請求的 If-Modified-Since,說明資源被改動過,就會把資源(包括 Header+Body)從新返回給瀏覽器,同時返回狀態碼 200。
若資源的最後修改時間小於或等於 If-Modified-Since,說明資源沒有改動過,只會返回 Header,而且返回狀態碼 304。瀏覽器接受到這個消息就可使用本地緩存庫的數據。
注意:Last-Modified 和 If-Modified-Since 指的是同一個值,只是在客戶端和服務器端的叫法不一樣。
客戶端第一次請求的時候,服務器會給每一個資源生成一個 ETag 標記。這個 ETag 是根據每一個資源生成的惟一 Hash 串,資源如何發生變化 ETag 隨之更改,以後將這個 ETag 返回給客戶端,客戶端把請求的資源和 ETag 都緩存到本地。
ETag 被保存之後,在下次請求時會看成 If-None-Match 字段被髮送出去。
在瀏覽器第二次請求服務器相同資源時,會把資源對應的 ETag 一併發送給服務器。在請求時 ETag 轉化成 If-None-Match,但其內容不變。
服務器收到請求後,會把 If-None-Match 與服務器上資源的 ETag 進行比較:
若是不一致,說明資源被改動過,則返回資源(Header+Body),返回狀態碼 200。
若是一致,說明資源沒有被改過,則返回 Header,返回狀態碼 304。瀏覽器接受到這個消息就可使用本地緩存庫的數據。
注意:ETag 和 If-None-Match 指的是同一個值,只是在客戶端和服務器端的叫法不一樣。
HTTP 緩存主要是對靜態數據進行緩存,把從服務器拿到的數據緩存到客戶端/瀏覽器。
若是在客戶端和服務器之間再加上一層 CDN,可讓 CDN 爲應用服務器提供緩存,若是在 CDN 上緩存,就不用再請求應用服務器了。而且 HTTP 緩存提到的兩種策略一樣能夠在 CDN 服務器執行。
CDN 的全稱是 Content Delivery Network,即內容分發網絡。
讓咱們來看看它是如何工做的吧:
客戶端發送 URL 給 DNS 服務器。
DNS 經過域名解析,把請求指向 CDN 網絡中的 DNS 負載均衡器。
DNS 負載均衡器將最近 CDN 節點的 IP 告訴 DNS,DNS 告之客戶端最新 CDN 節點的 IP。
客戶端請求最近的 CDN 節點。
CDN 節點從應用服務器獲取資源返回給客戶端,同時將靜態信息緩存。注意:客戶端下次互動的對象就是 CDN 緩存了,CDN 能夠和應用服務器同步緩存信息。
CDN 接受客戶端的請求,它就是離客戶端最近的服務器,它後面會連接多臺服務器,起到了緩存和負載均衡的做用。
說完客戶端(HTTP)緩存和 CDN 緩存,咱們離應用服務愈來愈近了,在到達應用服務以前,請求還要通過負載均衡器。
雖然說它的主要工做是對應用服務器進行負載均衡,可是它也能夠做緩存。能夠把一些修改頻率不高的數據緩存在這裏,例如:用戶信息,配置信息。經過服務按期刷新這個緩存就好了。
以 Nginx 爲例,咱們看看它是如何工做的:
用戶請求在達到應用服務器以前,會先訪問 Nginx 負載均衡器,若是發現有緩存信息,直接返回給用戶。
若是沒有發現緩存信息,Nginx 回源到應用服務器獲取信息。
另外,有一個緩存更新服務,按期把應用服務器中相對穩定的信息更新到 Nginx 本地緩存中。
經過了客戶端,CDN,負載均衡器,咱們終於來到了應用服務器。應用服務器上部署着一個個應用,這些應用以進程的方式運行着,那麼在進程中的緩存是怎樣的呢?
進程內緩存又叫託管堆緩存,以 Java 爲例,這部分緩存放在 JVM 的託管堆上面,同時會受到託管堆回收算法的影響。
因爲其運行在內存中,對數據的響應速度很快,一般咱們會把熱點數據放在這裏。
在進程內緩存沒有命中的時候,咱們會去搜索進程外的緩存或者分佈式緩存。這種緩存的好處是沒有序列化和反序列化,是最快的緩存。缺點是緩存的空間不能太大,對垃圾回收器的性能有影響。
目前比較流行的實現有 Ehcache、GuavaCache、Caffeine。這些架構能夠很方便的把一些熱點數據放到進程內的緩存中。
這裏咱們須要關注幾個緩存的回收策略,具體的實現架構的回收策略會有所不一樣,但大體的思路都是一致的:
FIFO(First In First Out):先進先出算法,最早放入緩存的數據最早被移除。
LRU(Least Recently Used):最近最少使用算法,把最久沒有使用過的數據移除緩存。
LFU(Least Frequently Used):最不經常使用算法,在一段時間內使用頻率最小的數據被移除緩存。
在分佈式架構的今天,多應用中若是採用進程內緩存會存在數據一致性的問題。
這裏推薦兩個方案:
消息隊列修改方案
Timer 修改方案
應用在修改完自身緩存數據和數據庫數據以後,給消息隊列發送數據變化通知,其餘應用訂閱了消息通知,在收到通知的時候修改緩存數據。
爲了不耦合,下降複雜性,對「實時一致性」不敏感的狀況下。每一個應用都會啓動一個 Timer,定時從數據庫拉取最新的數據,更新緩存。
不過在有的應用更新數據庫後,其餘節點經過 Timer 獲取數據之間,會讀到髒數據。這裏須要控制好 Timer 的頻率,以及應用與對實時性要求不高的場景。
進程內緩存有哪些使用場景呢?
場景一:只讀數據,能夠考慮在進程啓動時加載到內存。固然,把數據加載到相似 Redis 這樣的進程外緩存服務也能解決這類問題。
場景二:高併發,能夠考慮使用進程內緩存,例如:秒殺。
說完進程內緩存,天然就過分到進程外緩存了。與進程內緩存不一樣,進程外緩存在應用運行的進程以外,它擁有更大的緩存容量,而且能夠部署到不一樣的物理節點,一般會用分佈式緩存的方式實現。
分佈式緩存是與應用分離的緩存服務,最大的特色是,自身是一個獨立的應用/服務,與本地應用隔離,多個應用可直接共享一個或者多個緩存應用/服務。
既然是分佈式緩存,緩存的數據會分佈到不一樣的緩存節點上,每一個緩存節點緩存的數據大小一般也是有限制的。
數據被緩存到不一樣的節點,爲了能方便的訪問這些節點,須要引入緩存代理,相似 Twemproxy。他會幫助請求找到對應的緩存節點。
同時若是緩存節點增長了,這個代理也會只能識別而且把新的緩存數據分片到新的節點,作橫向的擴展。
爲了提升緩存的可用性,會在原有的緩存節點上加入 Master/Slave 的設計。當緩存數據寫入 Master 節點的時候,會同時同步一份到 Slave 節點。
一旦 Master 節點失效,能夠經過代理直接切換到 Slave 節點,這時 Slave 節點就變成了 Master 節點,保證緩存的正常工做。
每一個緩存節點還會提供緩存過時的機制,而且會把緩存內容按期以快照的方式保存到文件上,方便緩存崩潰以後啓動預熱加載。
當緩存作成分佈式的時候,數據會根據必定的規律分配到每一個緩存應用/服務上。
若是咱們把這些緩存應用/服務叫作緩存節點,每一個節點通常均可以緩存必定容量的數據,例如:Redis 一個節點能夠緩存 2G 的數據。
若是須要緩存的數據量比較大就須要擴展多個緩存節點來實現,這麼多的緩存節點,客戶端的請求不知道訪問哪一個節點怎麼辦?緩存的數據又如何放到這些節點上?
緩存代理服務已經幫咱們解決這些問題了,例如:Twemproxy 不但能夠幫助緩存路由,同時能夠管理緩存節點。
這裏有介紹三種緩存數據分片的算法,有了這些算法緩存代理就能夠方便的找到分片的數據了。
Hash 表是最多見的數據結構,實現方式是,對數據記錄的關鍵值進行 Hash,而後再對須要分片的緩存節點個數進行取模獲得的餘數進行數據分配。
例如:有三條記錄數據分別是 R1,R2,R3。他們的 ID 分別是 01,02,03,假設對這三個記錄的 ID 做爲關鍵值進行 Hash 算法以後的結果依舊是 01,02,03。
咱們想把這三條數據放到三個緩存節點中,能夠把這個結果分別對 3 這個數字取模獲得餘數,這個餘數就是這三條記錄分別放置的緩存節點。
Hash 算法是某種程度上的平均放置,策略比較簡單,若是要增長緩存節點,對已經存在的數據會有較大的變更。
一致性 Hash 是將數據按照特徵值映射到一個首尾相接的 Hash 環上,同時也將緩存節點映射到這個環上。
若是要緩存數據,經過數據的關鍵值(Key)在環上找到本身存放的位置。這些數據按照自身的 ID 取 Hash 以後獲得的值按照順序在環上排列。
若是這個時候要插入一條新的數據其 ID 是 115,那麼就應該插入到以下圖的位置。
同理若是要增長一個緩存節點 N4 150,也能夠放到以下圖的位置。
這種算法對於增長緩存數據,和緩存節點的開銷相對比較小。
這種方式是按照關鍵值(例如 ID)將數據劃分紅不一樣的區間,每一個緩存節點負責一個或者多個區間。跟一致性哈希有點像。
例如:存在三個緩存節點分別是 N1,N2,N3。他們用來存放數據的區間分別是,N1(0, 100], N2(100, 200], N3(300, 400]。
那麼數據根據本身 ID 做爲關鍵字作 Hash 之後的結果就會分別對應放到這幾個區域裏面了。
根據事物的兩面性,在分佈式緩存帶來高性能的同時,咱們也須要重視它的可用性。那麼哪些潛在的風險是咱們須要防範的呢?
當緩存失效,緩存過時被清除,緩存更新的時候。請求是沒法命中緩存的,這個時候請求會直接回源到數據庫。
若是上述狀況頻繁發生或者同時發生的時候,就會形成大面積的請求直接到數據庫,形成數據庫訪問瓶頸。咱們稱這種狀況爲緩存雪崩。
從以下兩方面來思考解決方案:
緩存方面:
避免緩存同時失效,不一樣的 key 設置不一樣的超時時間。
增長互斥鎖,對緩存的更新操做進行加鎖保護,保證只有一個線程進行緩存更新。緩存一旦失效能夠經過緩存快照的方式迅速重建緩存。對緩存節點增長主備機制,當主緩存失效之後切換到備用緩存繼續工做。
設計方面,這裏給出了幾點建議供你們參考:
熔斷機制:某個緩存節點不能工做的時候,須要通知緩存代理不要把請求路由到該節點,減小用戶等待和請求時長。
限流機制:在接入層和代理層能夠作限流,當緩存服務沒法支持高併發的時候,前端能夠把沒法響應的請求放入到隊列或者丟棄。
隔離機制:緩存沒法提供服務或者正在預熱重建的時候,把該請求放入隊列中,這樣該請求由於被隔離就不會被路由到其餘的緩存節點。
如此就不會由於這個節點的問題影響到其餘節點。當緩存重建之後,再從隊列中取出請求依次處理。
緩存通常是 Key,Value 方式存在,一個 Key 對應的 Value 不存在時,請求會回源到數據庫。
假如對應的 Value 一直不存在,則會頻繁的請求數據庫,對數據庫形成訪問壓力。若是有人利用這個漏洞攻擊,就麻煩了。
解決方法:若是一個 Key 對應的 Value 查詢返回爲空,咱們仍然把這個空結果緩存起來,若是這個值沒有變化下次查詢就不會請求數據庫了。
將全部可能存在的數據哈希到一個足夠大的 Bitmap 中,那麼不存在的數據會被這個 Bitmap 過濾器攔截掉,避免對數據庫的查詢壓力。
在數據請求的時候,某一個緩存恰好失效或者正在寫入緩存,同時這個緩存數據可能會在這個時間點被超高併發請求,成爲「熱點」數據。
這就是緩存擊穿問題,這個和緩存雪崩的區別在於,這裏是針對某一個緩存,前者是針對多個緩存。
解決方案:致使問題的緣由是在同一時間讀/寫緩存,因此只有保證同一時間只有一個線程寫,寫完成之後,其餘的請求再使用緩存就能夠了。
比較經常使用的作法是使用 mutex(互斥鎖)。在緩存失效的時候,不是當即寫入緩存,而是先設置一個 mutex(互斥鎖)。當緩存被寫入完成之後,再放開這個鎖讓請求進行訪問。
總結一下,緩存設計有五大策略,從用戶請求開始依次是:
HTTP 緩存
CDN 緩存
負載均衡緩存
進程內緩存
分佈式緩存
其中,前兩種緩存靜態數據,後三種緩存動態數據:
HTTP 緩存包括強制緩存和對比緩存。
CDN 緩存和 HTTP 緩存是好搭檔。
負載均衡器緩存相對穩定資源,須要服務協助工做。
進程內緩存,效率高,但容量有限制,有兩個方案能夠應對緩存同步的問題。
分佈式緩存容量大,能力強,牢記三個性能算法而且防範三個緩存風險。
關注公衆號【零壹技術棧】,持續推送高質量的後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。