如何从.NET MemoryCache中过期许多项目

时间:2013-09-26 01:17:01

标签: .net caching memorycache

从MemoryCache实例中删除大量项目的推荐方法是什么?

基于围绕this question的讨论,似乎首选方法是为整个应用程序使用单个缓存,并使用名称空间来使密钥允许多个逻辑类型的项目缓存在同一个实例中。

但是,使用单个缓存实例会导致从缓存中过期(删除)大量项目的问题。特别是在某种逻辑类型的所有项目都必须过期的情况下。

目前我找到的唯一解决方案是基于answer to this question但是从性能观点来看确实不是很好,因为您必须枚举缓存中的所有键,并测试命名空间,这可能非常耗时!

目前我提出的唯一解决方法是使用版本号为缓存中的所有对象创建一个瘦包装器,每当访问一个对象时,如果缓存版本与之不匹配,则丢弃它当前版本。因此,每当我需要清除某种类型的所有项目时,我都会提高当前的版本号,使所有缓存的项目无效。

上面的解决方案看起来非常可靠。但我不禁想知道是否有更直接的方法来实现同样的目标?

这是我目前的实施:

private class MemCacheWrapper<TItemType> 
              where TItemType : class
{            
  private int _version;
  private Guid _guid;
  private System.Runtime.Caching.ObjectCache _cache;

  private class ThinWrapper
  {
     public ThinWrapper(TItemType item, int version)
     {
        Item = item;
        Version = version;
     }

     public TItemType Item { get; set; }
     public int Version { get; set; }
  }

  public MemCacheWrapper()
  {
      _cache = System.Runtime.Caching.MemoryCache.Default;
      _version = 0;
      _guid = Guid.NewGuid();
  }

  public TItemType Get(int index)
  {                
     string key = string.Format("{0}_{1}", _guid, index);

     var lvi = _cache.Get(key) as ThinWrapper;

     if (lvi == null || lvi.Version != _version)
     {
         return null;
     }

     return lvi.Item;
  }

  public void Put(int index, TItemType item)
  {                
     string key = string.Format("{0}_{1}", _guid, index);

     var cip = new System.Runtime.Caching.CacheItemPolicy();
     cip.SlidingExpiration.Add(TimeSpan.FromSeconds(30));

     _cache.Set(key, new ThinWrapper(item, _version), cip);
  }

  public void Clear()
  {
     _version++;                
  }
}

4 个答案:

答案 0 :(得分:8)

我的建议从MemoryCache实例中删除大量项目的方法是使用ChangeMonitor,尤其是CacheEntryChangeMonitor

  

提供一个表示可以的ChangeMonitor类型的基类   实施以监控缓存条目的变化。

因此,它允许我们处理缓存项之间的依赖关系。

一个基本的例子是

    var cache = MemoryCache.Default;
    cache.Add("mycachebreakerkey", "mycachebreakerkey", DateTime.Now.AddSeconds(15));

    CacheItemPolicy policy = new CacheItemPolicy();
    policy.ChangeMonitors.Add(cache.CreateCacheEntryChangeMonitor(new string[] { "mycachebreakerkey" }));
    // just to debug removal
    policy.RemovedCallback = args => { Debug.WriteLine(args.CacheItem.Key + "-->" + args.RemovedReason); };
    cache.Add("cacheKey", "cacheKey", policy);

    // after 15 seconds mycachebreakerkey will expire
    // dependent item "cacheKey" will also be removed

对于大多数事情,您还可以创建自定义缓存实现或派生的更改监视器类型。

未经过测试,但CreateCacheEntryChangeMonitor建议您可以在MemoryCache之间创建依赖关系。

修改

ChangeMonitor是使运行时缓存中的内容无效的.net方法。无效表示此处=从缓存中删除。它由SqlDependency或一些asp.net组件用于监视文件更改。所以,我认为这个解决方案是可扩展的。

这是一个非常简单的基准测试,在我的笔记本电脑上运行。

        const int NbItems = 300000;

        var watcher = Stopwatch.StartNew();
        var cache = MemoryCache.Default;

        var breakerticks = 0L;
        var allticks = new List<long>();

        cache.Add("mycachebreakerkey", "mycachebreakerkey", new CacheItemPolicy() { RemovedCallback = args => { breakerticks = watcher.ElapsedTicks; } });

        foreach (var i in Enumerable.Range(1, NbItems))
        {
            CacheItemPolicy policy = new CacheItemPolicy();
            if (i % 4 == 0)
                policy.ChangeMonitors.Add(cache.CreateCacheEntryChangeMonitor(new string[] { "mycachebreakerkeyone" }));
            policy.RemovedCallback = args => { allticks.Add(watcher.ElapsedTicks); };// just to debug removal
            cache.Add("cacheKey" + i.ToString(), "cacheKey", policy);
        }

        cache.Remove("mycachebreakerkey");
        Trace.WriteLine("Breaker removal=>" + TimeSpan.FromTicks(breakerticks).TotalMilliseconds);
        Trace.WriteLine("Start removal=>" + TimeSpan.FromTicks(allticks.Min()).TotalMilliseconds);
        Trace.WriteLine("End removal=>" + TimeSpan.FromTicks(allticks.Max()).TotalMilliseconds);
        Trace.WriteLine(cache.GetCount());

        // Trace
        // Breaker removal: 225,8062 ms
        // Start removal: 0,251 ms
        // End removal: 225,7688 ms
        // 225000 items

