MemoryCache с поддержкой регионов?

Мне нужно добавить функциональность кеша, и я нашел новый блестящий класс под названием MemoryCache. Тем не менее, я нахожу MemoryCache немного ущербным (мне нужна функциональность регионов). Среди прочего мне нужно добавить что-то вроде ClearAll(region). Авторы приложили большие усилия, чтобы сохранить этот класс без поддержки регионов, например:

if (regionName != null)
{
throw new NotSupportedException(R.RegionName_not_supported);
}

слетает практически во всех методах. Я не вижу простого способа переопределить это поведение. Единственный способ добавить поддержку региона, о котором я могу думать, — это добавить новый класс как оболочку MemoryCache, а не как класс, наследуемый от MemoryCache. Затем в этом новом классе создайте словарь и позвольте каждому методу «буферизировать» вызовы региона. Звучит противно и неправильно, но в конце концов...

Знаете ли вы лучшие способы добавления регионов в MemoryCache?


person IamDeveloper    schedule 25.01.2012    source источник
comment
Кто-нибудь знает ПОЧЕМУ? Этот проклятый объект MemoryCache завел меня в кроличью нору исследований, когда я думал, что это займет 5 минут!   -  person Simon_Weaver    schedule 12.01.2019
comment
Также это может представлять интерес: >codereview.stackexchange.com/questions/48148/   -  person Simon_Weaver    schedule 12.01.2019


Ответы (4)


Я знаю, что вы давно не задавали этот вопрос, так что на самом деле это не ответ вам, а скорее дополнение для будущих читателей.

Я также был удивлен, обнаружив, что стандартная реализация MemoryCache НЕ поддерживает регионы. Было бы так легко предоставить сразу. Поэтому я решил обернуть MemoryCache в свой собственный простой класс, чтобы обеспечить те функции, которые мне часто нужны.

Я прилагаю свой код здесь, чтобы сэкономить время для других, у которых такая же потребность!

/// <summary>
/// =================================================================================================================
/// This is a static encapsulation of the Framework provided MemoryCache to make it easier to use.
/// - Keys can be of any type, not just strings.
/// - A typed Get method is provided for the common case where type of retrieved item actually is known.
/// - Exists method is provided.
/// - Except for the Set method with custom policy, some specific Set methods are also provided for convenience.
/// - One SetAbsolute method with remove callback is provided as an example.
///   The Set method can also be used for custom remove/update monitoring.
/// - Domain (or "region") functionality missing in default MemoryCache is provided.
///   This is very useful when adding items with identical keys but belonging to different domains.
///   Example: "Customer" with Id=1, and "Product" with Id=1
/// =================================================================================================================
/// </summary>
public static class MyCache
{
    private const string KeySeparator = "_";
    private const string DefaultDomain = "DefaultDomain";


    private static MemoryCache Cache
    {
        get { return MemoryCache.Default; }
    }

    // -----------------------------------------------------------------------------------------------------------------------------
    // The default instance of the MemoryCache is used.
    // Memory usage can be configured in standard config file.
    // -----------------------------------------------------------------------------------------------------------------------------
    // cacheMemoryLimitMegabytes:   The amount of maximum memory size to be used. Specified in megabytes. 
    //                              The default is zero, which indicates that the MemoryCache instance manages its own memory
    //                              based on the amount of memory that is installed on the computer. 
    // physicalMemoryPercentage:    The percentage of physical memory that the cache can use. It is specified as an integer value from 1 to 100. 
    //                              The default is zero, which indicates that the MemoryCache instance manages its own memory 
    //                              based on the amount of memory that is installed on the computer. 
    // pollingInterval:             The time interval after which the cache implementation compares the current memory load with the 
    //                              absolute and percentage-based memory limits that are set for the cache instance.
    //                              The default is two minutes.
    // -----------------------------------------------------------------------------------------------------------------------------
    //  <configuration>
    //    <system.runtime.caching>
    //      <memoryCache>
    //        <namedCaches>
    //          <add name="default" cacheMemoryLimitMegabytes="0" physicalMemoryPercentage="0" pollingInterval="00:02:00" />
    //        </namedCaches>
    //      </memoryCache>
    //    </system.runtime.caching>
    //  </configuration>
    // -----------------------------------------------------------------------------------------------------------------------------



