1. 程式人生 > 實用技巧 >Web效能優化-ReponseCaching

Web效能優化-ReponseCaching

Web效能影響因素有多個方面,對應優化方案也有多個,今天聊的是快取方向。

快取也包括好多種(程式猿太難了),但概括地分就是服務端快取和客戶端快取。

今天聊得是客戶端快取-瀏覽器快取

為區分兩種快取的差異,簡單多說兩句。

服務端快取最常見、最簡單的就是在咱們寫的後臺業務中加入快取機制(其他方式的就不展開了 但建議自行了解拓展一下)。

例如MemoryCache、Redis,哪怕最基礎Dictionary也可以作為快取。

用個人通俗的話概括其特點是:

  1. 需要額外新增程式碼邏輯或者依賴第三方元件才能實現(如用Redis做快取);
  2. 通過跳過或“簡化”讀取資源的過程來縮短響應時間(如從MemoryCache直接取資料而不訪問DB);
  3. 客戶端請求往往已經到達了最終Api(即通訊過程沒能簡化);
  4. 請求資源依然需要從服務端傳輸到客戶端(資料傳輸壓力沒有降低)。

回到瀏覽器快取,瀏覽器快取實際上也不止一種,但依然不過多展開,只說基於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的瀏覽器快取機制和使用方法就基本梳理完了。

還剩下一塊內容就是在併發場景下的快取更新問題。不過這個決定留在下篇文章再聊。

努力工作 認真生活 持續學習 以勤補拙