因此,需要225毫秒才能删除30万件30%的物品(再次放在我的笔记本电脑上,3年前)。你真的需要更快的东西吗?请注意,父项最后会被删除。这个解决方案的优点:

  • 无效项目将从缓存中删除
  • 你接近缓存(减少callstack,减少强制转换,减少间接)
  • 删除回调允许您在需要时自动重新加载缓存项
  • 如果缓存破解程序过期,则回调是在另一个不会影响asp.net请求的线程上。

我发现您的实施是相关的,并会在以后记住它。您的选择应该基于您的场景:项目数量,缓存项目的大小,命中率,依赖项数量......还要保留太多数据,缓存通常很慢并且可能会增加驱逐的可能性。

答案 1 :(得分:2)

查看this帖子,特别是Thomas F. Abraham发布的答案。 它有一个解决方案,使您可以清除整个缓存或命名子集。

这里的关键是:

// Cache objects are obligated to remove entry upon change notification.
base.OnChanged(null);

我自己实现了这一点,而且一切似乎都运行良好。

答案 2 :(得分:1)

Cyber​​maxs的基准示例非常棒。但它有一个不准确的地方。 在

H000 = 100 H001 = 200 H002 = 300;

缓存键“mycachebreakerkeyone”shuld为“mycachebreakerkey”。 由于这个错误,25%的项目在添加到缓存后就被删除了。他们不等待删除“父”“mycachebreakerkey”删除。

答案 3 :(得分:0)

如果您使用针对.NET Standard的“ MemoryCache”中的“ Microsoft.Extensions.Caching.Abstractions”实现,则可以使用CancellationToken s使缓存条目失效。

在创建缓存条目时,可以将CancellationToken与之关联。

例如,您可以创建一个CancellationToken“ A”并将其与一组条目关联,并将CancellationToken“ B”与另一组条目关联。取消CancellationToken“ A”时,与其关联的所有条目都会自动失效。

您可以运行下面的示例代码来了解其工作原理。

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

namespace Sample
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var cache = new MemoryCache(new MemoryCacheOptions());
            var evenAgeCts = new CancellationTokenSource();
            var oddAgeCts = new CancellationTokenSource();

            var students = new[]
            {
                new Student() { Name = "James", Age = 22 },
                new Student() { Name = "John", Age = 24 },
                new Student() { Name = "Robert", Age = 19 },
                new Student() { Name = "Mary", Age = 20 },
                new Student() { Name = "Patricia", Age = 39 },
                new Student() { Name = "Jennifer", Age = 19 },
            };


            Console.WriteLine($"Total cache entries: {cache.Count}");

            foreach (var student in students)
            {
                AddToCache(student, student.Name, cache, student.Age % 2 == 0 ? evenAgeCts.Token : oddAgeCts.Token);
            }

            Console.WriteLine($"Total cache entries (after adding students): {cache.Count}");

            evenAgeCts.Cancel();
            Console.WriteLine($"Even aged students cancellation token was cancelled!");
            Thread.Sleep(250);

            Console.WriteLine($"Total cache entries (after deleting Student): {cache.Count}");

            oddAgeCts.Cancel();
            Console.WriteLine($"Odd aged students cancellation token was cancelled!");
            Thread.Sleep(250);

            Console.WriteLine($"Total cache entries (after deleting Bar): {cache.Count}");
        }

        private static void AddToCache<TEntry>(TEntry entry, string key, IMemoryCache cache, CancellationToken ct)
        {
            cache.GetOrCreate($"{entry.GetType().Name}\t{key}", e =>
            {
                e.RegisterPostEvictionCallback(PostEvictionCallback);
                e.AddExpirationToken(new CancellationChangeToken(ct));

                return entry;
            });
        }

        private static void PostEvictionCallback(object key, object value, EvictionReason reason, object state)
        {
            var student = (Student)value;

            Console.WriteLine($"Cache invalidated because of {reason} - {student.Name} : {student.Age}");
        }
    }

    public class Student
    {
        public string Name { get; set; }

        public int Age { get; set; }
    }
}

在示例中,为简单起见,我使用了扩展方法“ IMemoryCache.GetOrCreate”。您可以使用方法“ IMemoryCache.CreateEntry”轻松实现相同的目标。