5

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

根据围绕这个问题的讨论,似乎首选方法是为整个应用程序使用单个缓存,并为键使用命名空间,以允许在同一实例中缓存多个逻辑类型的项目。

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

目前我找到的唯一解决方案是基于这个问题的答案,但从性能的角度来看,它真的不是很好,因为你必须枚举缓存中的所有键,并测试命名空间,这可能是相当耗时的!

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

上面的解决方法似乎很可靠。但我不禁想知道是否没有更直接的方法来完成同样的事情?

这是我目前的实现:

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

4 回答 4

11

推荐的从 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 岁前)。你真的需要更快的东西吗?请注意,父级在最后被删除。该解决方案的优势:

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

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

于 2013-12-17T14:44:01.183 回答
2

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

这里的关键是:

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

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

于 2014-04-03T10:51:25.687 回答
2

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

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

于 2019-04-10T21:42:20.800 回答
1

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

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

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

于 2017-03-11T09:33:38.153 回答