Как да изтече много елементи от .NET MemoryCache

Какъв е препоръчителният начин за премахване на голям брой елементи от екземпляр на 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++;                
  }
}

person Mike Dinescu    schedule 26.09.2013    source източник


Отговори (4)


Моят препоръчван начин за премахване на голям брой елементи от екземпляр на 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 ms за премахване на 25% от моите 300 000 елемента (отново на моя лаптоп, на 3 години). Наистина ли имате нужда от нещо по-бързо? Обърнете внимание, че родителят се премахва в края. Предимство на това решение:

  • невалидните елементи се премахват от кеша
  • вие сте близо до кеша (по-малко callstack, по-малко cast, по-малко indirection)
  • премахването на обратното извикване ви позволява да презаредите автоматично кеш елемент, ако е необходимо
  • ако кешбрейкърът изтече, тогава обратното извикване е в друга нишка, което няма да повлияе на asp.net заявките.

Намирам вашето внедряване за уместно и ще го имам предвид за по-късно. Вашият избор трябва да се основава на вашия сценарий: брой елементи, размер на елемента в кеша, коефициент на попадения, брой зависимости, ... също така запазването на твърде много данни означава, че кешът обикновено е бавен и може да увеличи вероятността от изгонване.

person Cybermaxs    schedule 17.12.2013
comment
Не мисля, че това решение е мащабируемо. Ако трябва активно да обезсилите голям брой елементи от кеша (да речем 20 - 30% от елементите в кеш от стотици хиляди) - person Mike Dinescu; 17.12.2013
comment
+1 за предоставяне на еталон. Мисля, че вашето решение може да е това, което търсех в крайна сметка. Ще се опитам сам да направя някои сравнителни тестове, за да видя как това се мащабира (т.е. преминаване от 10 000 елемента до 100 000 до 1 000 000 до 10 000 000 елемента). Очевидно подходът за създаване на версии, който използвах, е неоптимален от гледна точка на паметта - person Mike Dinescu; 18.12.2013

Вижте тази публикация и по-конкретно отговора което Томас Ф. Ейбрахам публикува. Има решение, което ви позволява да изчистите целия кеш или наименувано подмножество.

Ключовото тук е:

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

Приложих това сам и изглежда всичко работи добре.

person Jowen    schedule 03.04.2014

Ако използвате "MemoryCache " изпълнение от "Microsoft.Extensions.Caching.Abstractions", който е насочен към .NET Standard, можете да изтечете записите в кеша, като използвате 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".

person Marco Thomazini    schedule 10.04.2019

Примерът за сравнение на Cybermaxs е страхотен. Но има една неточност. На линията

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

кеш ключът "mycachebreakerkeyone" трябва да бъде "mycachebreakerkey". Поради тази грешка 25% от елементите се изтриват веднага след добавянето им в кеша. Те не чакат изтриването на "родител" "mycachebreakerkey", за да бъдат изтрити.

person Dmitry Zuev    schedule 11.03.2017