【问题标题】:How to Expire Many Items From a .NET MemoryCache如何使 .NET MemoryCache 中的许多项目过期
【发布时间】:2013-09-26 01:17:01
【问题描述】:

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

根据围绕this question 的讨论,似乎首选方法是为整个应用程序使用单个缓存,并使用 namespaces 作为键以允许缓存多种逻辑类型的项目同一个实例。

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

目前我找到的唯一解决方案是基于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++;                
  }
}

【问题讨论】:

    标签: .net caching memorycache


    【解决方案1】:

    我的推荐的从 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
    

    因此,删除 300 000 个项目中的 25% 需要 225 毫秒(再次在我的笔记本电脑上,3 岁前)。你真的需要更快的东西吗?请注意,父级在最后被删除。该解决方案的优势:

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

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

    【讨论】:

    • 我不认为这个解决方案是可扩展的。如果您需要主动使缓存中的大量项目无效(比如数十万缓存中的 20 - 30% 的项目)
    • +1 用于提供基准。我认为您的解决方案可能正是我一直在寻找的。我将尝试自己运行一些基准测试,看看它是如何扩展的(即从 10,000 个项目到 100,000 个到 1,000,000 个到 10,000,000 个项目)。显然,从内存的角度来看,我使用的版本控制方法不是最佳的
    【解决方案2】:

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

    这里的关键是:

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

    我自己实现了这个,一切似乎都很好。

    【讨论】:

      【解决方案3】:

      如果您使用“Microsoft.Extensions.Caching.Abstractions”中针对 .NET Standard 的“MemoryCache”实现,您可以使用 CancellationTokens 使缓存条目过期。

      创建缓存条目时,您可以将 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”轻松实现相同的目标。

      【讨论】:

        【解决方案4】:

        Cyber​​maxs 的基准示例非常棒。但它有一个不准确之处。 在线上

        policy.ChangeMonitors.Add(cache.CreateCacheEntryChangeMonitor(new string[] { "mycachebreakerkeyone" }));`
        

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

        【讨论】:

          猜你喜欢
          • 2013-06-03
          • 2016-02-15
          • 2018-05-10
          • 2019-08-13
          • 2014-04-06
          • 2012-11-21
          • 2014-05-21
          • 1970-01-01
          • 2011-10-16
          相关资源
          最近更新 更多