.Net Core缓存组件(MemoryCache)【缓存篇(二)】
一、前言
1、上篇.NET Core ResponseCache【缓存篇(一)】中我们提到了使用客户端缓存、和服务端缓存。本文我们介绍MemoryCache缓存组件,说到服务端缓存我们一般都会想到MemoryCache、Redis等等优秀的缓存组件,各自有各自使用的场景。MemoryCache的类型比较单一是Object对象存储、Redis的数据类型就相对比较多 String(字符串),List(列表),set(去重集合),zset(去重排序集合),hash(哈希)。还有HyperLogLog,bitMap,GeoHash,BloomFilter这四种还没有详细了解,等下篇讲解Redis的时候详细给各位姥爷供上。
二、MemoryCache缓存组件使用
1、首先我们需要将MemoryCache组件注入到程序中。
public void ConfigureServices(IServiceCollection services) { //添加内存缓存 services.AddMemoryCache(); }
2、使用的方法也比较简单和我们平常写代码差不多。在控制其中注入基类IMemoryCache,看到下面这个基类,我还以为会只有两个方法。
#region 注入缓存 private readonly IMemoryCache Cache; public MemoryCacheController(IMemoryCache cache) { Cache = cache; } #endregion
3、当我正常使用的时候发现方法原来使用推展方法写在这里。使用起来很简单我就直接代码了。
4、设置缓存主要是四种过期时间。
1:永久不过期 就是老子就是不过期气死你。
2:绝对过期时间 这个就好比我们买的安眠药,2020年07月22日过期。那你今天就要好好利用这个时间了。
3:相对过期时间 这个好比你吃饭三个小时,肚子就呱呱叫了,相对于当前时间延长。
4:滑动过期时间 这个就是相对过期时间的升级版,我们每隔三个小时就会肚子饿,那么我们是不是可以等到两个半小时就是吃饭,肚子就不会叫了。这样在一定时间内使用这个缓存就会在你最后使用缓存的时间上延长。
//设置缓存 当我们没有指定过期时间,理论是永久的(后面会说到不理论的) Cache.Set("key", "value"); Console.WriteLine(Cache.Get("key")); //设置缓存绝对过期时间 Cache.Set("key1", "value", new DateTimeOffset(new DateTime(2020, 7, 22))); Console.WriteLine(Cache.Get("key1")); //设置相对过期时间 Cache.Set("key2", "value", new TimeSpan(0, 0, 10)); Console.WriteLine(Cache.Get("key2")); //设置滑动过期时间 Cache.Set("key3", "value", new MemoryCacheEntryOptions() { //设置滑动过期 SlidingExpiration = new TimeSpan(0, 0, 5), //设置缓存的优先级,当我们缓存空间不足的时候会移除等级低的缓存,以此类推(清除的时候不会管是否过期) //Low, 低的意思 //Normal, 正常模式 默认模式 //High, 高 //NeverRemove 绝不回收,这个就算内存满了也不会清除。 Priority = CacheItemPriority.NeverRemove }); Console.WriteLine(Cache.Get("key3"));
5、这里我们还要讲一下缓存失效机制,这里它不是主动失效的,只有当我们再次对缓存MemoryCache组件进行增删查改的时候才会扫描里面的内存是否存在过期的,进行垃圾回收。这里我们使用GetOrCreate方法创建我们的缓存证实我们的说法。这个可以设置过期回调函数。图下我们可以看到当我们的缓存过期之后,就没有对缓存进行操作了就不会有回调函数触发。
Cache.GetOrCreate("key4", cacheEntry => { //设置滑动过期 cacheEntry.SlidingExpiration= new TimeSpan(0, 0, 5); //设置删除回调函数 cacheEntry.RegisterPostEvictionCallback(CallbackFunction); //设置内存 return cacheEntry.Value = "滑动过期时间带删除回调"; }); Thread.Sleep(2000); Console.WriteLine(Cache.Get("key4")); Thread.Sleep(4000); Console.WriteLine(Cache.Get("key4")); Thread.Sleep(5000);
6、但是我们将最后一句代码解开封印。这里有一个问题,当我们执行等待了5秒钟按道理过期了,但是没有触发删除回调,我截图上的是我将代码块拉到了之前执行的最后一行代码才有,这个我也蒙了。但是当我第二次进来访问就有回调函数了。
/// <summary> /// 删除回调函数 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="reason"></param> /// <param name="state"></param> public void CallbackFunction(object key, object value, EvictionReason reason, object state) { Console.WriteLine($"你被删除了key:{key},value:{value}回调函数"); }
7、CreateEntry设置缓存,有一点特殊正常设置缓存的时候,获取的时候是null,只有当我们执行Dispose或者using的时候才成功。带着这个疑问我们决定看看源码。
////CreateEntry设置缓存 var entity = Cache.CreateEntry("key5"); entity.Value = "5555"; Console.WriteLine("CreateEntry 获取信息:" + Cache.Get("key5")); //结果null
//方法1 var entity = Cache.CreateEntry("key5"); entity.Value = "5555"; entity.Dispose(); Console.WriteLine("CreateEntry 获取信息:" + Cache.Get("key5")); //结果5555 //方法2 using (var entity = Cache.CreateEntry("key5")) { entity.Value = "5555"; Console.WriteLine("CreateEntry 获取信息:" + Cache.Get("key5")); //结果5555 }
8、CacheEntry有一个重要的方法Dispose(),因为它继承IDisposable,在Dispost方法中调用了_notifyCacheEntryDisposed委托。
public void Dispose() { if (!_added) { _added = true; _scope.Dispose(); _notifyCacheEntryDisposed(this);//在此调用委托,而此委托是被MemoryCache类中的SetEntry赋值。目的是将CacheEntry实体放入MemoryCache类的字典中,也就是放入缓存中。 PropagateOptions(CacheEntryHelper.Current);
三、MemoryCache组件源码
1、首先看我们在Startup中声明添加的内存缓存的对应的源码。 services.AddMemoryCache();可以看到这里是将缓存组件使用单例模式注入我们程序中。
/// <summary> /// Adds a non distributed in memory implementation of <see cref="IMemoryCache"/> to the /// <see cref="IServiceCollection" />. /// </summary> /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param> /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> public static IServiceCollection AddMemoryCache(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.AddOptions(); services.TryAdd(ServiceDescriptor.Singleton<IMemoryCache, MemoryCache>()); return services; }
2、MemoryCacheOptions类:主要是配置一些参数
ExpirationScanFrequency:此字段表明隔一段时间扫描缓存,移除过期缓存,默认频率为一分钟。
SizeLimit:设置缓存的大小。
CompactionPercentage:压缩比例 默认为百分之五。
//添加内存缓存 services.AddMemoryCache(x=> { x.CompactionPercentage = 0.05;//缓存压缩大小 x.ExpirationScanFrequency= new TimeSpan(0, 0, 1); //默认自动扫描时间为一分钟 x.SizeLimit = 500 * 1024 * 1024; //大小为500M });
3、MemoryCache类
1:ConcurrentDictionary<object, CacheEntry> _entries:一个多线程安全的字典类型,其实缓存的本质就是这个字典,将所有缓存都放入这个字典中,然后通过字典的key(字典的key其实和缓存实体CacheEntry的key值一样)获取CacheEntry实体(CacheEntry实体包含key和value,也就是我们代码中设置的key和value)。
2:_expirationScanFrequency:Span类型,表示扫描过期缓存的频率时间,此字段的值来自MemoryCacheOptions类的ExpirationScanFrequency。需要注意的是:假如设置为一分钟扫描一次缓存集合,这个扫描不是主动扫描的,只有当对缓存集合操作时才会扫描。比如增删改查缓存集合的时候,会判断上一次扫描的时间离现在过去多久了,如果超过扫描设置的时间,才会扫描。
private void StartScanForExpiredItems()//扫描函数。在添加、查找、删除、修改缓存的时候,会调用此方法扫描过滤过期缓存,并不是主动隔一段时间执行 { var now = _clock.UtcNow; if (_expirationScanFrequency < now - _lastExpirationScan)//判断现在的时间和最后一次扫描时间,是不是大于设置的时间段 { _lastExpirationScan = now; Task.Factory.StartNew(state => ScanForExpiredItems((MemoryCache)state), this, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } } private static void ScanForExpiredItems(MemoryCache cache) { var now = cache._clock.UtcNow; foreach (var entry in cache._entries.Values)//然后遍历字典集合,移除过期缓存 { if (entry.CheckExpired(now)) { cache.RemoveEntry(entry); } } }
第一次,当它本可进取时,却故作谦卑;
第二次,当它空虚时,用爱欲来填充;
第三次,在困难和容易之间,它选择了容易;
第四次,它犯了错,却借由别人也会犯错来宽慰自己;
第五次,它自由软弱,却把它认为是生命的坚韧;
第六次,当它鄙夷一张丑恶的嘴脸时,却不知那正是自己面具中的一副;
第七次,它侧身于生活的污泥中虽不甘心,却又畏首畏尾。