《ASP.NET Core技术内幕与项目实战》精简集-高级组件4.1:缓存Cache
本节内容,涉及到7.4(P198-P221),以WebApi说明为主。主要NuGet包:
- Microsoft.Extensions.Caching.StackExchangeRedis(Redis缓存)
一、图解缓存机制
1、如图所示,首次请求数据时,先从缓存中获取,如果没有,则继续向数据库中获取。获取到数据后,将数据保存到缓存中。再次请求数据,一样先从缓存中获取,成功获取,“缓存命中”。多次请求中,命中次数占全部请求次数的比例,叫“命中率”。如果数据源的数据发生变化,而缓存中的数据没有更新,就会导致“缓存数据不一致”。
2、Web请求过程:客户端浏览器>网关服务器>Web服务器>数据库服务器,这些节点都可以设置缓存,缓存数据以键值对的方式储存在节点的内存中。
3、从图中可以看出,如果命中缓存,可以节省查询读取数据库的过程,可以明显提升请求效率。
4、常用的缓存包括:客户端响应缓存、服务端响应缓存、内存缓存、分布式缓存
二、客户端响应缓存和服务端响应缓存
1、客户端响应缓存
//GetNow1未设置客户端缓存 //GetNow2通过特性标注设置了客户端缓存 //特性[ResponseCache(Duration = 60)],告诉浏览器可以缓存数据60秒 //实质上是设置响应报文头的chche-control值为max-age=60,遵守HTTP的RFC7234规范 [ApiController] [Route("api/[controller]/[action]")] public class MyController: ControllerBase { [HttpGet] public DateTime GetNow1() { return DateTime.Now; } [HttpGet] [ResponseCache(Duration = 60)] public DateTime GetNow2() { return DateTime.Now; } }
2、服务端响应缓存
//配置中间件UseResponseCaching,其它与客户端响应缓存一致 //注意中间件位置,在UseCors之后,在MapControllers之前 var app = builder.Build(); ...... app.UseCors(); app.UseResponseCaching(); app.MapControllers(); app.Run();
3、客户端和服务端响应缓存的注意事项:
1)如果客户端禁用浏览器缓存,"cache-control:no-chche",不仅客户端响应缓存失效,服务端响应缓存也会失效。
2)服务端响应缓存有一些限制,如响应码为200、GET/HEAD响应、不能有Authorization、不能有Set-Cookie。
3)建议只使用客户端响应缓存,简单设置ResponseCacheAttribute即可
三、内存缓存
1、在(Program.cs)容器中注册内存缓存服务,builder.Services.AddMemoryCache
2、在控制器中注入IMemoryCache服务
1 [ApiController] 2 [Route("api/[controller]/[action]")] 3 public class MyController: ControllerBase 4 { 5 private readonly MyDbContext ctx; 6 private readonly ILogger<MyController> logger; 7 private readonly IMemoryCache memoryCache; 8 9 public MyController(MyDbContext ctx, ILogger<MyController> logger, IMemoryCache memoryCache) 10 { 11 this.ctx = ctx; 12 this.logger = logger; 13 this.memoryCache = memoryCache; 14 } 15 16 [HttpGet] 17 public async Task<Book[]> GetBooks() 18 { 19 logger.LogInformation("开始执行GetBooks"); 20 var books = await memoryCache.GetOrCreateAsync("AllBooks", async (e) => 21 { 22 //绝对过期时间 23 e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30); 24 //滑动过期时间 25 e.SlidingExpiration = TimeSpan.FromSeconds(10); 26 27 logger.LogInformation("从数据库中读取数据"); 28 29 return await ctx.Books.ToArrayAsync(); 30 }); 31 logger.LogInformation("把数据返回给调用者"); 32 return books; 33 } 34 }
代码解读:
7行,9-14行:依赖注入IMemoryCache内存缓存服务
20-30行:调用方法GetOrCreateAsync,尝试获取缓存键“AllBooks”的值,如果有,则返回缓存值。如果没有,则从数据库中读取数据,并将数据缓存到内存中(键为“AllBooks”),同时,设置缓存的绝对过期时间为30秒,滑动过程期间为10秒
补充说明:
①内存缓存,保存在Web服务器的内存中,且在当前运行程序的内存中,和进程相关。所以,如果Web服务器上,运行多个AspNetCore程序,程序之间的缓存相互隔离
②通过设置缓存键值的过期时间,可以有效防止“缓存数据不一致”。绝对过期,则超过设定时间后,缓存过期,需要重新从数据库获取数据;相对过期,指在设定的时间内,如果没有访问,则缓存过期,如有访问,则设定时间重置。一般绝对和相对过期结合使用,且设置绝对时间大于滑动过期时间。
③IMemoryCache,除了GetOrCreateAsync方法之外,还有TryGetValue、Remove、Get、Set、GetOrCreate等方法,一般使用GetOrCreateAsync,此方法可以有效防止缓存穿透。
④缓存常见问题一:缓存数据不一致。通过设置过期时间来缓解,如果要彻底解决,可以在数据变更时,调用Set方法,重新设置缓存值
⑤缓存常见问题二:缓存穿透。这是由使用Get和Set方法引起的,Get/Set的使用逻辑是,如果Get不到缓存,则读取数据库,并Set缓存键值。如果客户端大量请求数据库中没有的数据时,Get会一直返回null,而Set并不会将null作为键值保存,所以这种请求每次都会查询数据库,给数据库带来很大压力。GetOrCreateAsync,会将null也作为键值缓存,所以不会读取数据库。
⑥缓存常见问题三:缓存雪崩。如双十一,会提前做数据“预热”,即提前把数据读取出来并加入缓存。如果这些缓存的过期时间一致,会同时过期,造成数据库服务器请求大量集中,可以通过将过期时间设置为随机时间来避免。
四、分布式缓存
1、如果采用分布式部署,Web应用部署在多台Web服务器中,则可以选择分布式缓存。将缓存数据都保存到专门的缓存服务器中,所有Web应用都通过缓存服务器进行缓存数据的写入和获取。
2、简单的理解,就是原来使用本机的内存,现在使用另外一台服务器的内存。因为涉及跨服务器访问,性能比内存缓存稍低,但能解决分布式的问题。缓存服务器,并不直接使用内存,而是通过内存数据库进行缓存数据的管理,Redis使用比较多。
3、类似于内存缓存,AspNetCore提供了IDistributedCache接口,屏蔽了数据库了差异,无论使用Redis,或者其它数据库,用法都一样。同时,它的用法也几乎和IMemoryCache一样。键的类型为string,值的类型有两种,一是byte[],二是string。如果是byte[],需要自己进行读取和写入时的转换。
4、IDistributedCache的方法包括:GetAsync、SetAsync、RefreshAsync、RemoveAsync、GetStringAsync、SetStringAsync。
//首先注册Redis缓存服务 builder.Services.AddStackExchangeRedisCache(opt => { opt.Configuration = "localhost"; //配置Redis数据库连接 opt.InstanceName = "fun_"; //配置缓存键的前缀 }); //操作使用分布式缓存,和IMemoryCache相似 [ApiController] [Route("api/[controller]/[action]")] public class TestController : ControllerBase { //注入IDistributedCache服务 private readonly IDistributedCache disCache public TestController(IDistributedCache disCache) { this.disCache = disCache; } [HttpGet] public string Now() { //获取缓存键为“Now”的值 string s = disCache.GetString("Now"); //如果键值为空,则获取s值,并缓存数据,过期时间设置为30秒 if(s == null) { s = DateTime.Now.ToString(); //设置过期时间 var opt = new DistributedCacheEntryOptions(); opt.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30); //缓存数据 disCache.SetString("Now", s, opt); } return s; } }
特别说明:
1、本系列内容主要基于杨中科老师的书籍《ASP.NET Core技术内幕与项目实战》及配套的B站视频视频教程,同时会增加极少部分的小知识点
2、本系列教程主要目的是提炼知识点,追求快准狠,以求快速复习,如果说书籍学习的效率是视频的2倍,那么“简读系列”应该做到再快3-5倍