参考官方文档:https://learn.microsoft.com/zh-cn/aspnet/core/performance/caching/overview?view=aspnetcore-6.0
内存中缓存可以存储任何对象。 分布式缓存接口仅限于 byte[]
,应用程序需要自行解决针对缓存对象的序列化和反序列化问题。 内存中和分布式缓存都将缓存项存储为键值对。
1、内存缓存
内存缓存就是把缓存数据放到应用程序内存中,利用内存读写比磁盘、网络请求快的特点来提高应用性能。不同程序内存缓存相互独立,一旦程序重启,内存缓存中的数据也会丢失。
1.1、过期策略
- 绝对时间过期策略:不论缓存对象最近使用的频率如何,对应的
ICacheEntry
对象总是在指定的时间点之后过期。注意:当AbsoluteExpiration
和AbsoluteExpirationRelativeToNow
都设置值的时候,绝对过期时间取距离当前时间近的那个设置。 - 滑动过期:如果在指定的时间内没有读取过该缓存条目,缓存将会过期。反之,针对缓存的每一次使用都会将过期时间向后延长
SlidingExpiration
时长。 - 两种混用:如设置了绝对过期时间为1小时后,且滑动过期时间为5分钟,则过期采用的算法为:
Min(AbsoluteExpiration - Now , SlidingExpiration)
。注意:绝对过期时间一定要大于滑动过期时间。 - 利用 IChangeToken 对象发送通知:在对象被修改之前永不过期,修改后更改缓存值。
1.2、缓存穿透和缓存雪崩
缓存穿透:一直查询不存在的值。
解决方案:把null值也当成一个数据存入缓存。使用 GetOrCreate()
/GetOrCreateAsync()
方法即可,因为它会把 null 值也当成合法的缓存值
缓存雪崩:缓存项集中过期引起缓存雪崩。
解决方案:在基础过期时间之上,再加一个随机的过期时间。
1.3、代码中使用
添加 Nuget 引用:Microsoft.Extensions.Caching.Memory
在 startup.cs 服务配置里面加上:services.AddMemoryCache();
上面将服务添加到依赖注入容器后,在构造函数中请求 IMemoryCache
实例:
public class IndexModel : PageModel
{
private readonly IMemoryCache _memoryCache;
public IndexModel(IMemoryCache memoryCache) =>
_memoryCache = memoryCache;
// ...
}
方法一:(推荐)使用 GetOrCreate 和 GetOrCreateAsync 扩展方法来缓存数据
public async Task OnGetCacheGetOrCreateAsync()
{
var cachedValue = await _memoryCache.GetOrCreateAsync(
CacheKeys.Entry,//自定义key,object
cacheEntry =>
{
//设置绝对过期时间
cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
//设置滑动过期时间
cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
return Task.FromResult(DateTime.Now);
});
// ...
}
//GetOrCreate类似
方法二:使用 TryGetValue 来检查缓存中是否包含时间。 如果未缓存时间,则创建一个新条目,并使用 Set 将该条目添加到缓存中
public void OnGet()
{
CurrentDateTime = DateTime.Now;
if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
{
cacheValue = CurrentDateTime;
//设置绝对过期时间
_memoryCache.Set(CacheKeys.Entry, DateTime.Now, TimeSpan.FromDays(1));
//设置滑动过期时间
_memoryCache.Set(CacheKeys.Entry, DateTime.Now,new MemoryCacheEntryOptions() { SlidingExpiration=new TimeSpan(0,0,10)});
}
CacheCurrentDateTime = cacheValue;
}
2、分布式缓存
分布式缓存是由多个应用服务器共享的缓存,通常作为访问它的应用服务器的外部服务进行维护。
2.1、基于 Redis 的分布式缓存
首先请确保已经正常安装并启动了 Redis,并在项目中添加Nuget包:Microsoft.Extensions.Caching.Redis
关于这个组件工具的文档说明,参考:https://stackexchange.github.io/StackExchange.Redis/
//startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedRedisCache(option =>
{
option.Configuration = "localhost";
option.InstanceName = "Demo_";//每个rediskey前面加上的字符串,用于区分不同应用系统。
});
services.AddControllersWithViews();
}
//HomeController.cs
private readonly IDistributedCache distributedCache;//Redis
public HomeController(IDistributedCache distributedCache)
{
this.distributedCache = distributedCache;
}
public async Task<string> TestRedisCache()
{
var time = await distributedCache.GetStringAsync("CurrentTime");
if (null==time)
{
time = DateTime.Now.ToString();
await distributedCache.SetAsync("CurrentTime", Encoding.UTF8.GetBytes(time));
//设置带有过期时间的
//redisCache.SetString("str", DateTime.Now.Ticks.ToString(),new DistributedCacheEntryOptions() { AbsoluteExpiration=DateTime.Now.AddSeconds(10)});
}
return $"缓存时间:{time}(,当前时间:{DateTime.Now})";
}
可以额外了解下:StackExchange.Redis 。它是 .NET 领域知名的 Redis 客户端框架。
3、封装
解决缓存穿透和缓存雪崩问题。
3.1、扩展Random.NextDouble()
扩展 random 随机数使其支持 Double ,避免int类型不精确或者缓存过多导致随机数重复,造成缓存雪崩。
RondowExtensions
public static class RondowExtensions
{
public static double NextDouble(this Random random, double minValue, double maxValue)
{
if (minValue >= maxValue) throw new ArgumentOutOfRangeException(nameof(minValue), "minValue annot be bigger than maxValue");
double x = random.NextDouble();
return x * maxValue + (1 - x) * minValue;
}
}
3.2、内存缓存操作帮助类
IMemoryCacheHelper
public interface IMemoryCacheHelper
{
/// <summary>
/// 从缓存中获取数据,如果缓存中没有数据,则调用valueFactory获取数据。
/// </summary>
/// <remarks>
/// 这里加入了缓存数据的类型不能是IEnumerable、IQueryable等类型的限制
/// </remarks>
/// <typeparam name="TResult">缓存的值的类型</typeparam>
/// <param name="cacheKey">缓存的key</param>
/// <param name="valueFactory">提供数据的委托</param>
/// <param name="expireSeconds">缓存过期秒数的最大值,实际缓存时间是在[expireSeconds,expireSeconds*2)之间,这样可以一定程度上避免大批key集中过期导致的“缓存雪崩”的问题</param>
/// <returns></returns>
TResult? GetOrCreate<TResult>(string cacheKey, Func<ICacheEntry, TResult?> valueFactory, int expireSeconds = 60);
Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> valueFactory, int expireSeconds = 60);
/// <summary>
/// 删除缓存的值
/// </summary>
/// <param name="cacheKey"></param>
void Remove(string cacheKey);
}
MemoryCacheHelper
/// <summary>
/// IMemoryCacheHelper 内存缓存帮助实现类
/// </summary>
internal class MemoryCacheHelper : IMemoryCacheHelper
{
private readonly IMemoryCache _memoryCache;
public MemoryCacheHelper(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public TResult? GetOrCreate<TResult>(string cacheKey, Func<ICacheEntry, TResult?> valueFactory, int expireSeconds = 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, expireSeconds);
result = valueFactory(entry)!;
entry.Value = result;
}
return result;
}
public async Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> valueFactory, int expireSeconds = 60)
{
ValidateValueType<TResult>();
if (!_memoryCache.TryGetValue(cacheKey, out TResult result))
{
using ICacheEntry entry = _memoryCache.CreateEntry(cacheKey);
InitCacheEntry(entry, expireSeconds);
result = (await valueFactory(entry))!;
entry.Value = result;
}
return result;
}
public void Remove(string cacheKey) => _memoryCache.Remove(cacheKey);
/// <summary>
/// 过期时间
/// </summary>
/// <remarks>
/// Random.Shared 是.NET6新增的
/// </remarks>
/// <param name="entry">ICacheEntry</param>
/// <param name="baseExpireSeconds">过期时间</param>
private static void InitCacheEntry(ICacheEntry entry, int baseExpireSeconds) =>
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.NextDouble(baseExpireSeconds, baseExpireSeconds * 2));
/// <summary>
/// 验证值类型
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <exception cref="InvalidOperationException"></exception>
private static void ValidateValueType<TResult>()
{
// 因为IEnumerable、IQueryable等有延迟执行的问题,造成麻烦,因此禁止用这些类型
Type typeResult = typeof(TResult);
// 如果是IEnumerable<String>这样的泛型类型,则把String这样的具体类型信息去掉,再比较
if (typeResult.IsGenericType)
{
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.");
}
}
}
3.3、分布式缓存操作帮助类
IDistributedCacheHelper
public interface IDistributedCacheHelper
{
/// <summary>
/// 创建缓存
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="cacheKey"></param>
/// <param name="valueFactory"></param>
/// <param name="expireSeconds"></param>
/// <returns></returns>
TResult? GetOrCreate<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, TResult?> valueFactory, int expireSeconds = 60);
/// <summary>
/// 创建缓存
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="cacheKey"></param>
/// <param name="valueFactory"></param>
/// <param name="expireSeconds"></param>
/// <returns></returns>
Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, Task<TResult?>> valueFactory, int expireSeconds = 60);
/// <summary>
/// 删除缓存
/// </summary>
/// <param name="cacheKey"></param>
void Remove(string cacheKey);
/// <summary>
/// 删除缓存
/// </summary>
/// <param name="cacheKey"></param>
/// <returns></returns>
Task RemoveAsync(string cacheKey);
}
DistributedCacheHelper
/// <summary>
/// 分布式缓存帮助实现类
/// </summary>
public class DistributedCacheHelper : IDistributedCacheHelper
{
private readonly IDistributedCache _distCache;
public DistributedCacheHelper(IDistributedCache distCache)
{
_distCache = distCache;
}
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);
// 如果数据源中也没有查到,可能会返回null
TResult? result = valueFactory(options);
// 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) => _distCache.RemoveAsync(cacheKey);
private static DistributedCacheEntryOptions CreateOptions(int expireSeconds) => new()
{
// 过期时间.Random.Shared 是.NET6新增的
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.NextDouble(expireSeconds, expireSeconds * 2))
};
}
标签:缓存,string,memoryCache,过期,cacheKey,内存,public,分布式 From: https://www.cnblogs.com/xixi-in-summer/p/18073563