    /// <summary>
    /// Store an object and let it stay in cache until manually removed.
    /// </summary>
    public static void SetPermanent(string key, object data, string domain = null)
    {
        CacheItemPolicy policy = new CacheItemPolicy { };
        Set(key, data, policy, domain);
    }

    /// <summary>
    /// Store an object and let it stay in cache x minutes from write.
    /// </summary>
    public static void SetAbsolute(string key, object data, double minutes, string domain = null)
    {
        CacheItemPolicy policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(minutes) };
        Set(key, data, policy, domain);
    }

    /// <summary>
    /// Store an object and let it stay in cache x minutes from write.
    /// callback is a method to be triggered when item is removed
    /// </summary>
    public static void SetAbsolute(string key, object data, double minutes, CacheEntryRemovedCallback callback, string domain = null)
    {
        CacheItemPolicy policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(minutes), RemovedCallback = callback };
        Set(key, data, policy, domain);
    }

    /// <summary>
    /// Store an object and let it stay in cache x minutes from last write or read.
    /// </summary>
    public static void SetSliding(object key, object data, double minutes, string domain = null)
    {
        CacheItemPolicy policy = new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(minutes) };
        Set(key, data, policy, domain);
    }

    /// <summary>
    /// Store an item and let it stay in cache according to specified policy.
    /// </summary>
    /// <param name="key">Key within specified domain</param>
    /// <param name="data">Object to store</param>
    /// <param name="policy">CacheItemPolicy</param>
    /// <param name="domain">NULL will fallback to default domain</param>
    public static void Set(object key, object data, CacheItemPolicy policy, string domain = null)
    {
        Cache.Add(CombinedKey(key, domain), data, policy);
    }




    /// <summary>
    /// Get typed item from cache.
    /// </summary>
    /// <param name="key">Key within specified domain</param>
    /// <param name="domain">NULL will fallback to default domain</param>
    public static T Get<T>(object key, string domain = null)
    {
        return (T)Get(key, domain);
    }

    /// <summary>
    /// Get item from cache.
    /// </summary>
    /// <param name="key">Key within specified domain</param>
    /// <param name="domain">NULL will fallback to default domain</param>
    public static object Get(object key, string domain = null)
    {
        return Cache.Get(CombinedKey(key, domain));
    }

    /// <summary>
    /// Check if item exists in cache.
    /// </summary>
    /// <param name="key">Key within specified domain</param>
    /// <param name="domain">NULL will fallback to default domain</param>
    public static bool Exists(object key, string domain = null)
    {
        return Cache[CombinedKey(key, domain)] != null;
    }

    /// <summary>
    /// Remove item from cache.
    /// </summary>
    /// <param name="key">Key within specified domain</param>
    /// <param name="domain">NULL will fallback to default domain</param>
    public static void Remove(object key, string domain = null)
    {
        Cache.Remove(CombinedKey(key, domain));
    }



    #region Support Methods

    /// <summary>
    /// Parse domain from combinedKey.
    /// This method is exposed publicly because it can be useful in callback methods.
    /// The key property of the callback argument will in our case be the combinedKey.
    /// To be interpreted, it needs to be split into domain and key with these parse methods.
    /// </summary>
    public static string ParseDomain(string combinedKey)
    {
        return combinedKey.Substring(0, combinedKey.IndexOf(KeySeparator));
    }

    /// <summary>
    /// Parse key from combinedKey.
    /// This method is exposed publicly because it can be useful in callback methods.
    /// The key property of the callback argument will in our case be the combinedKey.
    /// To be interpreted, it needs to be split into domain and key with these parse methods.
    /// </summary>
    public static string ParseKey(string combinedKey)
    {
        return combinedKey.Substring(combinedKey.IndexOf(KeySeparator) + KeySeparator.Length);
    }

    /// <summary>
    /// Create a combined key from given values.
    /// The combined key is used when storing and retrieving from the inner MemoryCache instance.
    /// Example: Product_76
    /// </summary>
    /// <param name="key">Key within specified domain</param>
    /// <param name="domain">NULL will fallback to default domain</param>
    private static string CombinedKey(object key, string domain)
    {
        return string.Format("{0}{1}{2}", string.IsNullOrEmpty(domain) ? DefaultDomain : domain, KeySeparator, key);
    }

    #endregion

}
person Jakob Lithner    schedule 22.11.2013
comment
Перечисление через MemoryCache неэффективно, так как на время будет заблокирован весь кеш. Кроме того, ваш Clear() - это линейный поиск, поэтому он линейно ухудшается с количеством элементов кеша. Это лучшее решение: stackoverflow.com/a/22388943/220230 - person Piedone; 15.10.2014
comment
Спасибо, что заметили это. В данном простом примере я удалил метод Clear, чтобы не вводить других в заблуждение. Для тех, кому действительно нужен способ ручного удаления по регионам, я обращаюсь к данной ссылке. - person Jakob Lithner; 16.10.2014

