zoukankan      html  css  js  c++  java
  • 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值。

    此时可以使用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的浏览器缓存机制和使用方法就基本梳理完了。

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

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

  • 相关阅读:
    Navicat 远程连接ubuntu出现的问题
    替换 ubuntu 自带的python版本
    xpath疑惑
    xpath中返回值问题
    AttributeError: 'unicode' object has no attribute 'xpath'
    linux下mysql忘记密码解决方案
    IntelliJ idea常用快捷键
    最近的说明(本篇不谈具体技术,看技术的可以忽略)
    常用的排序算法介绍和在JAVA的实现(二)
    mysql数据库查询过程探究和优化建议
  • 原文地址:https://www.cnblogs.com/dinggeonly/p/13374029.html
Copyright © 2011-2022 走看看