7.4缓存
缓存是一个用来保存数据的区域,从缓存中读取数据要比从数据源读取数据的速度快很多。如果可以从缓存中获取要获取的数据则称之为“缓存命中”,多次请求命中的请求占全部请求的百分比叫做“命中率”,如果数据源中的数据保存到了缓存后,发生了变化则称之为“缓存数据不一致”。
客户端缓存
RFC 7234是HTTP中对缓存进行控制的规范,由cache-control
相应报头来控制,如服务器给浏览器的响应头文件中cache-control
的值为max-age=60
,表示服务器指示浏览器缓存这个响应内容60s。在ASP.Net Core中,只需要给要进行缓存控制的控制器的操作方法添加ResponseCacheAttribute
。
[HttpGet]
[ResponseCache(Duration=60)]//缓存60s
publicDateTimeNow()
{
returnDateTime.Now;
}
默认情况下,[ResponseCache]是通过cache-control响应报文头来控制浏览器,如果浏览器不支持缓存这个设置不会生效。
服务器响应缓存*
安装了“响应缓存中间件”,ASP.NET Core不仅会根据[ResponseCache]来设置响应报文,还会在服务器对响应进行服务端缓存。当在特定的时间内访问服务器相同的请求地址,服务器不会执行操作方法,而是将缓存的数据直接返回给客户端。
使用方法:
- 在操作方法上面使用[ResponseCache]
- 在Program.cs文件中app.MapControllers之前加上app.UseResponseCaching
注意:如果开启了CORS跨域请求,确保app.UseCors在app.UseResponseCaching之前
像chorme浏览器,如果浏览器中设置禁用浏览器缓存,但是启用了服务器的响应缓存中间件,但服务器的缓存仍然会不起作用。这是因为禁用浏览器缓存后,浏览器会向服务器端的请求头文件中加入cache-control:no-cache
,这样服务器也会禁用缓存机制。
建议:不启用“响应缓存中间件”,如果需要在客户端进行缓存,则只需要使用[ResponseCache]即可
内存缓存
内存缓存中保存的是一系列的键值对,不同的缓存内容具有不同的缓存键,每个缓存键对应一个缓存值。内存缓存保存在当前运行网站程序的内存中,和进程相关。使用内存缓存,确保在Program.cs中的builder.Build之前添加builder.Services.AddMemoryCache
来讲内存缓存的相关服务注册到容器中。使用内存缓存的时候,主要使用IMemoryCache
接口
方法 | 说明 |
bool TryGetValue(object key, out object value) |
尝试获取键为key的缓存值,如果有,则返回true否则返回false |
void Remove(object key) |
删除键为key的缓存值 |
T Set<T>(object key,T value) |
设置缓存键key的缓存值为value |
T GetOrCreate<T>(object key,Func<ICacheEntry,T> factory) |
获取键为key的缓存值,如果缓存中没有,则调用factory指向的函数从数据源获取数据,创建缓存并返回值 |
Task<T> GetOrCreateAsync<T>(object key,Func<ICacheEntry,Task<T>> factory) |
异步的GetOrCreate方法,ICacheEntry可以对缓存项进行详细的设置,比如缓存项被清除的回调、缓存项的优先级 |
[Route("[controller]/[action]")]
[ApiController]
publicclassTest1Controller : ControllerBase
{
privatereadonlyILogger<Test1Controller>logger;
privatereadonlyMyDbContextdbCtx;
privatereadonlyIMemoryCachememCache;//使用依赖注入的形式使用IMemoryCache
publicTest1Controller(MyDbContextdbCtx, IMemoryCachememCache, ILogger<Test1Controller>logger)
{
this.dbCtx=dbCtx;
this.memCache=memCache;
this.logger=logger;
}
[HttpGet]
publicasyncTask<Book[]>GetBooks()
{
logger.LogInformation("开始执行GetBooks");
varitems=awaitmemCache.GetOrCreateAsync("AllBooks", async (e) =>
{
logger.LogInformation("从数据库中读取数据");
returnawaitdbCtx.Books.ToArrayAsync();
});
logger.LogInformation("把数据返回给调用者");
returnitems;
}
}
过期策略
- 绝对过期时间
自设置缓存之后的指定时间后,缓存被删除
- 滑动过期时间
自设置缓存之后的指定时间后,如果对缓存数据没有访问,则删除,如果有访问,则缓存项会以最后一次的时间为准自动续期
//绝对过期时间
logger.LogInformation("开始执行Demo1:"+DateTime.Now);
varitems=awaitmemCache.GetOrCreateAsync("AllBooks", async (e) => {
e.AbsoluteExpirationRelativeToNow=TimeSpan.FromSeconds(10);//设置绝对过期时间
logger.LogInformation("从数据库中读取数据");
returnawaitdbCtx.Books.ToArrayAsync();
});
logger.LogInformation("Demo1执行结束");
//滑动过期时间
logger.LogInformation("开始执行Demo2:"+DateTime.Now);
varitems=awaitmemCache.GetOrCreateAsync("AllBooks2", async (e) => {
e.SlidingExpiration=TimeSpan.FromSeconds(10);//活动过期时间
logger.LogInformation("Demo2从数据库中读取数据");
returnawaitdbCtx.Books.ToArrayAsync();
});
logger.LogInformation("Demo2执行结束");
- 混合使用过期策略
一般设置绝对过期时间比滑动时间长,绝对时间到期后,无论滑动时间有没有到期,都会删除缓存
//混合使用过期时间策略
logger.LogInformation("开始执行Demo3:"+DateTime.Now);
varitems=awaitmemCache.GetOrCreateAsync("AllBooks3", async (e) => {
e.SlidingExpiration=TimeSpan.FromSeconds(10);//滑动时间
e.AbsoluteExpirationRelativeToNow=TimeSpan.FromSeconds(30);//绝对时间
logger.LogInformation("Demo3从数据库中读取数据");
returnawaitdbCtx.Books.ToArrayAsync();
});
logger.LogInformation("Demo3执行结束");
混合使用过期策略可以实现不经常被访问的数据不会长时间占内存,而频繁被访问的数据会避免数据不一致的问题
所有的缓存都会出现数据不一致的情况,对应不允许数据不一致的情况,可以直接不用缓存。
缓存穿透
使用IMemoryCache中的Get方法,会根据key来查找缓存项,如果找不到则返回Null
stringcacheKey="Book"+id;//缓存键
Book?b=memCache.Get<Book?>(cacheKey);
if (b==null)//如果缓存中没有数据 如果有恶意者使用不存在的某个Id大量访问,则会不断的查询数据库,导致服务器崩溃,这叫做缓存穿透
{
//查询数据库,然后写入缓存
b=awaitdbCtx.Books.FindAsync(id);
memCache.Set(cacheKey, b);
}
缓存穿透的问题是由于将查不到的数据用Null来表示,如果把“查不到”也当数据放到缓存中,即{key:不存在的某个Id,value:null}。日常开发中使用GetOrCreateAsync方法就可以避免缓存穿透,它将null也作为了合法的缓存值。
logger.LogInformation("开始执行Demo5");
stringcacheKey="Book"+id;
varbook=awaitmemCache.GetOrCreateAsync(cacheKey, async (e) => {
varb=awaitdbCtx.Books.FindAsync(id);
logger.LogInformation("数据库查询:{0}", b==null?"为空" : "不为空");
returnb;
});
logger.LogInformation("Demo5执行结束:{0}", book==null?"为空" : "不为空");
returnbook;
缓存雪崩
一般会是在网站启动的时候将大量的数据放到缓存以提高响应速度,如果这些缓存设定了相同的过期时间,则会同时过期,如果有访问则会导致大量的数据库访问,这样会同时的访问会将数据库服务器压垮。解决这个问题的办法是在基础过期的时间上增加一个随机的过期时间,这样就不会集中到同一时间了。
注意:设置缓存的时候,IQueryable、IEnumerable等类型可能存在延迟加载,当取出这种类型的变量时去执行时,如果他们所需要的对象已经被释放则会执行失败。因此最好将这两种对象转换为数组或者List类型再放到缓存中。
分布式缓存
在分布式系统中,将缓存数据放到专门的缓存服务器中,所有的Web都通过缓存服务器来进行写入和读取。
.net core中使用IDistributedCache
接口来进行操作,分布式缓存中提供了DistributedCacheEntryOptions
来配置过期时间。不同类型的缓存服务器支持的缓存键和缓存值不相同,所以IDistributedCache
统一将string作为key的类型,将byte[]作为值类型。
推荐使用Redis数据库作为缓存服务器,微软也提供了Redis作为缓存服务器的Nuget包Microsoft.Extensions.Caching.StackExchangeRedis。
- 在Program.cs的builder.Build之前添加代码进行注册Redis缓存
builder.Services.AddStackExchangeRedisCache(options=>
{
options.Configuration="localhost";//redis服务器的连接配置
options.InstanceName="yzk_";//其他程序也许也在使用redis服务器,为避免冲突,增加yzk_前缀
});
- 读写redis中的缓存数据
publicclassTest1Controller : ControllerBase
{
privatereadonlyIDistributedCachedistCache;
publicTest1Controller(IDistributedCachedistCache)
{
this.distCache=distCache;
}
[HttpGet]
publicstringNow()
{
strings=distCache.GetString("Now");
if (s==null)
{
s=DateTime.Now.ToString();
varopt=newDistributedCacheEntryOptions();
opt.AbsoluteExpirationRelativeToNow=TimeSpan.FromSeconds(30);
distCache.SetString("Now", s, opt);//将s存放在yzh_Now键对应的值中
}
returns;
}
}