环境:.net core2.2
nugt包依赖:

  • 1.Microsoft.Extensions.Caching.Abstractions
  • 2.Microsoft.Extensions.Caching.Memory

参考:
.Net Core缓存组件(MemoryCache)源码解析
拥抱.NET Core系列:MemoryCache 缓存过期

一、根据时间过期的四种策略

首先说下:一般我们使用缓存都是根据时间设置过期策略的,常用的是以下四种过期策略:

1.1 永不过期:

永远不会过期

1.2 设置绝对过期时间点:

到期后就失效

1.3 设置过期滑动窗口:

只要在窗口期内访问,它的过期时间就一直向后顺延一个窗口长度

1.4 滑动窗口+绝对过期时间点:

只要在窗口期内访问,它的过期时间就一直向后顺延一个窗口长度,但最长不能超过绝对过期时间点

二、测试四种时间过期策略的代码

通过以下代码验证上述的四种时间过期策略。

using Microsoft.Extensions.Caching.Memory;
using System;
using System.Threading.Tasks;

namespace ConsoleApp6
{
    class Program
    {
        public static IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
        static async Task Main(string[] args)
        {
            //await Test1();
            //await Test2();
            //await Test3();
            await Test4();
            Console.WriteLine("ok");
            Console.ReadLine();
        }

