Web性能优化-ReponseCaching

2021-01-16 23:13

阅读:595

标签:mamicode   repo   服务端   信息   ica   中间件   tco   routing   bsp   

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 Customers = new List14         {
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值。

此时可以使用Key If-Modified-Since,传递的Value就是上面所说客户单要提供的时间点(只能精确秒),验证逻辑见下图说明。

可根据上次请求返回的Header的last-modified值填写不同的时间值进行测试,如命中断点则说明没有使用缓存。

与If-Modified-Since对应的Key还有If-Unmodified-Since

技术图片

  

基于数据ETag的验证规则

在实际应用中有这样一个场景:有一个资源,初次请求后本地缓存了10分钟,第15分钟时缓存已过期,但是服务器端资源并没有变化。

按照上面的验证规则,客户端就会从服务端重新下载资源到本地,进入新的缓存周期。

这就导致了不必要的数据传输,产生了不必要的带宽浪费。这都是我们不希望的结果。而基于ETag的“数据再验证”则可以避免这个问题。

此时我们可以使用Key If-None-Match,传递的Value是一个ETag值。当传递的ETag值与服务器的ETag值不一致,说明资源被变更了,此时将获取最新数据到本地。

如果两个ETag值一致,说明资源并没有发生变更,此时服务器端并不返回资源数据(Response Body将是空数据),状态码也将变为304。

验证逻辑接响应结果见下图说明。可以通过调用Update接口更新资源,然后再通过传递不同的ETag去访问Get接口,来观察缓存是否被使用的规律。

与If-None-Match对应的Key还有If-Match

技术图片

 

 

到这里,基于HTTP的浏览器缓存机制和使用方法就基本梳理完了。

还剩下一块内容就是在并发场景下的缓存更新问题。不过这个决定留在下篇文章再聊。

 

努力工作 认真生活 持续学习 以勤补拙

Web性能优化-ReponseCaching

标签:mamicode   repo   服务端   信息   ica   中间件   tco   routing   bsp   

原文地址:https://www.cnblogs.com/dinggeonly/p/13374029.html


评论


亲,登录后才可以留言!