Вы можете создать более одного экземпляра MemoryCache, по одному для каждого раздела ваших данных.

http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache.aspx :

вы можете создать несколько экземпляров класса MemoryCache для использования в одном приложении и в одном экземпляре AppDomain

person Evgeniy Berezovsky    schedule 10.02.2012
comment
где лучше всего создавать экземпляры всех этих новых экземпляров MemoryCache? Есть ли поставщик MemoryCache, с помощью которого вы можете управлять всеми этими экземплярами? - person Henley; 01.05.2013
comment
@HenleyChiu Я не думаю, что в базовых библиотеках что-то есть. Просто используйте стандартные средства для обмена состоянием, например, например. статическая, глобально видимая ConcurrentDictionary‹string, MemoryCache› - person Evgeniy Berezovsky; 02.09.2013
comment
В некоторых случаях использование нескольких экземпляров MemoryCache может снизить эффективность кэширования. См.: stackoverflow.com/questions/8463962/ - person Nathan; 14.10.2014
comment
Внедрение зависимостей с помощью IMemoryCache предназначено для использования в качестве синглтона. - person Simon_Weaver; 12.01.2019

Я только недавно столкнулся с этой проблемой. Я знаю, что это старый вопрос, но, возможно, это может быть полезно для некоторых людей. Вот моя итерация решения Thomas F. Abraham

namespace CLRTest
{
    using System;
    using System.Collections.Concurrent;
    using System.Diagnostics;
    using System.Globalization;
    using System.Linq;
    using System.Runtime.Caching;

    class Program
    {
        static void Main(string[] args)
        {
            CacheTester.TestCache();
        }
    }

    public class SignaledChangeEventArgs : EventArgs
    {
        public string Name { get; private set; }
        public SignaledChangeEventArgs(string name = null) { this.Name = name; }
    }

    /// <summary>
    /// Cache change monitor that allows an app to fire a change notification
    /// to all associated cache items.
    /// </summary>
    public class SignaledChangeMonitor : ChangeMonitor
    {
        // Shared across all SignaledChangeMonitors in the AppDomain
        private static ConcurrentDictionary<string, EventHandler<SignaledChangeEventArgs>> ListenerLookup = 
            new ConcurrentDictionary<string, EventHandler<SignaledChangeEventArgs>>();

        private string _name;
        private string _key;
        private string _uniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);

        public override string UniqueId
        {
            get { return _uniqueId; }
        }

        public SignaledChangeMonitor(string key, string name)
        {
            _key = key;
            _name = name;
            // Register instance with the shared event
            ListenerLookup[_uniqueId] = OnSignalRaised;
            base.InitializationComplete();
        }


        public static void Signal(string name = null)
        {
            // Raise shared event to notify all subscribers
            foreach (var subscriber in ListenerLookup.ToList())
            {
                subscriber.Value?.Invoke(null, new SignaledChangeEventArgs(name));
            }
        }

        protected override void Dispose(bool disposing)
        {
            // Set delegate to null so it can't be accidentally called in Signal() while being disposed
            ListenerLookup[_uniqueId] = null;
            EventHandler<SignaledChangeEventArgs> outValue = null;
            ListenerLookup.TryRemove(_uniqueId, out outValue);
        }