        /// <summary>
        /// 4.验证滑动窗口+绝对过期时间(这里的绝对过期时间是缓存从开始到结束最长的时间,滑动窗口表示在滑动时间内只要没访问就立马过期)
        /// </summary>
        /// <returns></returns>
        private async static Task Test4()
        {
            _cache.Set("xiaoming", "222", new MemoryCacheEntryOptions()
            {
                SlidingExpiration = TimeSpan.FromSeconds(1.5),
                AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(1000 * 3.5)
            });
            await Task.Delay(1000 * 1);
            Console.WriteLine("1s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("2s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("3s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("4s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("5s:" + _cache.Get("xiaoming"));
        }

        /// <summary>
        /// 3.验证滑动窗口过期
        /// </summary>
        /// <returns></returns>
        private async static Task Test3()
        {
            _cache.Set("xiaoming", "222", new MemoryCacheEntryOptions()
            {
                SlidingExpiration = TimeSpan.FromSeconds(1.5)
            });
            await Task.Delay(1000 * 1);
            Console.WriteLine("1s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("2s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("3s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("4s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("5s:" + _cache.Get("xiaoming"));
        }

        /// <summary>
        /// 2.验证绝对过期时间
        /// </summary>
        /// <returns></returns>
        private async static Task Test2()
        {
            _cache.Set("xiaoming", "222", TimeSpan.FromSeconds(3));
            await Task.Delay(1000 * 1);
            Console.WriteLine("1s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("2s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("3s:" + _cache.Get("xiaoming"));
            await Task.Delay(1000 * 1);
            Console.WriteLine("4s:" + _cache.Get("xiaoming"));
        }

        /// <summary>
        /// 1.验证永不过期
        /// </summary>
        private async static Task Test1()
        {
            _cache.Set("xiaoming", "222");
            await Task.Delay(1000 * 3);
            Console.WriteLine(_cache.Get("xiaoming"));
        }
    }
}

三、封装的帮助类CacheManager

为了更方便的操作缓存我封装了下面的缓存帮助类,能直接按照上述几种策略进行缓存。

using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;

namespace server.Models
{
    public class CacheManager
    {

        public static CacheManager Default = new CacheManager();

        private IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
        
        /// <summary>
        /// 取得缓存数据
        /// </summary>
        /// <typeparam name="T">类型值</typeparam>
        /// <param name="key">关键字</param>
        /// <returns></returns>
        public T Get<T>(string key)
        {
            if (string.IsNullOrWhiteSpace(key))
                throw new ArgumentNullException(nameof(key));
            T value;
            _cache.TryGetValue<T>(key, out value);
            return value;
        }

        /// <summary>
        /// 设置缓存(永不过期)
        /// </summary>
        /// <param name="key">关键字</param>
        /// <param name="value">缓存值</param>
        public void Set_NotExpire<T>(string key, T value)
        {
            if (string.IsNullOrWhiteSpace(key))
                throw new ArgumentNullException(nameof(key));

            T v;
            if (_cache.TryGetValue(key, out v))
                _cache.Remove(key);
            _cache.Set(key, value);
        }

        /// <summary>
        /// 设置缓存(滑动过期:超过一段时间不访问就会过期,一直访问就一直不过期)
        /// </summary>
        /// <param name="key">关键字</param>
        /// <param name="value">缓存值</param>
        public void Set_SlidingExpire<T>(string key, T value, TimeSpan span)
        {
            if (string.IsNullOrWhiteSpace(key))
                throw new ArgumentNullException(nameof(key));

            T v;
            if (_cache.TryGetValue(key, out v))
                _cache.Remove(key);
            _cache.Set(key, value, new MemoryCacheEntryOptions()
            {
                SlidingExpiration = span
            });
        }

        /// <summary>
        /// 设置缓存(绝对时间过期:从缓存开始持续指定的时间段后就过期,无论有没有持续的访问)
        /// </summary>
        /// <param name="key">关键字</param>
        /// <param name="value">缓存值</param>
        public void Set_AbsoluteExpire<T>(string key, T value, TimeSpan span)
        {
            if (string.IsNullOrWhiteSpace(key))
                throw new ArgumentNullException(nameof(key));

            T v;
            if (_cache.TryGetValue(key, out v))
                _cache.Remove(key);
            _cache.Set(key, value, span);
        }

        /// <summary>
        /// 设置缓存(绝对时间过期+滑动过期:比如滑动过期设置半小时,绝对过期时间设置2个小时,那么缓存开始后只要半小时内没有访问就会立马过期,如果半小时内有访问就会向后顺延半小时,但最多只能缓存2个小时)
        /// </summary>
        /// <param name="key">关键字</param>
        /// <param name="value">缓存值</param>
        public void Set_SlidingAndAbsoluteExpire<T>(string key, T value, TimeSpan slidingSpan, TimeSpan absoluteSpan)
        {
            if (string.IsNullOrWhiteSpace(key))
                throw new ArgumentNullException(nameof(key));

            T v;
            if (_cache.TryGetValue(key, out v))
                _cache.Remove(key);
            _cache.Set(key, value, new MemoryCacheEntryOptions()
            {
                SlidingExpiration = slidingSpan,
                AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(absoluteSpan.TotalMilliseconds)
            });
        }

        /// <summary>
        /// 移除缓存
        /// </summary>
        /// <param name="key">关键字</param>
        public void Remove(string key)
        {
            if (string.IsNullOrWhiteSpace(key))
                throw new ArgumentNullException(nameof(key));

            _cache.Remove(key);
        }

        /// <summary>
        /// 释放
        /// </summary>
        public void Dispose()
        {
            if (_cache != null)
                _cache.Dispose();
            GC.SuppressFinalize(this);
        }
    }
}

四、关于MemoryCacheOptions

这个类是在创建MemoryCache实例的时候的一个参数new MemoryCache(new MemoryCacheOptions()),那么它里面有几个属性还是要说明以下:

4.1 ExpirationScanFrequency:

表示定期扫描并移除过期缓存项的频率。默认的是1分钟。从字面上看应该是有一个定时器每个1分钟就会扫描,然而不是这样的,只有在你访问其中的缓存项的时候才检查上次的扫描时间,如果超过了1分钟就会重新扫描,由此可见它的最快扫描间隔是1分钟,如果你一直没访问缓存,那么它就一直不扫描。

4.2 SizeLimitSize

SizeLimit:表示缓存中允许的总内容大小。
Size:表示缓存项的大小。
注意,每个缓存项的大小不是自动计算出来的,而是你添加缓存项的时候指定的,如:_cache.Set<int>("name", 20, new MemoryCacheEntryOptions() { Size = 4 });

4.3 CompactionPercentage:

表示当缓存大小超出的时候压缩的比例,默认值0.05。
具体来说,你先要给MemoryCache设置一个SizeLimit,比如说100吧(默认是null,也就是不存在大小限制),这样当你每次添加缓存项的时候就会检查是否超出了大小(缓存项的Size之和>=SizeLimit),然后CompactionPercentage使用默认的0.05,也就是每次超出100后就会把MemoryCache压缩到95以下(100*(1-0.05))。压缩的时候先检查有没有过期的,然后再按照缓存项的优先级(每个缓存项可以设置优先级,代码为:_cache.Set<int>("name", 20, new MemoryCacheEntryOptions() { Size = 4, Priority = CacheItemPriority.Low });,默认优先级是Normal,总共四级:Low、Normal、High、NeverRemove)为顺序从低到高移除,直至容量降到95以下。

4.4 注意

一般我们创建缓存容器的时候代码写法是:new CacheManager(new MemoryCacheOptions()),此时并没有设置SizeLimit,所以就不存在压缩问题了。

五、关于MemoryCacheEntryOptions

5.1 时间过期策略相关的属性

AbsoluteExpiration、AbsoluteExpirationRelativeToNow、SlidingExpiration和ExpirationTokens这几个属性属于时间过期策略的,这里不再说明其作用。

5.2 Size和Priority

Size表示的是这个缓存项占的大小,在计算压缩的时候会用到。
Priority表示缓存项的权重(也叫做优先级),当缓存容器大小不够时就会从低到高逐个移除,直到容量满足压缩比例要求时为止。

5.3 ExpirationTokens和PostEvictionCallbacks

看《六、拓展内容》

六、拓展内容

6.1 自定义过期策略

上面说了根据时间过期的4中策略对于一般来说足够使用了,但是如果你不想根据时间决定是否过期的话,微软提供了一个“自定义过期策略”,就是在需要过期的时候你自己通知缓存容器去触发过期。
直接看一个应用的代码:

class Program
{
    public static IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
    static async Task Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        var token = new CancellationChangeToken(cts.Token);
        var entry = _cache.CreateEntry("key");
        entry.Value = "小明";
        entry.ExpirationTokens.Add(token);
        entry.Dispose();//将entry放到缓存到_cache容器中
        Console.WriteLine("key=" + _cache.Get<string>("key"));
        cts.Cancel();//通知容器,关联的缓存项已过期
        Console.WriteLine("key=" + _cache.Get<string>("key"));
        Console.ReadLine();
     }
}

观察输出结果:
在这里插入图片描述
从上面可以看出,我们手动让缓存项“key”过期了,但是要注意用于缓存过期的token只能使用一次(参考:.netcore入门17:IChangeToken和ChangeToken用法简介)。
那么下面有个需求:我想监测配置文件的内容,并将它缓存到程序中怎么实现呢?(注意:不使用缓存完全可以,这里是为了演示自定义过期策略)
看如下代码:

class Program
{
    public static IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
    static async Task Main(string[] args)
    {
        string _getConfig()
        {
            string dirPath = "c:\\temp";
            string fileName = "demo.txt";
            var fileProvider = new PhysicalFileProvider(dirPath);
            string str = _cache.GetOrCreate<string>("text", entry =>
              {
                  entry.AddExpirationToken(fileProvider.Watch(fileName));
                  return File.ReadAllText(Path.Combine(dirPath, fileName));
              });
            return str;
        }
        while (true)
        {
            Console.WriteLine("str=" + _getConfig());
            Thread.Sleep(2000);
        }
    }
}

执行效果如下:
在这里插入图片描述
我们可以看到:每当文件内容发生变化时就会触发fileProvider.Watch(fileName)返回的token发生变化,从而引起过期,当下次访问_getConfig()时又会有一个新的token被创建用于监测文件改变并且将读取到的文件内容缓存到了系统中。

6.2 缓存过期回调

很多时候我们希望缓存过期之后能做一些事情,比如重新写入缓存等等,MemoryCache提供了这样的机制。
直接看示例代码:

class Program
{
    public static IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
    static async Task Main(string[] args)
    {
        var entry = _cache.CreateEntry("key");
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(1);
        entry.RegisterPostEvictionCallback((key, value, reason, state) =>
        {
            Console.WriteLine("key=" + key);
            Console.WriteLine("value=" + value);
            Console.WriteLine("reason=" + reason);
            Console.WriteLine("state=" + state);
        }, "statestr");
        entry.Dispose();//将entry缓存到_cache中
        Thread.Sleep(1500);
        _cache.Get("key");
        Console.ReadLine();
    }
}

执行效果:
在这里插入图片描述
从上面可以看出,缓存失效的话会触发回调。但是要注意:MemoryCache中没有定时器,只有我们访问缓存时才可能会触发过期检查,进而执行我们注册的回调!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