Web性能优化-ReponseCaching
Posted dinggeonly
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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值。
此时可以使用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的主要内容,如果未能解决你的问题,请参考以下文章