        private void OnSignalRaised(object sender, SignaledChangeEventArgs e)
        {
            if (string.IsNullOrWhiteSpace(e.Name) || string.Compare(e.Name, _name, true) == 0)
            {
                // Cache objects are obligated to remove entry upon change notification.
                base.OnChanged(null);
            }
        }
    }

    public static class CacheTester
    {
        private static Stopwatch _timer = new Stopwatch();

        public static void TestCache()
        {
            MemoryCache cache = MemoryCache.Default;
            int size = (int)1e6;

            Start();
            for (int idx = 0; idx < size; idx++)
            {
                cache.Add(idx.ToString(), "Value" + idx.ToString(), GetPolicy(idx, cache));
            }
            long prevCnt = cache.GetCount();
            Stop($"Added    {prevCnt} items");

            Start();
            SignaledChangeMonitor.Signal("NamedData");
            Stop($"Removed  {prevCnt - cache.GetCount()} entries");
            prevCnt = cache.GetCount();

            Start();
            SignaledChangeMonitor.Signal();
            Stop($"Removed  {prevCnt - cache.GetCount()} entries");
        }

        private static CacheItemPolicy GetPolicy(int idx, MemoryCache cache)
        {
            string name = (idx % 10 == 0) ? "NamedData" : null;

            CacheItemPolicy cip = new CacheItemPolicy();
            cip.AbsoluteExpiration = System.DateTimeOffset.UtcNow.AddHours(1);
            var monitor = new SignaledChangeMonitor(idx.ToString(), name);
            cip.ChangeMonitors.Add(monitor);
            return cip;
        }

        private static void Start()
        {
            _timer.Start();
        }

        private static void Stop(string msg = null)
        {
            _timer.Stop();
            Console.WriteLine($"{msg} | {_timer.Elapsed.TotalSeconds} sec");
            _timer.Reset();
        }
    }
}

Его решение заключалось в использовании события для отслеживания ChangeMonitors. Но метод dispose работал медленно, когда количество записей превышало 10 тыс. Я предполагаю, что этот код SignaledChangeMonitor.Signaled -= OnSignalRaised удаляет делегата из списка вызовов, выполняя линейный поиск. Поэтому, когда вы удаляете много записей, это становится медленным. Я решил использовать ConcurrentDictionary вместо события. В надежде, что dispose станет быстрее. Я провел несколько основных тестов производительности и вот результаты:

Added    10000 items | 0.027697 sec
Removed  1000 entries | 0.0040669 sec
Removed  9000 entries | 0.0105687 sec

Added    100000 items | 0.5065736 sec
Removed  10000 entries | 0.0338991 sec
Removed  90000 entries | 0.1418357 sec

Added    1000000 items | 6.5994546 sec
Removed  100000 entries | 0.4176233 sec
Removed  900000 entries | 1.2514225 sec

Я не уверен, что мой код не имеет критических недостатков. Я хотел бы знать, так ли это.

person Darkhan Zholmukhanov    schedule 29.03.2019

Другой подход заключается в реализации оболочки вокруг MemoryCache, которая реализует регионы путем составления ключа и имени региона, например.

public interface ICache 
{
...
    object Get(string key, string regionName = null);
...
}

public class MyCache : ICache
{
    private readonly MemoryCache cache

    public MyCache(MemoryCache cache)
    {
        this.cache = cache.
    }
...
    public object Get(string key, string regionName = null)
    {
        var regionKey = RegionKey(key, regionName);

        return cache.Get(regionKey);
    }   

    private string RegionKey(string key, string regionName)
    {
       // NB Implements region as a suffix, for prefix, swap order in the format
       return string.IsNullOrEmpty(regionName) ? key : string.Format("{0}{1}{2}", key, "::", regionName);
    }
...
}

Это не идеально, но работает для большинства случаев использования.

Я реализовал это, и он доступен в виде пакета NuGet: Meerkat.Caching.

person Paul Hatcher    schedule 18.05.2016