Web效能優化-ReponseCaching
Web效能影響因素有多個方面,對應優化方案也有多個,今天聊的是快取方向。
快取也包括好多種(程式猿太難了),但概括地分就是服務端快取和客戶端快取。
今天聊得是客戶端快取-瀏覽器快取。
為區分兩種快取的差異,簡單多說兩句。
服務端快取最常見、最簡單的就是在咱們寫的後臺業務中加入快取機制(其他方式的就不展開了 但建議自行了解拓展一下)。
例如MemoryCache、Redis,哪怕最基礎Dictionary也可以作為快取。
用個人通俗的話概括其特點是:
- 需要額外新增程式碼邏輯或者依賴第三方元件才能實現(如用Redis做快取);
- 通過跳過或“簡化”讀取資源的過程來縮短響應時間(如從MemoryCache直接取資料而不訪問DB);
- 客戶端請求往往已經到達了最終Api(即通訊過程沒能簡化);
- 請求資源依然需要從服務端傳輸到客戶端(資料傳輸壓力沒有降低)。
回到瀏覽器快取,瀏覽器快取實際上也不止一種,但依然不過多展開,只說基於HTTP協議的快取機制。
沒錯,是快取機制。既然是機制,就和服務端的快取實現有明顯區別了。
服務端一般需要寫一些額外的邏輯,加入額外的依賴,才能實現目的。但基於HTTP的快取,可以認為只是做了一些配置,然後按照規範使用就可以了。
先把Demo程式碼奉上。
Controller部分邏輯
1 //只有這個包是額外引入的 2 using Marvin.Cache.Headers; 3 using Microsoft.AspNetCore.Mvc;4 using System.Collections.Generic; 5 using System.Linq; 6 7 namespace ResponseCaching.Controllers 8 { 9 [ApiController] 10 [Route("[controller]")] 11 public class CacheController : ControllerBase 12 { 13 public static IList<Customer> Customers = new List<Customer> 14{ 15 new Customer{Id = 1, Name = "Demo君",Age=20} 16 }; 17 18 //這裡只是為了提醒一下,可以這樣通過特性進行單獨配置 19 [HttpCacheExpiration(CacheLocation = CacheLocation.Public, MaxAge = 70)] 20 [HttpCacheValidation(MustRevalidate = true, NoCache = false)] 21 public ActionResult Get() 22 { 23 return Ok(Customers); 24 } 25 26 [HttpPut] 27 public ActionResult Update() 28 { 29 Customers.Where(x => x.Id == 1).ToList().ForEach(x => x.Age = 100); 30 return Ok(new { Description = "更新了資料", Data = Customers }); 31 } 32 33 [HttpPost] 34 public ActionResult AddNew() 35 { 36 var customer = new Customer 37 { 38 Id = Customers.Count + 1, 39 Name = "Demo君" + Customers.Count + 1, 40 Age = 20 41 }; 42 Customers.Add(customer); 43 return Ok(new { Description = "新增了資料", Data = Customers }); 44 } 45 46 [HttpDelete] 47 public ActionResult Delete() 48 { 49 Customers.Remove(Customers.Where(x => x.Id == 1).FirstOrDefault()); 50 return Ok(new { Description = "移除了資料", Data = Customers }); 51 } 52 } 53 }
Customer物件
1 namespace ResponseCaching 2 { 3 public class Customer 4 { 5 public int Id { get; set; } 6 public string Name { get; set; } 7 public int Age { get; set; } 8 } 9 }
程式碼比較簡單,簡單看一下就能懂:提供對一個數據源(Customers)進行增刪改查的4個Api,即4個Action。
所以剩下關鍵就是如何利用這個快取機制了。
一、啟用響應快取機制
在Startup類中新增以下程式碼:
ConfigureServices中新增
services.AddResponseCaching(options=>{});
Configure中新增
app.UseResponseCaching();
二、引入Marvin.Cache.Headers
HTTP快取機制,主要依賴於通過HTTP Header在伺服器和客戶端段之間進行快取相關引數、狀態的傳遞。
而這個依賴就是支援從服務端按照協議返回必要的Header,請留意稍後截圖中的Header組成。
在Startup類中新增以下程式碼:
ConfigureServices中新增
services.AddHttpCacheHeaders(expirationModelOptionsAction=>{},validationModelOptionsAction=>{});
Configure中新增
app.UseHttpCacheHeaders();
注意事項:
以上兩個方法的呼叫順序不能顛倒,正確順序是:
services.AddResponseCaching(options=>{}); services.AddHttpCacheHeaders(expirationModelOptionsAction =>{ }, validationModelOptionsAction =>{ }); …… app.UseResponseCaching(); app.UseHttpCacheHeaders();
三、服務端的“配置”
快取機制啟用了,Header支援也添加了,剩下就是配置具體的引數了。
在以上程式碼基礎上,為各個引數進行單獨配置的示例程式碼如下:
1 using Marvin.Cache.Headers; 2 using Microsoft.AspNetCore.Builder; 3 using Microsoft.AspNetCore.Hosting; 4 using Microsoft.Extensions.Configuration; 5 using Microsoft.Extensions.DependencyInjection; 6 using Microsoft.Extensions.Hosting; 7 using System.Linq; 8 9 namespace ResponseCaching 10 { 11 public class Startup 12 { 13 public Startup(IConfiguration configuration) 14 { 15 Configuration = configuration; 16 } 17 18 public IConfiguration Configuration { get; } 19 20 // This method gets called by the runtime. Use this method to add services to the container. 21 public void ConfigureServices(IServiceCollection services) 22 { 23 services.AddControllers(); 24 25 services.AddResponseCaching(configureOptions => 26 { 27 configureOptions.SizeLimit = 50 * 1024 * 1024; //Default:100M 28 configureOptions.MaximumBodySize = 10 * 1024 * 1024;//Default:64M 29 configureOptions.UseCaseSensitivePaths = true; //Default:false 30 }); 31 32 //Marvin.Cache.Headers中介軟體 (只)負責生成Response Header資訊 33 services.AddHttpCacheHeaders(expirationModelOptionsAction => 34 { 35 //Default:60 36 //體現在Hearder中expires和last-modified的時間差 37 expirationModelOptionsAction.MaxAge = 50; 38 //Default:Public 39 expirationModelOptionsAction.CacheLocation = CacheLocation.Public; 40 } 41 , validationModelOptionsAction => 42 { 43 validationModelOptionsAction.MustRevalidate = true; //Default:false 44 45 //Default:[Accept,Accept-Language,Accept-Encoding] 46 var vary = validationModelOptionsAction.Vary.ToList(); 47 vary.AddRange(new string[] { "Id", "Age" }); //留意此細節 48 validationModelOptionsAction.Vary = vary; 49 50 validationModelOptionsAction.VaryByAll = false; //Default:false 51 } 52 ); 53 } 54 55 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 56 { 57 if (env.IsDevelopment()) 58 { 59 app.UseDeveloperExceptionPage(); 60 } 61 62 app.UseResponseCaching(); 63 app.UseHttpCacheHeaders(); 64 65 app.UseHttpsRedirection(); 66 67 app.UseRouting(); 68 69 app.UseAuthorization(); 70 71 app.UseEndpoints(endpoints => 72 { 73 endpoints.MapControllers(); 74 }); 75 } 76 } 77 }
四、客戶端端的“配合”
服務端準備就緒,客戶端也需要配合一下。簡單說就是遵守協議。HTTP協議怎麼規定的,就怎麼執行。
步驟1在Action Get內新增斷點,然後啟動VS除錯。
此時程式會命中該斷點。這說明第一次請求進入到了這個API內部。
F5繼續執行程式,瀏覽器(以Chrome為例演示)執行效果如下圖。
步驟2在瀏覽器通過F5或Ctrl+R重新整理頁面
此時你會發現,程式並沒有再次命中斷點。因為瀏覽器本身支援HTTP快取,當前快取已經生效。
此時,你可能會好奇Ctrl+F5強制重新整理會怎麼樣。答案是:會命中斷點!試一試,然後記住這個結果。
由於瀏覽器默默幫我們做了一些事,所以上面的過程並沒有暴露出快取的原理細節。我們改用Postman嘗試一下。
步驟3用Postman請求Get介面
在Postman輸入Get介面地址,直接訪問。我們得到的響應資訊如下圖所示,請留意Header的組成。
並且,當我們反覆請求該介面時候,會發現斷點每次都會命中。因為我們沒按協議辦事(就是剛才瀏覽器幫我們辦的事),伺服器也不知所措。
步驟4 告訴伺服器“你可以給我快取結果”
我們細看以上圖中的Header,有幾個關鍵的Key。
- cache-control:伺服器端快取的一些引數資訊(參照前面的程式碼找一下對應關係就明白了)
- expires:快取過期時間
- last-modified:請求資源最後變更時間
- etag:看名字就知道了,它是請求資源的“電子標籤”,資源變了,它就會變
之所以快取沒過期但是沒有生效,就是因為客戶端沒有和伺服器端溝通好“如何使用快取”。
我們需要做的就是告知伺服器端“什麼條件下不使用快取”。這個告知方法有兩種:
- 基於過期時間的驗證規則:快取有沒有在客戶端預期的更新時間範圍內(新鮮度);
- 基於資料ETag的驗證規則:快取有沒有與客戶端的資料保持一致。
溝通方式就是我們前面提到的Header傳遞。我們只需在客戶端的請求中新增必要的Header值即可。
基於過期時間的驗證規則
這個規則比較容易理解,就是如果按照客戶端的“標準”,快取過期了,那就不使用快取,概括地說就是“超時規則”。
而這個標準就是:客戶端給出一個時間點,伺服器端的資源快取超時時間點如果在這個時間點之前,那麼快取就是過期的。
其中,客戶端提供的這個時間點,一般使用的是在上次請求時,伺服器端返回Header中的expires值。
此時可以使用KeyIf-Modified-Since,傳遞的Value就是上面所說客戶單要提供的時間點(只能精確秒),驗證邏輯見下圖說明。
可根據上次請求返回的Header的last-modified值填寫不同的時間值進行測試,如命中斷點則說明沒有使用快取。
與If-Modified-Since對應的Key還有If-Unmodified-Since。
基於資料ETag的驗證規則
在實際應用中有這樣一個場景:有一個資源,初次請求後本地快取了10分鐘,第15分鐘時快取已過期,但是伺服器端資源並沒有變化。
按照上面的驗證規則,客戶端就會從服務端重新下載資源到本地,進入新的快取週期。
這就導致了不必要的資料傳輸,產生了不必要的頻寬浪費。這都是我們不希望的結果。而基於ETag的“資料再驗證”則可以避免這個問題。
此時我們可以使用KeyIf-None-Match,傳遞的Value是一個ETag值。當傳遞的ETag值與伺服器的ETag值不一致,說明資源被變更了,此時將獲取最新資料到本地。
如果兩個ETag值一致,說明資源並沒有發生變更,此時伺服器端並不返回資源資料(Response Body將是空資料),狀態碼也將變為304。
驗證邏輯接響應結果見下圖說明。可以通過呼叫Update介面更新資源,然後再通過傳遞不同的ETag去訪問Get介面,來觀察快取是否被使用的規律。
與If-None-Match對應的Key還有If-Match。
到這裡,基於HTTP的瀏覽器快取機制和使用方法就基本梳理完了。
還剩下一塊內容就是在併發場景下的快取更新問題。不過這個決定留在下篇文章再聊。
努力工作 認真生活 持續學習 以勤補拙