.NET Core缓存
目录
缓存的概念
客户端响应缓存
cache-control
服务器端响应缓存
内存缓存(In-memory cache)
用法
GetOrCreateAsync
缓存过期时间策略
缓存的过期时间
解决方法:
两种过期时间策略:
绝对过期时间
滑动过期时间
两种过期时间混用
总结
缓存的概念
缓存(Caching)是用来保存数据的区域,从缓存区域读取数据的速度比从数据源读取数据的速度快很多,从数据源获取数据后,我们可以把数据保存到缓存中,下次再需要获取同样数据时,可以直接从缓存中获取之前保存的数据,是系统优化中简单又有效的工具,投入小收效大。数据库中的索引等简单有效的优化功能本质上都是缓存。
- 缓存命中:从缓存中获取了要获取的数据
- 缓存命中率:多次请求中,命中的请求占全部请求的百分比
- 缓存数据不一致:数据源中数据保存到缓存后,发生了变化
- 多级缓存:在Web开发中,存在多级缓存,浏览器存在“浏览器端缓存”,网关节电服务器存在“节点缓存”,,Web服务器上可能存在“服务器端缓存”,只要在任何一个节点上命中缓存,请求就会直接返回,而不会继续向后传递。
客户端响应缓存
cache-control
- RFC7324是HTTP协议中对缓存进行控制的规范,其中重要的是cache-control这个响应报文头。服务器如果返回cache-control:max-age=60,则表示服务器指示浏览器端“可以缓存这个响应内容60秒”。
- 我们只要给需要进行缓存控制的控制器的操作方法添加ResponseCacheAttribute这个Attribute,ASP.NET Core会自动添加cache-control报文头。
[Route("api/[controller]/[action]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet]
[ResponseCache(Duration = 20)]
public DateTime Now()
{
return DateTime.Now;
}
}
服务器端响应缓存
- 如果ASP.NET Core中安装了“响应缓存中间件” ,那么ASP.NET Core不仅会继续根据[ResponseCache]设置来生成cache-control响应报文头来设置客户端缓存,而且服务器端也会按照[ResponseCache]的设置来对响应进行服务器端缓存。和客户端端缓存的区别?来自多个不同客户端的相同请求。
- “响应缓存中间件”的好处:对于来自不同客户端的相同请求或者不支持客户端缓存的客户端,能降低服务器端的压力。
- 用法:app.MapControllers()之前加上app.UseResponseCaching()。请确保app.UseCors()写到app.UseResponseCaching()之前。
缺点:
- 无法解决恶意请求给服务器带来的压力。
- 服务器端响应缓存还有很多限制,包括但不限于:响应状态码为200的GET或者HEAD响应才可能被缓存;报文头中不能含有Authorization、Set-Cookie等。
- 可以采用内存缓存、分布式缓存等。
由于服务器端响应缓存开发调试的麻烦以及过于苛刻的限制,因此除非开发人员能够灵活掌握并应用,否则不建议启用“响应缓存中间件”。对于只需要进行客户端响应缓存处理的操作方法,标注ResponseCache即可,如果还需要在服务器端进行缓存处理,建议采用ASP.NET Core提供的内存缓存、分布式缓存等机制来编写程序。
内存缓存(In-memory cache)
把缓存数据放到应用程序的内存。内存缓存中保存的是一系列的键值对,就像Dictionary类型一样。
内存缓存的数据保存在当前运行的网站程序的内存中,是和进程相关的。因为在Web服务器中,多个不同网站是运行在不同的进程中的,因此不同网站的内存缓存是不会互相干扰的,而且网站重启后,内存缓存中的所有数据也就都被清空了。
用法
- 添加服务:builder.Services.AddMemoryCache()
- 注入IMemoryCache接口,查看接口的方法:TryGetValue、Remove、Set、GetOrCreate、GetOrCreateAsync
GetOrCreateAsync
GetOrCreateAsync<TItem>(object key, Func<ICacheEntry, Task<TItem>> factory)
获取缓存键为key的缓存值,方法的返回值为获取的缓存值,如果缓存中没有缓存键为key的缓存值,则调用factory指向的回调从数据源获取数据,把获取的数据作为缓存值保存到缓存中,并且把获取的数据作为方法的返回值。
Program.cs
//添加内存缓存服务
services.AddMemoryCache();
//添加DbContext
services.AddScoped<MyDbContext>();
public record Book(long Id, string Name);
public class BookConfig : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("T_Books");
}
}
public class MyDbContext:DbContext
{
DbSet<Book> books { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlServer("Server=.;Database=demo;Trusted_Connection=true;MultipleActiveResultSets=true;TrustServerCertificate=true;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}
[Route("api/[controller]/[action]")]
[ApiController]
public class TestController : ControllerBase
{
private readonly IMemoryCache memoryCache;
private readonly ILogger<TestController> logger;
public TestController(IMemoryCache memoryCache, ILogger<TestController> logger)
{
this.memoryCache = memoryCache;
this.logger = logger;
}
[HttpGet]
public async Task<ActionResult<Book?>> GetById(int id)
{
using (MyDbContext ctx = new MyDbContext())
{
//没加内存缓存
//var result = await ctx.Set<Book>().SingleOrDefaultAsync(o => o.Id == id);
//if (result == null)
//{
// return NotFound($"找不到id={id}的书");
//}
//else
//{
// return result;
//}
//内存缓存
logger.LogInformation($"开始执行GetBookById,id={id}");
Book? b = await memoryCache.GetOrCreateAsync("Book" + id, async (e) =>
{
logger.LogInformation($"缓存没找到,到数据库查询,id={id}");
return await ctx.Set<Book>().SingleOrDefaultAsync(o => o.Id == id);
});
logger.LogInformation($"GetBookById结果:{b}");
if (b == null)
{
return NotFound($"找不到id={id}的书");
}else
{
return Ok(b);
}
}
}
}
查询两次结果
缓存过期时间策略
缓存的过期时间
上面的例子中的缓存不会过期,除非重启服务器。
解决方法:
- 在数据改变的时候调用Remove或者Set来删除或者修改缓存(优点:及时);
- 过期时间(只要过期时间比较短,缓存数据不一致的情况也不会持续很长时间。)
两种过期时间策略:
GetOrCreateAsync()方法的回调方法中有一个ICacheEntry类型的参数,通过ICacheEntry对当前的缓存项做设置。
绝对过期时间
设置缓存完的指定时间后,缓存项被清除。
AbsoluteExpirationRelativeToNow:用来设定缓存项的绝对过期时间。
Book? b = await memoryCache.GetOrCreateAsync("Book" + id, async (e) =>
{
logger.LogInformation($"缓存没找到,到数据库查询,id={id}");
//缓存有效期10s
e.AbsoluteExpirationRelativeToNow=TimeSpan.FromSeconds(10);
return await ctx.Set<Book>().SingleOrDefaultAsync(o => o.Id == id);
});
滑动过期时间
设置缓存完的指定时间后,如果对应的缓存数据没有被访问,缓存项被清除,如果在指定时间内,被访问一次,则缓存项的过期时间会自动续期。
Book? b = await memoryCache.GetOrCreateAsync("Book" + id, async (e) =>
{
logger.LogInformation($"缓存没找到,到数据库查询,id={id}");
e.SlidingExpiration=TimeSpan.FromSeconds(10);
return await ctx.Set<Book>().SingleOrDefaultAsync(o => o.Id == id);
});
两种过期时间混用
使用滑动过期时间策略,如果一个缓存项一直被频繁访问,那么这个缓存项就会一直被续期而不过期。可以对一个缓存项同时设定滑动过期时间和绝对过期时间,并且把绝对过期时间设定的比滑动过期时间长,这样缓存项的内容会在绝对过期时间内随着访问被滑动续期,但是一旦超过了绝对过期时间,缓存项就会被删除。
总结
- 无论用那种过期时间策略,程序中都会存在缓存数据不一致的情况。部分系统(博客等)无所谓,部分系统不能忍受(比如金融)。
- 可以通过其他机制获取数据源改变的消息,再通过代码调用IMemoryCache的Set方法更新缓存。