系列文章目录
链接: 【ASP.NET Core】REST与RESTful详解,从理论到实现
链接: 【ASP.NET Core】深入理解Controller的工作机制
文章目录
前言
不知道大家在发现执行SQL查询时,相同的查询再次执行所消耗的时间远少于第一次。这是因为数据库维护一套用于缓存SQL语句解析和优化后的执行计划的机制,避免对重复SQL语句进行重复的解析生成抽象语法树、语义检查、优化等耗时操作。
还有就是在浏览器打开网页的时候,通过网络监视窗,我们也能发现很多请求来自于【memory cache】,字面意义上的一种缓存。也是通过缓存来降低HTTP请求。
这就是缓存的魔力,在系统中简单又有效的工具,投入小但收效甚大。本文将聚焦于ASP.NET Core中的内存缓存,讨论它如何在我们的程序中起到提升应用性能的作用。
提示:以下是本篇文章正文内容,下面案例可供参考
一、ASP.NET Core中的内存缓存——MemoryCache
1.1 内存缓存的结构
在ASP.NET Core中,内存缓存(MemoryCache)一种将数据存储在应用程序进程内存中的机制,其中数据是以一种键值对的形式存储。
1.2 MemoryCache的注册
在ASP.NET Core中,我们一般采用Program文件里注册服务,在程序里通过依赖注入的方式使用MemoryCache。AddMemoryCache是一个MemoryCacheServiceCollectionExtensions下的静态扩展方法里是将MemoryCache以单例模式注册到IOC容器里,并且通过AddOptions添加选项模式的依赖,支持配置的动态更新和验证。
Program.cs 注册
builder.Services.AddMemoryCache();
MemoryCacheServiceCollectionExtensions扩展方法
public static IServiceCollection AddMemoryCache(this IServiceCollection services)
{
ThrowHelper.ThrowIfNull(services);
services.AddOptions();
services.TryAdd(ServiceDescriptor.Singleton<IMemoryCache, MemoryCache>());
return services;
}
public static IServiceCollection AddOptions(this IServiceCollection services)
{
System.ThrowHelper.ThrowIfNull(services, "services");
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
return services;
}
1.3 MemoryCache的配置项
1.3.1 缓存时间的过期策略
MemoryCache中的过期分两种,其一是绝对过期 (Absolute Expiration),另一个是滑动过期 (Sliding Expiration)。
绝对过期 :SetAbsoluteExpiration(DateTimeOffset/TimeSpan)
缓存项在指定确切时间点后失效。适用于数据在特定时间点后一定过期的场景。当使用 DateTimeOffset 时,缓存项在指定确切时间点后失效。使用 TimeSpan 时,缓存项在当前时间基础上加上指定的时间间隔后失效。滑动过期 :SetSlidingExpiration(TimeSpan)
缓存项在指定的时间间隔内没有被访问则失效。每次访问(读取)都会重置这个倒计时。适用于访问频繁的数据,如用户会话数据。但是需要保证过期永不失效的问题。
如果一个缓存项一直被频繁访问,那么这个缓存项就会一直被续期而不过期。可以对一个缓存项同时设定滑动过期时间和绝对过期时间,并且把绝对过期时间设定的比滑动过期时间长,这样缓存项的内容会在绝对过期时间内随着访问被滑动续期,但是一旦超过了绝对过期时间,缓存项就会被删除。这种就是绝对过期和滑动过期组合使用的方式。
1.3.2 缓存的优先级
缓存项的优先级(CacheItemPriority)是控制缓存清理策略的重要参数。当内存不足时,优先级较低的项会被优先回收,以腾出空间给更重要的数据。
通过CacheItemPriority枚举型定义四种类型的优先级Low,Normal(默认),High,NeverRemove(永不删除)
Controller里示例
[HttpGet("{id}")]
public async Task<ActionResult<Movie?>> Movies(int id)
{
//_logger.LogInformation("1");
_logger.LogDebug("开始获取数据");
Movie? movie = await _memoryCache.GetOrCreateAsync($"Movie{id}", async (e) =>
{
e.SetPriority(CacheItemPriority.High);//缓存优先级高
e.SetAbsoluteExpiration(TimeSpan.FromMinutes(10));//绝对过期时间-10分钟
e.SetSlidingExpiration(TimeSpan.FromMinutes(5));//缓存滑动过期时间-5分钟
//e.SetAbsoluteExpiration(TimeSpan.FromSeconds(Random.Shared.Next(1000)));
return await _movieAssert.GetMovieAsync(id);
});
if (movie is null)
{
return NotFound("没有数据");
}
return movie;
}
1.4 MemoryCache的基本操作与扩展
通过观察Microsoft.Extensions.Caching.Memory下的IMemoryCache接口。我们发现创建缓存实体(CreateEntry),获取缓存统计信息(GetCurrentStatistics),移除指定缓存(Remove),和视图获取指定缓存(TryGetValue)方法。
并且在Microsoft.Extensions.Caching.Memory命名空间下,还有一个CacheExtensions静态扩展方法,提供更加丰富的操作方法。比如
TryGetValue:试图获取某个key的TItem值,用out修饰。并且返回bool值。在该扩展方法中使用IMemoryCache的TryGetValue获取值。如果值为空附默认值并返回false,如果值存在并且类型匹配,给TItem赋值返回true。
public static bool TryGetValue<TItem>(this IMemoryCache cache, object key, out TItem? value) { if (cache.TryGetValue(key, out object value2)) { if (value2 == null) { value = default(TItem); return true; } if (value2 is TItem val) { value = val; return true; } } value = default(TItem); return false; }
GetOrCreateAsync:该方法通过试图获取key对应的缓存值,如果缓存不存在则新建一个新的缓存。通过上文封装的静态方法TryGetValue判断缓存是否存在。如果不存在就通过IMemoryCache的CreateEntry创建缓存,并且调用委托执行工厂方法。最终返回缓存值,保证即使是不存在的数据也有一个对应的缓存,防止缓存穿透。
public static async Task<TItem?> GetOrCreateAsync<TItem>(this IMemoryCache cache, object key, Func<ICacheEntry, Task<TItem>> factory, MemoryCacheEntryOptions? createOptions) { if (!cache.TryGetValue(key, out object value)) { using ICacheEntry entry = cache.CreateEntry(key); if (createOptions != null) { entry.SetOptions(createOptions); } value = (entry.Value = await factory(entry).ConfigureAwait(continueOnCapturedContext: false)); } return (TItem)value; }
CacheExtensions还有Get和Set方法,获取和写入缓存,以及一系列重载。
二、内存缓存中碰到的各类问题
2.1 缓存穿透
频繁查询不合规的数据,但实际数据库里并不存在,每一次查询都会引起数据库的查询操作,给数据库造成特别大的性能压力,导致每次请求都绕过缓存直接访问数据库。
解决方法:把“查不到”也当成一个数据放入缓存,用封装好的GetOrCreateAsync,因为它把null值也当成合法的缓存值
在上面的GetOrCreateAsync源码分析中已经分析了GetOrCreateAsync是如何解决缓存穿透的问题
2.2 缓存击穿
某个热点Key在缓存中突然失效,比如过期了或者被删除了,导致大量并发请求瞬间失去缓存保护,直接涌向数据库,造成数据库短时间内压力剧增的现象。
解决方法:不设置过期时间,通过后台定时任务主动更新缓存。或者是使用Lock锁,当缓存失效时保证只有一条请求能实际执行数据的获取,其他请求等待该请求更新缓存后再获取数据。
2.3 缓存雪崩
缓存雪崩是缓存项集中过期导致原本由缓存承接的大量请求瞬间涌向数据库,数据库因无法承受压力而崩溃的现象。
解决方法:将过期时间打散,比如在设置基础过期时间上添加一段时间范围的随机数,以此保证缓存不会在同一个时间点过期。
2.4 缓存键重复
键需要精心设计以确保唯一性和可读性。避免键冲突。
总结
文章围绕ASP.NET Core 中 MemoryCache 展开,先介绍其结构、注册方式、配置项(过期策略、优先级)及基本操作与扩展方法,后阐述内存缓存常见问题(穿透、击穿、雪崩)及解决办法。