MemoryCache AbsoluteExpiration 行为奇怪

Posted

技术标签:

【中文标题】MemoryCache AbsoluteExpiration 行为奇怪【英文标题】:MemoryCache AbsoluteExpiration acting strange 【发布时间】:2012-09-19 18:11:05 【问题描述】:

我正在尝试在 .net 4.5 中使用 MemoryCache 来跟踪并自动更新各种项目,但似乎无论我设置为 AbsoluteExpiration 什么,它都只会在 15 秒内过期或更多。

我希望缓存项每 5 秒过期一次,但它总是在至少 15 秒后过期,如果我将过期时间移出,它最终会是 15 秒 + 我的刷新间隔,但绝不会更少超过 15 秒。

是否有一些我没有看到的内部计时器分辨率?我查看了一些反映的 System.Runtime.Caching.MemoryCache 代码,但没有发现任何问题,而且我无法在互联网上找到其他有此问题的人。

我在下面有一个非常基本的例子来说明这个问题。

我想要CacheEntryUpdate 每 5 秒左右被点击一次并更新新数据,但正如我所说,它只会在 15 秒以上被点击。

static MemoryCache MemCache;
static int RefreshInterval = 5000;

protected void Page_Load(object sender, EventArgs e)

    if (MemCache == null)
        MemCache = new MemoryCache("MemCache");

    if (!MemCache.Contains("cacheItem"))
    
        var cacheObj = new object();
        var policy = new CacheItemPolicy
        
            UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
            AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
        ;
        var cacheItem = new CacheItem("cacheItem", cacheObj);
        MemCache.Set("cacheItem", cacheItem, policy);
    


private void CacheEntryUpdate(CacheEntryUpdateArguments args)

    var cacheItem = MemCache.GetCacheItem(args.Key);
    var cacheObj = cacheItem.Value;

    cacheItem.Value = cacheObj;
    args.UpdatedCacheItem = cacheItem;
    var policy = new CacheItemPolicy
    
        UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
        AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
    ;
    args.UpdatedCacheItemPolicy = policy;

【问题讨论】:

为什么要在这么短的时间内缓存对象?如果要在 5-15 秒内驱逐对象,那么使用缓存似乎没有多大价值。 我发现了更奇怪的行为:如果你在 CacheItemPolicy 上设置 RemovedCallback,延迟会减少到 10 秒 更新我之前的评论:并非总是 10 秒,- 有时是 0,有时是 20。但通常正好是 10 秒 【参考方案1】:

我想通了。 System.Runtime.Caching.CacheExpires 上有一个名为 _tsPerBucket 的 internal static readonly TimeSpan,它在 20 秒时硬编码。

显然,此字段用于内部计时器运行并检查缓存项是否已过期。

我正在通过使用反射覆盖该值并清除默认 MemoryCache 实例以重置所有内容来解决此问题。它似乎工作,即使它是一个巨大的黑客。

这是更新后的代码:

static MemoryCache MemCache;
static int RefreshInterval = 1000;

protected void Page_Load(object sender, EventArgs e)

    if (MemCache == null)
    
        const string assembly = "System.Runtime.Caching, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
        var type = Type.GetType("System.Runtime.Caching.CacheExpires, " + assembly, true, true);
        var field = type.GetField("_tsPerBucket", BindingFlags.Static | BindingFlags.NonPublic);
        field.SetValue(null, TimeSpan.FromSeconds(1));

        type = typeof(MemoryCache);
        field = type.GetField("s_defaultCache", BindingFlags.Static | BindingFlags.NonPublic);
        field.SetValue(null, null);

        MemCache = new MemoryCache("MemCache");
    

    if (!MemCache.Contains("cacheItem"))
    
        var cacheObj = new object();
        var policy = new CacheItemPolicy
        
            UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
            AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
        ;
        var cacheItem = new CacheItem("cacheItem", cacheObj);
        MemCache.Set("cacheItem", cacheItem, policy);
    


