缓存

缓存(Caching)是系统优化中简单又有效的工具,投入小收效大,数据库中的索引等简单有效的优化功能本质上都是缓存。
缓存的概念:缓存命中,缓存命中率,缓存数据不一致,多级缓存。

客户端缓存

RFC7324是HTTP协议中对缓存进行控制的规范,其中重要的是cache-control这个响应报文头。服务器如果返回cache-control:max-age=60,则表示服务器指示浏览器端“可以缓存这个相应内容60秒”。
我们只要给需要进行缓存控制的控制器的操作方法添加ResponseCache特性,ASP.NET Core就会自动添加cache-control报文头。

1
2
3
4
5
6
7
8
9
[Route("[controller]/[action]")]
[ApiController]
public class TimeController : ControllerBase {
[HttpGet]
[ResponseCache(Duration = 10)]//缓存这个内容10秒
public DateTime GetTime([FromServices]Value value) {
return DateTime.Now;
}
}

服务器端缓存

服务器端缓存位于浏览器和服务器执行代码之间。
如果ASP.NET Core中安装了“响应缓存中间件”,那么ASP.NET Core不仅会继续根据[ResponseCache]设置来生成cache-control响应报文头来设置客户端缓存,而且服务器端也会按照[ResponseCache]的设置来对响应进行服务器端缓存。服务器端的缓存对不同客户端都生效,而客户端缓存只对自身生效。
用法:
app.MapControllers()之前app.UseCors()之后加上app.UseResponseCaching()。

1
app.UseResponseCaching();

大部分浏览器的“开发者工具”中可以禁用缓存,如果禁用了缓存,则在请求报文头中加入了”cache-control:no-cache”,如果加了该请求头,服务器端缓存和浏览器端缓存都会失效。
服务器端响应缓存还有很多限制,包括但不限于:响应状态码为200的GET或者HEAD请求才可能被缓存;报文头中不能含有Authorization、Set-Cookie等。
最好采用内存缓存、分布式缓存等。

内存缓存

1、把缓存数据放到应用程序的内存。内存缓存中保存的是一系列的键值对,就像字典类型一样。
2、内存缓存的数据保存在当前运行的网站程序的内存中,是和进程相关的。因为在web服务器中,多个不同网站是运行在不同的进程中的,因此不同网站的内存缓存是不会互相干扰的,而且网站重启后,内存缓存中的所有数据也就都被清空了。

用法

