AspNetCore响应缓存
一、客户端响应缓存(强缓存)
1. 基本使用
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[ResponseCache(Duration = 3600)]
[HttpGet]
public string Get()
{
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
}
2. 配置文件
- 定义配置
services.Configure<MvcOptions>(mvc =>
{
mvc.CacheProfiles.Add("default1",new CacheProfile
{
Duration=3600
});
mvc.CacheProfiles.Add("default2", new CacheProfile
{
Duration = 7200
});
});
- 引用配置
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
//CacheProfileName 和 Duration 必须配置其中一项,否则报错
[ResponseCache(CacheProfileName = "default1")]
[HttpGet]
public string Get()
{
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
}
- 调试注意
- 不要直接在浏览器地址栏里请求该资源,浏览器会禁用这一行为的缓存,解决办法是通过xhr去请求。
- 不用启用浏览器的禁用缓存
二、服务端响应缓存
简介
由于客户端缓存数据是保存在客户端的,对于相同的资源不同客户端端都要去请求服务器,而服务器又要请求数据库,对于一些不变的用户共享的资源(比如菜单)
我们可以通过启用服务端缓存来减轻数据库压力。通过Microsoft.AspNetCore.ResponseCaching组件可以实现服务端响应缓存。需要注意以下几点问题
-
该组件依赖IMemoryCache即使用节点的内存缓存,不支持分布式(redis)缓存,考虑多节点多实例的情况下相同的数据每个节点要缓存很多副本(但性能很高,不建议存储太大的资源)
分布式缓存建议自行缓存,而不是依赖改组件 -
一旦启用该组件,带有ResponseCache特性的的端点都将被服务端缓存
-
请求头不能带有Authorization标头,请求必须是get或者head。具体参考响应中间件
-
测试服务端缓存请通过postman测试或者禁用浏览器缓存(由于浏览器默认会启用客户端缓存),在指定的端点添加一个断点或者日志,来判断端点是否重复调用
服务注册
//注入服务
services.AddResponseCaching();
//启用中间件:如果还需要配置跨域,则跨域必须写在UseResponseCaching之前,因为UseResponseCaching中间件的缓存被击中就要返回了,就不好经过跨域中间件了
app.UseResponseCaching();
基本使用
- 不指定规则
[HttpGet]
[ResponseCache(Duration =1000)]
public string Get()
{
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
最终存储
key: GETHTTPSLOCALHOST:5001/API/VALUES
value: response.Body
exipresIn:1000
- 根据请求头:对于同一个资源有的希望通过xml响应,有的希望json,我们可以根据accept-type进行存储
[HttpGet]
[ResponseCache(CacheProfileName = "default1",VaryByHeader ="User-Agent")]
public string Get()
{
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
key1: GETHTTPSLOCALHOST:5001/API/VALUES
value: CachedVaryByRules,是一个规则,该规则要求请求头含User-Agent的value
exipresIn:1000
key2: 000000131S96PHUSER-AGENT=PostmanRuntime/7.28.4
value: response.Body
exipresIn:1000
key3: 000000131S96QHUSER-AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36 Edg/96.0.1054.43
value: response.Body
exipresIn:1000
请求该资源时:
1. 通过request可以得到key1(自己想啊)->进而保存一个规则CachedVaryByRules(规则要求请求头User-Agent参与key的生成)->通过规则计算key2为“000000131S96PHUSER-AGENT=PostmanRuntime/7.28.4”->通过key2就能拿到响应体了。
2. 不同浏览器的user-agent不同,进而产生多个键值对
3. 了解了这些我们对于存储,性能指标等就有了把控了
- 根据查询字符串
//显然q的取值范围有很多种,最终会产生非常多的键值对,导致内存不足
[HttpGet]
[ResponseCache(CacheProfileName = "default1", VaryByQueryKeys = new string[] { "q" })]
public string Get(int q)
{
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
//原理同上
- 根据查询字符串和请求头
[HttpGet]
[ResponseCache(CacheProfileName = "default1", VaryByQueryKeys = new string[] { "q" }, VaryByHeader ="User-Agent")]
public string Get(int q)
{
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
//原理同上
三、ETag响应缓存(弱缓存)
显然强缓存,即使服务端数据发生了改变,客户端也无法知晓更改,在客户端不清除缓存的前提下,拿到的就是旧的数据。而Etag机制可以解决这一问题,
具体流程如下:
- 浏览器请求sum.js文件
- 服务端响应sum.js文件的同时响应一个Etag=md5(sum.js)请求头,值为该文件的md5摘要,假设当前为vff787fa4f545
- 浏览器发现服务端响应了Etag会缓存响应(吧sum.js保存起来)
- 浏览器第二次请求sum.js文件,在请求头会携带if-none-match=vff787fa4f545
- 服务器接收到if-none-match的值并再次计算md5(sum.js)如果相等(文件没改)则返回304状态码(只返回状态码),否则直接返回新的文件和Etag
- 浏览器发现了304直接使用本地缓存,减少了带宽特别是对于图片,和一些静态文件,这很有用
可见对于sum.js资源的请求始终保持和服务器实时对话,服务器可以控制浏览器是否使用本地缓存
注意强缓存和若缓存没有依赖关系,可以同时启用,但是如果启用了强缓存,只有等强缓存到期之后才会走弱缓存
示列代码:
具体可以参考我开发的一个项目:ResponseCacheValidation
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddCors(cors =>
{
cors.AddDefaultPolicy(policy =>
{
policy.AllowAnyHeader();
policy.AllowAnyMethod();
policy.AllowAnyOrigin();
});
});
var app = builder.Build();
app.UseCors();
// Configure the HTTP request pipeline.
var cache = new Dictionary<string, object>();
app.Use(async (context, next) =>
{
var path = context.Request.Path.Value;
if (path != null && path.EndsWith("sum.js"))
{
//缓存是否被击中
if (cache.ContainsKey(path))
{
var etag = context.Request.Headers.IfNoneMatch;
dynamic data = cache[path];
//数据是否被修改
if (data.Hash == etag)
{
string file = data.File;
//告诉浏览器数据未修改
context.Response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;
return;//结束
}
}
//否则就返回新的
if (!cache.ContainsKey(path))
{
var file = "function sum(a,b){return a+b;}";
//推荐用md5等计算文件的hash,c#的hashcode应该是不支持分布式环境的
var etag = file.GetHashCode().ToString();
var data = new
{
Hash = etag,
File = file,
};
cache.Add(path, data);
}
dynamic data1 = cache[path];
string file1 = data1.File;
string etag1 = data1.Hash;
//context.Response.Headers.Add(HeaderNames.CacheControl,new StringValues("public,max-age=20"));
context.Response?.Headers.Add(HeaderNames.ETag, new StringValues(etag1));
context.Response?.WriteAsync(file1);
return;//结束
}
await next();
});
app.MapControllers();
app.Run();
四、内存缓存和分布式缓存
基本包
-
抽象包:Microsoft.Extensions.Caching.Abstractions定义了两个核心接口IMemoryCache和IDistributedCache接口,我们只需要面向这两个接口进行编程就好了
-
内存缓存:Microsoft.Extensions.Caching.Memory该包通过内存缓存来实现IMemoryCache接口,通过IMemoryCache来实现IDistributedCache接口。该包基于System.Runtime.Caching包实现源码参考
基本使用
- 注册服务
builder.Services.AddMemoryCache();
//如果启用了分布式内存缓存则不需要启用上面的那个,不过写了也没事
builder.Services.AddDistributedMemoryCache();
- 使用服务
//注意内存缓存和分布式缓存是两个体系
public void Get([FromServices]IMemoryCache memoryCache, [FromServices] IDistributedCache distributedCache)
{
memoryCache.Set("data1",new { Name ="json"});
distributedCache.Set("data2", System.Text.Encoding.UTF8.GetBytes("Redis") ,new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10),
});
}
- 替换IDistributedCache的实现为redis
安装:Microsoft.Extensions.Caching.Redis包
//只需要修改服务注册即可,这就是面向接口编程和面向ioc的特性之一
services.AddDistributedRedisCache(c=>c.Configuration="localhost");