private void CacheEntryUpdate(CacheEntryUpdateArguments args)

    var cacheItem = MemCache.GetCacheItem(args.Key);
    var cacheObj = cacheItem.Value;

    cacheItem.Value = cacheObj;
    args.UpdatedCacheItem = cacheItem;
    var policy = new CacheItemPolicy
    
        UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
        AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
    ;
    args.UpdatedCacheItemPolicy = policy;

【讨论】:

无法让它工作,谁能提供一些上下文?我不明白这段代码需要放在哪里才能工作。谢谢! @kmdsax 代码应该存在于您的缓存所在的任何地方?在示例中,我只是将它放在了一个 aspx 页面中。实际上,我们有一个单例缓存对象,它在初始化时运行上面的 if (MemCache == null) 块代码。反射代码应该在它所在的应用程序的生命周期内应用更新的计时器分辨率,所以你应该只需要在缓存开始时运行它 轮询间隔似乎是可配置的,见这里:msdn.microsoft.com/en-us/library/… 您应该更新轮询间隔而不是使用反射写入私有字段...【参考方案2】:

您是否愿意/能够从旧的 System.Runtime.Caching 更改为新的 Microsft.Extensions.Caching? 1.x 版支持 netstandard 1.3 和 net451。如果是这样,那么改进后的 API 将支持您所描述的用法,而无需使用反射。

MemoryCacheOptions 对象有一个属性 ExpirationScanFrequency 可以让你控制缓存清理的扫描频率,见https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions.expirationscanfrequency?view=aspnetcore-2.0

请注意,不再基于计时器过期(这是性能设计决定),因此现在内存压力或为缓存项目调用基于 Get() 的方法之一现在是过期的触发器。但是,您可以使用取消令牌强制基于时间的到期,请参阅此 SO 答案以获取示例 https://***.com/a/47949111/3140853。

【讨论】:

我已经很久没有接触过这些了,但如果这是实现我最初要求的最佳方式,那么我愿意接受你的解决方案??‍♂️ 【参考方案3】:

对于 MatteoSp - 配置中的 pollingInterval 或构造函数中的 NameValueCollection 是不同的计时器。这是一个间隔,调用时将使用其他两个配置属性来确定内存是否处于需要使用 Trim 方法删除条目的级别。

【讨论】:

在这里也有同样的经历。例如,将轮询间隔更改为一秒不会影响从缓存中删除或更新项目的时间。【参考方案4】:

基于@Jared 回答的更新版本。修改默认的MemoryCache实例,这里新建一个。

class FastExpiringCache

    public static MemoryCache Default  get;  = Create();

    private static MemoryCache Create()
    
        MemoryCache instance = null;
        Assembly assembly = typeof(CacheItemPolicy).Assembly;
        Type type = assembly.GetType("System.Runtime.Caching.CacheExpires");
        if( type != null)
        
            FieldInfo field = type.GetField("_tsPerBucket", BindingFlags.Static | BindingFlags.NonPublic);
            if(field != null && field.FieldType == typeof(TimeSpan))
            
                TimeSpan originalValue = (TimeSpan)field.GetValue(null);
                field.SetValue(null, TimeSpan.FromSeconds(3));
                instance = new MemoryCache("FastExpiringCache");
                field.SetValue(null, originalValue); // reset to original value
            
        
        return instance ?? new MemoryCache("FastExpiringCache");
    

【讨论】:

以上是关于MemoryCache AbsoluteExpiration 行为奇怪的主要内容,如果未能解决你的问题,请参考以下文章

MemoryCache.AddOrGetExisting 有啥用?

MemoryCache缓存 ---缓存时效

MemoryCache - 防止项目过期

如何分离 MemoryCache 上的对象引用

MemoryCache.Default 在 .NET Core 中不可用?

如何清除 System.Runtime.Caching.MemoryCache