Asp.Net Core 缓存
## 1.Asp.Net Core 缓存
1.1 缓存
(1)什么是缓存:
缓存(Caching)是系统优化中简单又有效的工具,投入小收效大。数据库中的索引等简单有效的优化功能本质上都是缓存。
(2)缓存的概念
- 缓存命中
- 缓存命中率
- 缓存数据不一致
(3)多级缓存
1.2 Asp.Net Core客户端响应缓存
-
RFC7324是HTTP协议中对缓存进行控制的规范,其中重要的是cache-control这个响应报文头。服务器如果返回cache-control:max-age=60,则表示服务器指示浏览器端“可以缓存这个响应内容60秒”
-
我们只要给需要进行缓存控制的控制器的操作方法添加ResponseCacheAttribute这个Attribute,ASP.NET Core会自动添加cache-control报文头
缓存10s
1.3 服务端响应缓存
(1)Response Caching Middleware
“响应缓存中间件”的好处:对于来自不同客户端的相同请求或者不支持客户端缓存的客户端,能降低服务器端的压力
用法:app.MapControllers()之前加上app.UseResponseCaching()。请确保app.UseCors()写到app.UseResponseCaching()之前。
(2)服务器端响应缓存很鸡肋
- 无法解决恶意请求给服务器带来的压力。
- 服务器端响应缓存还有很多限制,包括但不限于:响应状态码为200的GET或者HEAD响应才可能被缓存;报文头中不能含有Authorization、Set-Cookie等
1.4 Asp.Net Core内存缓存
(1)内存缓存
- 把缓存数据放到应用程序的内存。内存缓存中保存的是一系列的键值对,就像Dictionary类型一样
- 内存缓存的数据保存在当前运行的网站程序的内存中,是和进程相关的。因为在Web服务器中,多个不同网站是运行在不同的进程中的,因此不同网站的内存缓存是不会互相干扰的,而且网站重启后,内存缓存中的所有数据也就都被清空了
(2)内存缓存用法
- builder.Services.AddMemoryCache()
- 注入IMemoryCache接口,查看接口的方法:TryGetValue、Remove、Set、GetOrCreate、GetOrCreateAsync
1.5 Asp.Net Core 缓存过期时间策略
(1)缓存过期时间:
使用缓存怎么和更新数据保持一致
- 在数据改变的时候调用Remove或者Set来删除或者修改缓存(优点:及时)
- 过期时间
- 绝对过期时间
- 滑动过期时间
(2)缓存的绝对过期时间
-
GetOrCreateAsync()方法的回调方法中有一个ICacheEntry类型的参数,通过ICacheEntry对当前的缓存项做设置。
-
AbsoluteExpirationRelativeToNow用来设定缓存项的绝对过期时间。
(3)缓存的滑动过期时间
- ICacheEntry的SlidingExpiration属性用来设定缓存项的滑动过期时间
(4)绝对过期时间和滑动过期时间混用
可以对一个缓存项同时设定滑动过期时间和绝对过期时间,并且把绝对过期时间设定的比滑动过期时间长。这样缓存项的内容会在绝对过期时间内随着访问被滑动续期,但是一旦超过了绝对过期时间,缓存项就会被删除。
1.6 Asp.Net Core缓存穿透
(1)缓存穿透:
数据库不存在,缓存中也不存在,每次访问都会执行数据库操作,给数据库造成压力
解决缓存穿透:
-
接口层增加校验
-
把“查不到”也当成一个数据放入缓存。缓存有效时间可以设置短点
-
GetOrCreateAsync方法即可,因为它会把null值也当成合法的缓存值。
(2)缓存击穿:
缓存不存在,数据库中存在。缓存没获取到,同时取数据库获取。
- 加互斥锁
1.7 Asp.Net Core缓存雪崩
(1)缓存雪崩:
- 缓存项集中过期引起缓存雪崩
解决办法:
- 基础过期时间之上,增加随机过期时间
Random.Shared.Next(20,30)
(2)缓存数据混乱
缓存Key加上数据Id,保证缓存Key的唯一性
1.8 封装内存缓存操作的帮助类
- IQueryable、IEnumerable等类型可能存在着延迟加载的问题
- 实现随机缓存过期时间
/// <summary>
/// 用ASP.NET的IMemoryCache实现的内存缓存
/// </summary>
public class MemoryCacheHelper : IMemoryCacheHelper
{
private readonly IMemoryCache memoryCache;
public MemoryCacheHelper(IMemoryCache memoryCache)
{
this.memoryCache = memoryCache;
}
private static void ValidateValueType<TResult>()
{
//因为IEnumerable、IQueryable等有延迟执行的问题,造成麻烦,因此禁止用这些类型
Type typeResult = typeof(TResult);
if (typeResult.IsGenericType)//如果是IEnumerable<String>这样的泛型类型,则把String这样的具体类型信息去掉,再比较
{
typeResult = typeResult.GetGenericTypeDefinition();
}
//注意用相等比较,不要用IsAssignableTo
if (typeResult == typeof(IEnumerable<>) || typeResult == typeof(IEnumerable)
|| typeResult == typeof(IAsyncEnumerable<TResult>)
|| typeResult == typeof(IQueryable<TResult>) || typeResult == typeof(IQueryable))
{
throw new InvalidOperationException($"TResult of {typeResult} is not allowed, please use List<T> or T[] instead.");
}
}
private static void InitCacheEntry(ICacheEntry entry, int baseExpireSeconds)
{
//过期时间.Random.Shared 是.NET6新增的
double sec = Random.Shared.NextDouble(baseExpireSeconds, baseExpireSeconds * 2);
TimeSpan expiration = TimeSpan.FromSeconds(sec);
entry.AbsoluteExpirationRelativeToNow = expiration;
}
public TResult? GetOrCreate<TResult>(string cacheKey, Func<ICacheEntry, TResult?> valueFactory, int baseExpireSeconds = 60)
{
ValidateValueType<TResult>();
//因为IMemoryCache保存的是一个CacheEntry,所以null值也认为是合法的,因此返回null不会有“缓存穿透”的问题
//不调用系统内置的CacheExtensions.GetOrCreate,而是直接用GetOrCreate的代码,这样免得包装一次委托
if (!memoryCache.TryGetValue(cacheKey, out TResult result))
{
using ICacheEntry entry = memoryCache.CreateEntry(cacheKey);
InitCacheEntry(entry, baseExpireSeconds);
result = valueFactory(entry)!;
entry.Value = result;
}
return result;
}
public async Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> valueFactory, int baseExpireSeconds = 60)
{
ValidateValueType<TResult>();
if (!memoryCache.TryGetValue(cacheKey, out TResult result))
{
using ICacheEntry entry = memoryCache.CreateEntry(cacheKey);
InitCacheEntry(entry, baseExpireSeconds);
result = (await valueFactory(entry))!;
entry.Value = result;
}
return result;
}
public void Remove(string cacheKey)
{
memoryCache.Remove(cacheKey);
}
}
1.9 Asp.Net Core分布式缓存
(1)分布式缓存
- 常用的分布式缓存服务器有Redis、Memcached等
- .NET Core中提供了统一的分布式缓存服务器的操作接口IDistributedCache,用法和内存缓存类似
- 分布式缓存和内存缓存的区别:缓存值的类型为byte[],需要我们进行类型转换,也提供了一些按照string类型存取缓存值的扩展方法
(2)缓存服务器
- 数据库做缓存性能并不好
- Memcached是缓存专用,性能非常高,但是集群、高可用等方面比较弱,而且有“缓存键的最大长度为250字节”等限制。可以安装EnyimMemcachedCore这个第三方NuGet包
- Redis不局限于缓存,Redis做缓存服务器比Memcached性能稍差,但是Redis的高可用、集群等方便非常强大,适合在数据量大、高可用性等场合使用。
(3)分布式缓存用法
-
Microsoft.Extensions.Caching.StackExchangeRedis
-
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = “peng_”;//避免混乱
});
1.10 Asp.Net Core 封装分布式缓存帮助类
- 解决缓存穿透、缓存雪崩等问题。
- 自动地进行其他类型的转换。
public class DistributedCacheHelper : IDistributedCacheHelper
{
private readonly IDistributedCache distCache;
public DistributedCacheHelper(IDistributedCache distCache)
{
this.distCache = distCache;
}
private static DistributedCacheEntryOptions CreateOptions(int baseExpireSeconds)
{
//过期时间.Random.Shared 是.NET6新增的
double sec = Random.Shared.NextDouble(baseExpireSeconds, baseExpireSeconds * 2);
TimeSpan expiration = TimeSpan.FromSeconds(sec);
DistributedCacheEntryOptions options = new DistributedCacheEntryOptions();
options.AbsoluteExpirationRelativeToNow = expiration;
return options;
}
public TResult? GetOrCreate<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, TResult?> valueFactory, int expireSeconds = 60)
{
string jsonStr = distCache.GetString(cacheKey);
//缓存中不存在
if (string.IsNullOrEmpty(jsonStr))
{
var options = CreateOptions(expireSeconds);
TResult? result = valueFactory(options);//如果数据源中也没有查到,可能会返回null
//null会被json序列化为字符串"null",所以可以防范“缓存穿透”
string jsonOfResult = JsonSerializer.Serialize(result,
typeof(TResult));
distCache.SetString(cacheKey, jsonOfResult, options);
return result;
}
else
{
//"null"会被反序列化为null
//TResult如果是引用类型,就有为null的可能性;如果TResult是值类型
//在写入的时候肯定写入的是0、1之类的值,反序列化出来不会是null
//所以如果obj这里为null,那么存进去的时候一定是引用类型
distCache.Refresh(cacheKey);//刷新,以便于滑动过期时间延期
return JsonSerializer.Deserialize<TResult>(jsonStr)!;
}
}
public async Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, Task<TResult?>> valueFactory, int expireSeconds = 60)
{
string jsonStr = await distCache.GetStringAsync(cacheKey);
if (string.IsNullOrEmpty(jsonStr))
{
var options = CreateOptions(expireSeconds);
TResult? result = await valueFactory(options);
string jsonOfResult = JsonSerializer.Serialize(result,
typeof(TResult));
await distCache.SetStringAsync(cacheKey, jsonOfResult, options);
return result;
}
else
{
await distCache.RefreshAsync(cacheKey);
return JsonSerializer.Deserialize<TResult>(jsonStr)!;
}
}
public void Remove(string cacheKey)
{
distCache.Remove(cacheKey);
}
public Task RemoveAsync(string cacheKey)
{
return distCache.RemoveAsync(cacheKey);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)