1、启用:builder.Services.AddMemoryCache()
2、注入IMemoryCache接口,查看接口的方法:TryGetValue、Remove、Set、GetOrCreate、GerOrCreateAsync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Route("[controller]/[action]")]
[ApiController]
public class BookController : ControllerBase {
private readonly IMemoryCache _memoryCache;//注入
public BookController(IMemoryCache memoryCache) {
_memoryCache = memoryCache;
}
[HttpGet]
public async Task<ActionResult<Book?>> GetBookById(int Id) {
var book = await _memoryCache.GetOrCreateAsync("Book" + Id, async (e) => {
return await DataBase.GetBookAsync(Id);
});
if(book == null) {
return NotFound("未找到");
} else {
return Ok(book);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DataBase {
public static Task<Book?> GetBookAsync(int Id) {
return Task.FromResult(DataBase.GetBook(Id));
}
public static Book? GetBook(int Id) {
switch (Id) {
case 1: return new Book() { Id = 1, Name = "C#图解教程" };
case 2: return new Book() { Id = 2, Name = "Java从入门到精通" };
case 3: return new Book() { Id = 3, Name = "C语言入门" };
default: return null;
}
}
}

缓存过期

1、在数据改变的时候调用Remove或者Set来删除或者修改缓存。
2、绝对过期时间:
到了设定的时间就清除指定的缓存。
在GetOrCreateAsync()方法的回调函数中有一个ICacheEntry类型的的参数,通过ICacheEntry对当前的缓存项做设置。
AbsoluteExpirationRelativeToNow用来设定缓存项的绝对过期时间。
3、滑动过期时间:
在设定时间内如果继续发请求,就续命。
ICacheEntry的SlidingExpiration属性用来设定缓存项的滑动过期时间。

1
2
3
4
5
6
var book = await _memoryCache.GetOrCreateAsync("Book" + Id, async (e) => {
Console.WriteLine("去数据库找ID为" + Id + "的书");
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);//30秒绝对过期时间
e.SlidingExpiration = TimeSpan.FromSeconds(10);//10秒滑动过期时间
return await DataBase.GetBookAsync(Id);
});

混合过期时间:
使用滑动过期实践策略,如果一个缓存项一直被频繁访问,那么这个缓存项就会一直被续期而不过期。可以对一个缓存项同时设定滑动过期时间和绝对过期时间,并且把绝对过期时间设定的比滑动过期时间长,这样缓存项的内容会在绝对过期时间内伴随着访问被滑动续期,但是一超过绝对过期时间,缓存项就会被删除。

缓存的问题

缓存穿透

1
2
3
4
5
var book = _memoryCache.Get<Book?>("Book" + Id);
if(book == null) {//缓存中不存在
book = await DataBase.GetBookAsync(Id);
_memoryCache.Set("Book" + Id, book);
}

在这段代码中,当我们从缓存中得到了null,我们就认为缓存中不存在,于是在数据库中查询。但如果数据库中本来就不存在也会返回null,这样会造成一个问题,如果用户请求一个不存在的书ID,就会不停的访问数据库,这样的漏洞称为缓存穿透。
解决方法:
把查不到也当做一个数据放入缓存中。
当我们使用GetOrCreateAsync()方法时,该方法会把null当成合法的缓存值,所以即便不喜欢用回调函数也要尽量使用GetOrCreateAsync()方法。

缓存雪崩

如果缓存中有很多数据,在固定的时间后统一失效,然后同时去数据库重新获取,就会造成数据库周期性的负载增大,进而造成缓存雪崩。
解决方法:
在基础过期时间之上,再加一个随机的过期时间。

1
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.Next(10,15));//随机10-15秒的过期时间

缓存数据混乱

解决方法:合理给Key命名。

延迟加载

IQueryable、IEnumerable类型可能存在延迟加载的问题,如果把这两种类型的变量指向的对象保存到缓存中,在我们把它们取出来再去执行的时候,如果它们延迟加载时候需要的对象已经被释放,就会执行失败,因此需要禁止缓存这两种类型。

分布式缓存

如果集群节点的数量非常多的话,每个节点的数据不能共享,每个节点都需要相同的数据,就会到数据库服务器重复查询,可能会把数据库压垮。
不是再读取内存中的缓存,而是创建一个缓存服务器来存储缓存数据。
1、常用的分布式缓存服务器有Redis、Memcached等。
2、.Net Core中没有内置分布式缓存,但是提供了统一的分布式缓存服务器的操作接口IDistributedCache,用法和内存缓存类似,用于可以更好的使用不同的分布式缓存服务器。
3、分布式缓存和内存缓存的区别:缓存值的类型为byte[],需要我们进行类型转换,也提供了一些按照string类型存取缓存的扩展方法,string类型在底层还是转换成char[]。

1
2
3
Book book = new Book();
JsonSerializer.Deserialize<Book>("json字符串");//将json字符串转为对应对象
JsonSerializer.Serialize(book);//将对象转为json字符串

用SQLSever做缓存性能不好。
Memcached是缓存专用,性能高但是集群、高可用等方面较弱,而且有“缓存键的最大长度为250字节”等限制。可以安装EnyimMemcachedCore这个第三方NuGet包。
Redis不局限于缓存,虽然性能比Memcached性能稍差,但高可用、集群等非常强大,适合在数据量大、高可用性等场合使用。
我用Redis做个演示:
1、安装Microsoft.Extensions.Caching.StackExchangeRedis包。
2、注册服务

1
2
3
4
builder.Services.AddStackExchangeRedisCache(options =>{
options.Configuration = "localhost";//缓存服务器地址
options.InstanceName = "Cache1_";//Key的前缀,避免和其他数据冲突
});

3、使用服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private readonly IMemoryCache _memoryCache;//注入
private readonly IDistributedCache _distributedCache;//注入
public BookController(IMemoryCache memoryCache, IDistributedCache distributedCache) {
_memoryCache = memoryCache;
_distributedCache = distributedCache;
}
[HttpGet]
public async Task<ActionResult<Book>> GetBookRedis(int Id) {
Book? book;
string? str = await _distributedCache.GetStringAsync("Book" + Id);
if(str == null) {
await Console.Out.WriteLineAsync("去数据库查找");
book =await DataBase.GetBookAsync(Id);
await _distributedCache.SetStringAsync("Book"+Id,JsonSerializer.Serialize(book));
} else {
book = JsonSerializer.Deserialize<Book>(str);
}
if (book == null) {
Console.WriteLine("未找到ID为" + Id + "的书");
return NotFound("未找到");
} else {
Console.WriteLine("找到了ID为" + Id + "的书");
return Ok(book);
}
}

Redis不会出现缓存穿透问题,但需要自己处理一下缓存雪崩问题。