深入解析SpringMVC核心原理:從手寫簡易版MVC框架開始(SmartMvc) : https://github.com/silently9527/SmartMvcIDEA多線程文件下載插件: https://github.com/silently9527/FastDownloadIdeaPlugin
公眾號:貝塔學JAVA
摘要
緩存的目的是為了提高系統的訪問速度,讓數據更加接近於使用者,通常也是提升性能的常用手段。緩存在生活中其實也是無處不在,比如物流系統,他們基本上在各地都有分倉庫,如果本地倉庫有數據,那麼送貨的速度就會很快;CPU讀取數據也採用了緩存,寄存器->高速緩存->內存->硬盤/網絡;我們經常使用的maven倉庫也同樣有本地倉庫和遠程倉庫。現階段緩存的使用場景也越來越多,比如:瀏覽器緩存、反向代理層緩存、應用層緩存、數據庫查詢緩存、分佈式集中緩存。
本文我們就先從瀏覽器緩存和Nginx緩存開始聊起。
瀏覽器緩存
瀏覽器緩存是指當我們去訪問一個網站或者Http服務的時候,服務器可以設置Http的響應頭信息,其中如果設置緩存相關的頭信息,那麼瀏覽器就會緩存這些數據,下次再訪問這些數據的時候就直接從瀏覽器緩存中獲取或者是隻需要去服務器中校驗下緩存時候有效,可以減少瀏覽器與服務器之間的網絡時間的開銷以及節省帶寬。
Htpp相關的知識,歡迎去參觀 《面試篇》Http協議
Cache-Control
該命令是通用首部字段(請求首部和響應首部都可以使用),用於控制緩存的工作機制,該命令參數稍多,常用的參數:
- no-cache: 表示不需要緩存該資源
- max-age(秒): 緩存的最大有效時間,當max-age=0時,表示不需要緩存
Expires
控制資源失效的日期,當瀏覽器接受到Expires之後,瀏覽器都會使用本地的緩存,在過期日期之後才會向務器發送請求;如果服務器同時在響應頭中也指定了Cache-Control的max-age指令時,瀏覽器會優先處理max-age。
如果服務器不想要讓瀏覽器對資源緩存,可以把Expires和首部字段Date設置相同的值
Last-Modified / If-Modified-Since
Last-Modified
Last-Modified 用於指明資源最終被修改的時間。配合If-Modified-Since一起使用可以通過時間對緩存是否有效進行校驗;後面實戰會使用到這種方式。
If-Modified-Since
如果請求頭中If-Modified-Since的日期早於請求資源的更新日期,那麼服務會進行處理,返回最新的資源;如果If-Modified-Since指定的日期之後請求的資源都未更新過,那麼服務不會處理請求並返回304 Mot Modified的響應,表示緩存的文件有效可以繼續使用。
實戰事例
使用SpringMVC做緩存的測試代碼:
@ResponseBody
@RequestMapping("/http/cache")
public ResponseEntity<String> cache(@RequestHeader(value = "If-Modified-Since", required = false)
String ifModifiedSinceStr) throws ParseException {
DateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
Date ifModifiedSince = dateFormat.parse(ifModifiedSinceStr);
long lastModifiedDate = getLastModifiedDate(ifModifiedSince);//獲取文檔最後更新時間
long now = System.currentTimeMillis();
int maxAge = 30; //數據在瀏覽器端緩存30秒
//判斷文檔是否被修改過
if (Objects.nonNull(ifModifiedSince) && ifModifiedSince.getTime() == lastModifiedDate) {
HttpHeaders headers = new HttpHeaders();
headers.add("Date", dateFormat.format(new Date(now))); //設置當前時間
headers.add("Expires", dateFormat.format(new Date(now + maxAge * 1000))); //設置過期時間
headers.add("Cache-Control", "max-age=" + maxAge);
return new ResponseEntity<>(headers, HttpStatus.NOT_MODIFIED);
}
//文檔已經被修改過
HttpHeaders headers = new HttpHeaders();
headers.add("Date", dateFormat.format(new Date(now))); //設置當前時間
headers.add("Last-Modified", dateFormat.format(new Date(lastModifiedDate))); //設置最近被修改的日期
headers.add("Expires", dateFormat.format(new Date(now + maxAge * 1000))); //設置過期時間
headers.add("Cache-Control", "max-age=" + maxAge);
String responseBody = JSON.toJSONString(ImmutableMap.of("website", "https://silently9527.cn"));
return new ResponseEntity<>(responseBody, headers, HttpStatus.OK);
}
//獲取文檔的最後更新時間,方便測試,每15秒換一次;去掉毫秒值
private long getLastModifiedDate(Date ifModifiedSince) {
long now = System.currentTimeMillis();
if (Objects.isNull(ifModifiedSince)) {
return now;
}
long seconds = (now - ifModifiedSince.getTime()) / 1000;
if (seconds > 15) {
return now;
}
return ifModifiedSince.getTime();
}
- 當第一次訪問
http://localhost:8080/http/cache的時候,我們可以看到如下的響應頭信息:
前面我們已提到了Cache-Control的優先級高於Expires,實際的項目中我們可以同時使用,或者只使用Cache-Control。Expires的值通常情況下都是系統當前時間+緩存過期時間
- 當我們在15秒之內再次訪問
http://localhost:8080/http/cache會看到如下的請求頭:
此時發送到服務器端的頭信息If-Modified-Since就是上次請求服務器返回的Last-Modified,瀏覽器會拿這個時間去和服務器校驗內容是否發送了變化,由於我們後台程序在15秒之內都表示沒有修改過內容,所以得到了如下的響應頭信息
響應的狀態碼304,表示服務器告訴瀏覽器,你的緩存是有效的可以繼續使用。
If-None-Match / ETag
If-None-Match
請求首部字段If-None-Match傳輸給服務器的值是服務器返回的ETag值,只有當服務器上請求資源的ETag值與If-None-Match不一致時,服務器才去處理該請求。
ETag
響應首部字段ETag能夠告知客服端響應實體的標識,它是一種可將資源以字符串的形式做唯一標識的方式。服務器可以為每份資源指定一個ETag值。當資源被更新時,ETag的值也會被更新。通常生成ETag值的算法使用的是md5。
- 強ETag值:不論實體發生了多麼細微的變化都會改變其值
- 弱ETag值:只用於提示資源是否相同,只有當資源發送了根本上的變化,ETag才會被改變。使用弱ETag值需要在前面添加
W/
ETag: W/"etag-xxxx"
通常建議選擇弱ETag值,因為大多數時候我們都會在代理層開啓gzip壓縮,弱ETag可以驗證壓縮和不壓縮的實體,而強ETag值要求響應實體字節必須完全一致。
實戰事例
@ResponseBody
@RequestMapping("/http/etag")
public ResponseEntity<String> etag(@RequestHeader(value = "If-None-Match", required = false)
String ifNoneMatch) throws ParseException {
long now = System.currentTimeMillis();
int maxAge = 30; //數據在瀏覽器端緩存30秒
String responseBody = JSON.toJSONString(ImmutableMap.of("website", "https://silently9527.cn"));
String etag = "W/\"" + MD5Encoder.encode(responseBody.getBytes()) + "\""; //弱ETag值
if (etag.equals(ifNoneMatch)) {
return new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
}
DateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
HttpHeaders headers = new HttpHeaders();
headers.add("ETag", etag);
headers.add("Date", dateFormat.format(new Date(now))); //設置當前時間
headers.add("Cache-Control", "max-age=" + maxAge);
return new ResponseEntity<>(responseBody, headers, HttpStatus.OK);
}
ETag是用於發送到服務器端進行內容變更驗證的,第一次請求http://localhost:8080/http/etag,獲取到的響應頭信息:
在30秒之內,我們再次刷新頁面,可以看到如下的請求頭信息:
這裏的If-None-Match就是上一次請求服務返回的ETag值,服務器校驗If-None-Match值與ETag值相等,所以返回了304告訴瀏覽器緩存可以用。
ETag與Last-Modified兩者應該如何選擇?
通過上面的兩個事例我們可以看出ETag需要服務器先查詢出需要響應的內容,然後計算出ETag值,再與瀏覽器請求頭中If-None-Match來比較覺得是否需要返回數據,對於服務器來説僅僅是節省了帶寬,原本應該服務器調用後端服務查詢的信息依然沒有被省掉;而Last-Modified通過時間的比較,如果內容沒有更新,服務器不需要調用後端服務查詢出響應數據,不僅節省了服務器的帶寬也降低了後端服務的壓力;
對比之後得出結論:通常來説為了降低後端服務的壓力ETag適用於圖片/js/css等靜態資源,而類似用户詳情信息需要調用後端服務的數據適合使用Last-Modified來處理。
Nginx
通常情況下我們都會使用到Nginx來做反向代理服務器,我們可以通過緩衝、緩存來對Nginx進行調優,本篇我們就從這兩個方面來聊聊Nginx調優
緩衝
默認情況下,Nginx在返回響應給客户端之前會盡可能快的從上游服務器獲取數據,Nginx會盡可能的將上有服務器返回的數據緩衝到本地,然後一次性的全部返回給客户端,如果每次從上游服務器返回的數據都需要寫入到磁盤中,那麼Nginx的性能肯定會降低;所以我們需要根據實際情況對Nginx的緩存做優化。
proxy_buffer_size: 設置Nginx緩衝區的大小,用來存儲upstream端響應的header。proxy_buffering: 啓用代理內容緩衝,當該功能禁用時,代理一接收到上游服務器的返回就立即同步的發送給客户端,proxy_max_temp_file_size被設置為0;通過設置proxy_buffering為on,proxy_max_temp_file_size為0 可以確保代理的過程中不適用磁盤,只是用緩衝區; 開啓後proxy_buffers和proxy_busy_buffers_size參數才會起作用proxy_buffers: 設置響應上游服務器的緩存數量和大小,當一個緩衝區佔滿後會申請開啓下一個緩衝區,直到緩衝區數量到達設置的最大值proxy_busy_buffers_size:proxy_busy_buffers_size不是獨立的空間,他是proxy_buffers和proxy_buffer_size的一部分。nginx會在沒有完全讀完後端響應就開始向客户端傳送數據,所以它會劃出一部分busy狀態的buffer來專門向客户端傳送數據(建議為proxy_buffers中單個緩衝區的2倍),然後它繼續從後端取數據。
proxy_busy_buffer_size參數用來設置處於busy狀態的buffer有多大。
1)如果完整數據大小小於busy_buffer大小,當數據傳輸完成後,馬上傳給客户端;
2)如果完整數據大小不小於busy_buffer大小,則裝滿busy_buffer後,馬上傳給客户端;
典型是設置成proxy_buffers的兩倍。
Nginx代理緩衝的設置都是作用到每一個請求的,想要設置緩衝區的大小到最佳狀態,需要測量出經過反向代理服務器器的平均請求數和響應的大小;proxy_buffers指令的默認值 8個 4KB 或者 8個 8KB(具體依賴於操作系統),假如我們的服務器是1G,這台服務器只運行了Nginx服務,那麼排除到操作系統的內存使用,保守估計Nginx能夠使用的內存是768M
- 每個活動的連接使用緩衝內存:8個4KB = 8 4 1024 = 32768字節
- 系統可使用的內存大小768M: 768 1024 1024 = 805306368字節
- 所以Nginx能夠同時處理的連接數:805306368 / 32768 = 24576
經過我們的粗略估計,1G的服務器只運行Nginx大概可以同時處理24576個連接。
假如我們測量和發現經過反向代理服務器響應的平均數據大小是 900KB , 而默認的 8個4KB的緩衝區是無法滿足的,所以我們可以調整大小
http {
proxy_buffers 30 32k;
}
這樣設置之後每次請求可以達到最快的響應,但是同時處理的連接數減少了,(768 * 1024 * 1024) / (30 * 32 * 1024)=819個活動連接;
如果我們系統的併發數不是太高,我們可以將proxy_buffers緩衝區的個數下調,設置稍大的proxy_busy_buffers_size加大往客户端發送的緩衝區,以確保Nginx在傳輸的過程中能夠把從上游服務器讀取到的數據全部寫入到緩衝區中。
http {
proxy_buffers 10 32k;
proxy_busy_buffers_size 64k;
}
緩存
Nignx除了可以緩衝上游服務器的響應達到快速返回給客户端,它還可以是實現響應的緩存,通過上圖我們可以看到
- 1A: 一個請求到達Nginx,先從緩存中嘗試獲取
- 1B: 緩存不存在直接去上游服務器獲取數據
- 1C: 上游服務器返回響應,Nginx把響應放入到緩存
- 1D: 把響應返回到客户端
- 2A: 另一個請求達到Nginx, 到緩存中查找
- 2B: 緩存中有對應的數據,直接返回,不去上游服務器獲取數據
Nginx的緩存常用配置:
proxy_cache_path: 放置緩存響應和共享的目錄。levels設置緩存文件目錄層次, levels=1:2 表示兩級目錄,最多三層,其中 1 表示一級目錄使用一位16進製作為目錄名,2 表示二級目錄使用兩位16進製作為目錄名,如果文件都存放在一個目錄中,文件量大了會導致文件訪問變慢。keys_zone設置緩存名字和共享內存大小,inactive當被放入到緩存後如果不被訪問的最大存活時間,max_size設置緩存的最大空間proxy_cache: 定義響應應該存放到哪個緩存區中(keys_zone設置的名字)proxy_cache_key: 設置緩存使用的Key, 默認是完整的訪問URL,可以自己根據實際情況設置proxy_cache_lock: 當多個客户端同時訪問一下URL時,如果開啓了這個配置,那麼只會有一個客户端會去上游服務器獲取響應,獲取完成後放入到緩存中,其他的客户端會等待從緩存中獲取。proxy_cache_lock_timeout: 啓用了proxy_cache_lock之後,如果第一個請求超過了proxy_cache_lock_timeout設置的時間默認是5s,那麼所有等待的請求會同時到上游服務器去獲取數據,可能會導致後端壓力增大。proxy_cache_min_uses: 設置資源被請求多少次後才會被緩存proxy_cache_use_stale: 在訪問上游服務器發生錯誤時,返回已經過期的數據給客户端;當緩存內容對於過期時間不敏感,可以選擇採用這種方式proxy_cache_valid: 為不同響應狀態碼設置緩存時間。如果設置proxy_cache_valid 5s,那麼所有的狀態碼都會被緩存。
設置所有的響應被緩存後最大不被訪問的存活時間6小時,緩存的大小設置為1g,緩存的有效期是1天,配置如下:
http {
proxy_cache_path /export/cache/proxy_cache keys_zone=CACHE:10m levels=1:2 inactive=6h max_size=1g;
server {
location / {
proxy_cache CACHE; //指定存放響應到CACHE這個緩存中
proxy_cache_valid 1d; //所有的響應狀態碼都被緩存1d
proxy_pass: http://upstream;
}
}
}
如果當前響應中設置了Set-Cookie頭信息,那麼當前的響應不會被緩存,可以通過使用proxy_ignore_headers來忽略頭信息以達到緩存
proxy_ignore_headers Set-Cookie
如果這樣做了,我們需要把cookie中的值作為proxy_cache_key的一部分,防止同一個URL響應的數據不同導致緩存數據被覆蓋,返回到客户端錯誤的數據
proxy_cache_key "$host$request_uri$cookie_user"
注意,這種情況還是有問題,因為在緩存的key中添加cookie信息,那麼可能導致公共資源被緩存多份導致浪費空間;要解決這個問題我們可以把不同的資源分開配置,比如:
server {
proxy_ignore_headers Set-Cookie;
location /img {
proxy_cache_key "$host$request_uri";
proxy_pass http://upstream;
}
location / {
proxy_cache_key "$host$request_uri$cookie_user";
proxy_pass http://upstream;
}
}
清理緩存
雖然我們設置了緩存加快了響應,但是有時候會遇到緩存錯誤的請求,通常我們需要為自己開一個後面,方便發現問題之後通過手動的方式及時的清理掉緩存。Nginx可以考慮使用ngx_cache_purge模塊進行緩存清理。
location ~ /purge/.* {
allow 127.0.0.1;
deny all;
proxy_cache_purge cache_one $host$1$is_args$args
}
該方法要限制訪問權限; proxy_cache_purge緩存清理的模塊,cache_one指定的key_zone,$host$1$is_args$args 指定的生成緩存key的參數
存儲
如果有大的靜態文件,這些靜態文件基本不會別修改,那麼我們就可以不用給它設置緩存的有效期,讓Nginx直接存儲這些文件直接。如果上游服務器修改了這些文件,那麼可以單獨提供一個程序把對應的靜態文件刪除。
http {
proxy_temp_path /var/www/tmp;
server {
root /var/www/data;
location /img {
error_page 404 = @store
}
location @store {
internal;
proxy_store on;
proxy_store_access group:r all:r;
proxy_pass http://upstream;
}
}
}
請求首先會去/img中查找文件,如果不存在再去上游服務器查找;internal 指令用於指定只允許來自本地 Nginx 的內部調用,來自外部的訪問會直接返回 404 not found 狀態。proxy_store表示需要把從上游服務器返回的文件存儲到 /var/www/data; proxy_store_access設置訪問權限
總結
Cache-Control和Expires設置資源緩存的有效期- 使用
Last-Modified / If-Modified-Since判斷緩存是否有效 - 使用
If-None-Match / ETag判斷緩存是否有效 - 通過配置Nginx緩衝區大小對Nginx調優
- 使用Nginx緩存加快請求響應速度
如何加快請求響應的速度,本篇我們主要圍繞着Http緩存和Nignx反向代理兩個方面來聊了緩存,你以為這樣就完了嗎,不!下一篇我們將從應用程序的維度來聊聊緩存
寫到最後 點關注,不迷路
文中或許會存在或多或少的不足、錯誤之處,有建議或者意見也非常歡迎大家在評論交流。
最後,白嫖不好,創作不易,希望朋友們可以點贊評論關注三連,因為這些就是我分享的全部動力來源🙏
公眾號:貝塔學JAVA原文地址:https://silently9527.cn/archives/94