NET-Core利用etag进行浏览器缓存
title: .NET Core浏览器缓存方案
date: 2022-12-02 14:17:36
tags:
- .NET
缓存介绍及方案
在后端开发中经常会使用缓存来提高接口的响应速度,降低系统的计算成本,比如将一些不经常改变的数据通过Redis或者直接在应用内缓存下来,后续一段时间内用户访问该接口时直接返回数据,而不是重新计算
实际上,数据不仅可以缓存在后端服务器,还可以通过浏览器进行缓存,下面介绍一下Http请求时Header内跟缓存相关的属性:
Cache-Control
:包含了大部分用于缓存控制的属性,比如max-age
,public
,private
,no-cache
,no-store
,下面细说一下这些值的作用:public
: 所有人都可以缓存,比如客户端和代理服务器(没有强制作用,实际上中间服务器想缓存也没办法控制,只是一个告知作用)private
:所有内容只有客户端可以缓存no-cache
:不进行强制缓存,但是是否使用缓存需要经过协商缓存来验证决定max-age
:缓存的最大时间,单位是秒no-store
:不进行缓存,每次都是从服务器获取最新的数据,和no-cache
的区别在于no-cache
是可以缓存的,只是每次都要经过协商缓存来验证是否使用缓存
Expired
:过期时间,如果在过期时间内,浏览器不会向服务器发送请求,直接使用缓存,Http1.0的属性,Http1.1已经不再使用,可以忽略Etag
+If-None-Match
:用于协商缓存,第一次服务器计算返回内容的MD5作为etag返回,第二次客户端请求时,将Etag放到If-None-Match
字段请求服务器,服务器判断etag是否发生变化,用于协商缓存Last-Modified
+If-Modified-Since
:服务器返回的最后修改时间,用于协商缓存,客户端第二次请求时将Last-Modified
放到If-Modified-Since
中,服务器根据这两个值来判断是否使用缓存
知道了缓存需要的几个属性之后,我们还需要知道浏览器缓存的2种方式:
- 强缓存,请求不会到后端,而是直接从缓存中获取,如果缓存过期了,才会向后端发送请求
- 协商缓存,请求会到后端,后端会根据请求头中的
Etag
或者Last-Modified
来判断是否使用缓存,如果使用缓存,返回304,不使用缓存,返回200,Etag优先级高于Last-Modified(有Etag先用Etag,没有Etag再用Last-Modified)
强缓存是通过Cache-Control
中的max-age字段控制的,而即使是在缓存时间内,也不一定就会是强缓存,还跟用户的行为有关,也和请求的方式有关,根据我的一些操作实验,我总结了一下:
- 浏览器填入接口地址后,多次F5刷新,不会走强缓存,还是会发起http请求
- 在访问过一次接口地址后,在新的标签页打开接口地址,会走强缓存,不会发起http请求
- vue开发的网页内使用axios发起请求获取数据,会走强缓存,不会发起http请求,同时F5刷新页面也不会发起http请求
- 控制台勾选了停止缓存,所有操作都不会走强缓存,都会发起http请求
- 前进后退走强缓存,不会发起http请求
协商缓存通过后端控制,下面以AspNetCore.CacheOutput
库中的缓存方案为例,来看看如何使用Etag+Max-age实现客户端缓存:
在方法执行前执行如下代码:
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
Console.WriteLine("进入了缓存AOP");
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (context.Result != null)
{
return;
}
if (!IsCachingAllowed(context, AnonymousOnly))
{
await next();
return;
}
SwapResponseBodyToMemoryStream(context);
IServiceProvider serviceProvider = context.HttpContext.RequestServices;
IApiCacheOutput cache = serviceProvider.GetRequiredService(typeof(IApiCacheOutput)) as IApiCacheOutput;
CacheKeyGeneratorFactory cacheKeyGeneratorFactory = serviceProvider.GetRequiredService(typeof(CacheKeyGeneratorFactory)) as CacheKeyGeneratorFactory;
ICacheKeyGenerator cacheKeyGenerator = cacheKeyGeneratorFactory.GetCacheKeyGenerator(this.CacheKeyGenerator);
EnsureCacheTimeQuery();
string expectedMediaType = GetExpectedMediaType(context);
string cacheKey = cacheKeyGenerator.MakeCacheKey(context, expectedMediaType, ExcludeQueryStringFromCacheKey);
context.HttpContext.Items[CurrentRequestCacheKey] = cacheKey;
if (!await cache.ContainsAsync(cacheKey))
{
ActionExecutedContext result = await next();
if (result.Exception != null)
{
await SwapMemoryStreamToResponseBody(context);
}
return;
}
context.HttpContext.Items[CurrentRequestSkipResultExecution] = true;
if (context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch].Any())
{
string etag = await cache.GetAsync<string>(cacheKey + Constants.EtagKey);
if (etag != null)
{
if (context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch].Any(e => e == etag))
{
SetEtag(context.HttpContext.Response, etag);
CacheTime time = CacheTimeQuery.Execute(DateTime.Now);
ApplyCacheHeaders(context.HttpContext.Response, time);
context.HttpContext.Response.ContentLength = 0;
context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified;
return;
}
}
}
byte[] val = await cache.GetAsync<byte[]>(cacheKey);
if (val == null)
{
ActionExecutedContext result = await next();
if (result.Exception != null)
{
await SwapMemoryStreamToResponseBody(context);
}
return;
}
await context.HttpContext.Response.Body.WriteAsync(val, 0, val.Length);
string contentType = await cache.GetAsync<string>(cacheKey + Constants.ContentTypeKey) ?? expectedMediaType;
context.HttpContext.Response.Headers[HeaderNames.ContentType] = contentType;
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
string responseEtag = await cache.GetAsync<string>(cacheKey + Constants.EtagKey);
if (responseEtag != null)
{
SetEtag(context.HttpContext.Response, responseEtag);
}
CacheTime cacheTime = CacheTimeQuery.Execute(DateTime.Now);
if (
DateTimeOffset.TryParse(
await cache.GetAsync<string>(cacheKey + Constants.LastModifiedKey),
out DateTimeOffset lastModified
)
)
{
ApplyCacheHeaders(context.HttpContext.Response, cacheTime, lastModified);
}
else
{
ApplyCacheHeaders(context.HttpContext.Response, cacheTime);
}
}
上面这段代码的核心逻辑如下:
- 获取http请求头的if-none-match字段,如果有值,则说明客户端缓存了该资源,需要和服务端的Etag进行比较,如果一致,则返回304,否则继续往下
- 第一步条件不满足的话说明Etag不一致,查找服务端本地的缓存,如果存在,则返回新的数据和Etag,否则执行方法逻辑,获取执行后的结果计算Etag并返回
在方法执行完成后,执行如下代码:
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
await base.OnResultExecutionAsync(context, next);
if (
context.HttpContext.RequestAborted.IsCancellationRequested ||
context.HttpContext.Response == null ||
context.HttpContext.Items[CurrentRequestSkipResultExecution] != null ||
!(
context.HttpContext.Response.StatusCode >= (int)HttpStatusCode.OK &&
context.HttpContext.Response.StatusCode < (int)HttpStatusCode.Ambiguous
)
)
{
await SwapMemoryStreamToResponseBody(context);
return;
}
if (!IsCachingAllowed(context, AnonymousOnly))
{
await SwapMemoryStreamToResponseBody(context);
return;
}
DateTimeOffset actionExecutionTimestamp = DateTimeOffset.Now;
CacheTime cacheTime = CacheTimeQuery.Execute(actionExecutionTimestamp.DateTime);
if (cacheTime.AbsoluteExpiration > actionExecutionTimestamp)
{
IServiceProvider serviceProvider = context.HttpContext.RequestServices;
IApiCacheOutput cache = serviceProvider.GetRequiredService(typeof(IApiCacheOutput)) as IApiCacheOutput;
CacheKeyGeneratorFactory cacheKeyGeneratorFactory = serviceProvider.GetRequiredService(typeof(CacheKeyGeneratorFactory)) as CacheKeyGeneratorFactory;
ICacheKeyGenerator cacheKeyGenerator = cacheKeyGeneratorFactory.GetCacheKeyGenerator(this.CacheKeyGenerator);
string cacheKey = context.HttpContext.Items[CurrentRequestCacheKey] as string;
if (!string.IsNullOrWhiteSpace(cacheKey))
{
if (!await cache.ContainsAsync(cacheKey))
{
SetEtag(context.HttpContext.Response, CreateEtag());
context.HttpContext.Response.Headers.Remove(HeaderNames.ContentLength);
var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
string controller = actionDescriptor?.ControllerTypeInfo.FullName;
string action = actionDescriptor?.ActionName;
string baseKey = cacheKeyGenerator.MakeBaseCacheKey(controller, action);
string contentType = context.HttpContext.Response.ContentType;
string etag = context.HttpContext.Response.Headers[HeaderNames.ETag];
var memoryStream = context.HttpContext.Response.Body as MemoryStream;
if (memoryStream != null)
{
byte[] content = memoryStream.ToArray();
await cache.AddAsync(baseKey, string.Empty, cacheTime.AbsoluteExpiration);
await cache.AddAsync(cacheKey, content, cacheTime.AbsoluteExpiration, baseKey);
await cache.AddAsync(
cacheKey + Constants.ContentTypeKey,
contentType,
cacheTime.AbsoluteExpiration,
baseKey
);
await cache.AddAsync(
cacheKey + Constants.EtagKey,
etag,
cacheTime.AbsoluteExpiration,
baseKey
);
await cache.AddAsync(
cacheKey + Constants.LastModifiedKey,
actionExecutionTimestamp.ToString(),
cacheTime.AbsoluteExpiration,
baseKey
);
}
}
}
}
ApplyCacheHeaders(context.HttpContext.Response, cacheTime, actionExecutionTimestamp);
await SwapMemoryStreamToResponseBody(context);
}
这段代码的核心逻辑就是如果方法执行前缓存命中了,则返回,如果缓存未命中则在执行方法逻辑之后,然后将结果缓存到内存中,将响应的缓存头和数据放到Response中,把结果返回给客户端
精细化控制缓存策略
如果需要让浏览器每次都来服务器校验缓存是否过期(也就是走协商缓存),可以使用no-cache
(参考AspNetCore.CacheOutput库文档),这样就不会在响应头中加上Cache-Control和Expires字段了,这样浏览器每次都会向服务器发起请求,然后服务器会根据Etag来判断是否返回304,可以控制资源随时更新
我们以AspNetCore.CacheOutput库为例,下面写几个示例代码:
// 仅使用协商缓存 Cache-Control: no-cache 保证每次都会向服务器发起请求,然后服务器会根据Etag来判断是否返回304
[CacheOutput(ServerTimeSpan = 500,NoCache = true)]
// Cache-Control为空,一般情况下和上面一致,不过不保证浏览器会走强缓存
[CacheOutput(ServerTimeSpan = 500)]
// 同时使用强缓存和协商缓存 根据不同的情况走不同的策略
[CacheOutput(ClientTimeSpan = 500,ServerTimeSpan = 500)]
// 仅仅使用强缓存,到期或者根据用户行为,可能会发起http请求,服务器由于没有缓存,所以会重新执行方法
[CacheOutput(ClientTimeSpan = 500)]
总结
浏览器缓存最大的好处就是在进行强缓存时,不会发送http请求到后端,这样后端服务器的压力就更小了,可以认为成本几乎是0,但有一个缺点就是无法100%保证浏览器能够走强缓存,因为浏览器的缓存策略是由浏览器自己决定的,我们只能尽量的让浏览器走强缓存,而不是每次都去请求后端服务器
如果没有走强缓存而是走协商缓存的话,每次都会发送http请求到后端,后端服务器想缓存的话还需要使用本地内存缓存或者Redis,否则就得重新计算,不过相较于直接使用Redis或者内存缓存还是有好处的,就是直接返回304+ETag,不需要带响应体,如果数据量大且并发量高的话,对带宽又是一个非常大的节约,同时还可以控制数据的过期时间,这样就可以保证在数据更新后浏览器能请求到最新的数据
补充测试代码
记录请求体长度的中间件:
public class ContentLengthMiddleware
{
RequestDelegate _next;
public ContentLengthMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
using (var buffer = new MemoryStream())
{
var request = context.Request;
var response = context.Response;
var bodyStream = response.Body;
response.Body = buffer;
await _next(context);
Console.WriteLine($"{request.Path} ({response.ContentType}) Content-Length: {response.ContentLength ?? buffer.Length}");
buffer.Position = 0;
await buffer.CopyToAsync(bodyStream);
}
}
}
测试接口:
[HttpGet]
[CacheOutput(ClientTimeSpan = 500)]
public IEnumerable<WeatherForecast> Get()
{
Thread.Sleep(3000);
var rng = new Random();
return Enumerable.Range(1, 100).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
参考链接
- https://blog.csdn.net/qq_34556414/article/details/106337292
- https://blog.csdn.net/u012193330/article/details/99668549
- https://www.ltonus.com/Fenxiang/web-cache-rule.html
- https://stackoverflow.com/questions/11245767/is-chrome-ignoring-cache-control-max-age
NET-Core利用etag进行浏览器缓存的更多相关文章
- Nginx使用Expires增加浏览器缓存加速(转)
转载自:Nginx使用Expires增加浏览器缓存加速 Nginx可以更改HTTP头部,这个是Web服务器必须的,当然Nginx更可以支持在HTTP头部中添加Expires等相关信息,增强浏览器缓存, ...
- 浏览器缓存相关的Http头介绍:Expires,Cache-Control,Last-Modified,ETag
转自:http://www.path8.net/tn/archives/2745 缓存对于web开发有重要作用,尤其是大负荷web系统开发中. 缓存分很多种:服务器缓存,第三方缓存,浏览器缓存等.其中 ...
- CommonsChunkPlugin并不是分离第三方库的好办法(DllPlugin科学利用浏览器缓存)
webpack算是个磨人的小妖精了.之前一直站在glup阵营,使用browserify打包,发现webpack已经火到爆炸,深怕被社区遗落,赶紧拿起来把玩一下.本来只想玩一下的.尝试打包了以后,就想启 ...
- webpack分离第三方库(CommonsChunkPlugin并不是分离第三方库的好办法DllPlugin科学利用浏览器缓存)
webpack算是个磨人的小妖精了.之前一直站在glup阵营,使用browserify打包,发现webpack已经火到爆炸,深怕被社区遗落,赶紧拿起来把玩一下.本来只想玩一下的.尝试打包了以后,就想启 ...
- 浏览器缓存详解:expires,cache-control,last-modified,etag详细说明
最近在对CDN进行优化,对浏览器缓存深入研究了一下,记录一下,方便后来者 画了一个草图: 每个状态的详细说明如下: 1.Last-Modified 在浏览器第一次请求某一个URL时,服务器端的返回状态 ...
- 浏览器缓存之Expires Etag Last-Modified max-age详解
前段时间去面试移动端的H5开发工程师,在最后面试的时候被问到了max-age Expires Etag有什么不同,在什么情况下应用,当时乱编了一通,自我感觉良好,结果…… 大家懂得,现在讲他们几个的区 ...
- 利用gulp解决微信浏览器缓存问题
做了好多项目,这次终于要解决微信浏览器缓存这个令人头疼的问题了.每次上传新的文件,在微信浏览器中访问时,总要先清除微信的缓存,实在麻烦,在网上搜罗了很多解决办法,终于找到了方法:利用gulp解决缓存问 ...
- 利用nginx设置浏览器协商缓存
强缓存与协商缓存的区别 强缓存:浏览器不与服务端协商直接取浏览器缓存 协商缓存:浏览器会先向服务器确认资源的有效性后才决定是从缓存中取资源还是重新获取资源 协商缓存运作原理 现在有一个这样的业务情景: ...
- ASP.NET Boilerplate 学习 AspNet Core2 浏览器缓存使用 c#基础,单线程,跨线程访问和线程带参数 wpf 禁用启用webbroswer右键菜单 EF Core 2.0使用MsSql/MySql实现DB First和Code First ASP.NET Core部署到Windows IIS QRCode.js:使用 JavaScript 生成
ASP.NET Boilerplate 学习 1.在http://www.aspnetboilerplate.com/Templates 网站下载ABP模版 2.解压后打开解决方案,解决方案目录: ...
- [转]浏览器缓存详解: expires, cache-control, last-modified, etag详细说明
最近在对CDN进行优化,对浏览器缓存深入研究了一下,记录一下,方便后来者 画了一个草图: 每个状态的详细说明如下: 1.Last-Modified 在浏览器第一次请求某一个URL时,服务器端的返回状态 ...
随机推荐
- LeetCode------斐波那契数列(2)
来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/fei-bo-na-qi-shu-lie-lcof 写一个函数,输入 n ,求斐波那契(Fibo ...
- SystemParametersInfo函数说明
SystemParametersinfo 返回值:如果函数调用成功,返回值非零:如果函数调用失败,那么返回值为零.若想获取更多错误信息,请调用GetLastError函数. SystemParamet ...
- 快速创建软件安装包-ClickOnce
大家好,我是沙漠尽头的狼. .NET是免费,跨平台,开源,用于构建所有应用的开发人员平台. 今天介绍使用ClickOnce制作软件安装包,首先我们先了解什么是ClickOne. 1. 什么是Click ...
- c++ 关于引用变量你不知道的东西
引用变量延迟绑定 我们知道引用变量定义时要立刻赋值,告诉编译器他是谁的引用.如果不赋值,编译会失败. 如果引用变量是单个定义的,对他赋值还比较简单. struct test_T { int data; ...
- mycat搭建
搭建mycat 一.准备工作 1.确保jdk已安装成功,并且jdk版本选用1.7以上版本 2.准备一台新的主机mysql_mycat放到master的前面做代理 mycat ip 192.168.23 ...
- SpringBoot 03: 常用web组件 - - - 拦截器 + Servlet + 过滤器
常用web组件 拦截器 Servlet 过滤器 使用思想 创建自定义类 实现或者继承框架里的接口或类 将自定义类注册到框架中 使用自定义类 拦截器 说明 拦截器是SpringMVC中的一种对象,能拦截 ...
- 嵌入式-c语言基础:冒泡排序实现从大到小排列
#include<stdio.h> int main() { /*冒泡排序:从大到小*/ /*i=0 第1轮(i+1):需要比较9次(sizeArr-i-1)*/ /*i=1 第2轮(i+ ...
- nacos集群搭建和反向代理
搭建环境 安装ngin https://www.linuxprobe.com/linux-install-nginx.html 配置jdk1.8 https://blog.csdn.net/qq_42 ...
- 2022-11-09 Acwing每日一题
本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的.同时也希望 ...
- 高精度加法(Java)
题目描述 高精度加法,相当于 a+b problem,不用考虑负数. 输入格式 分两行输入. a , b ≤ 10^500 输出格式 输出只有一行,代表 a + b 的值. 思路 使用数组进行模拟, ...