【问题标题】:MemoryCache with regions support?支持区域的 MemoryCache?
【发布时间】:2012-02-18 16:37:33
【问题描述】:

我需要添加缓存功能并找到一个名为 MemoryCache 的新类。但是,我发现 MemoryCache 有点残废(我需要区域功能)。除其他事项外,我需要添加诸如 ClearAll(region) 之类的内容。作者做了很大的努力来保持这个类没有区域支持,代码如下:

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

几乎每一种方法都能飞。 我没有看到一种简单的方法来覆盖这种行为。我能想到的添加区域支持的唯一方法是添加一个新类作为 MemoryCache 的包装器,而不是作为从 MemoryCache 继承的类。然后在这个新类中创建一个 Dictionary 并让每个方法“缓冲”区域调用。听起来很恶心和错误,但最终......

您知道将区域添加到 MemoryCache 的更好方法吗?

【问题讨论】:

标签: c# .net memorycache


【解决方案1】:

我知道您已经很久没有提出这个问题了,所以这并不是对您的真正答案,而是对未来读者的补充。

我还惊讶地发现 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

}

【讨论】:

  • 通过 MemoryCache 枚举是低效的,因为它会暂时锁定整个缓存。此外,您的 Clear() 是线性搜索,因此线性缓存项的数量会变得更糟。这是一个更好的解决方案:stackoverflow.com/a/22388943/220230
  • 感谢您的关注。在给定的简单示例中,我现在删除了 Clear 方法以避免将其他人误入歧途。对于那些真正需要按地区手动删除的人,我参考给定的链接。
【解决方案2】:

您可以只创建一个以上的 MemoryCache 实例,为您的数据的每个分区创建一个。

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

您可以创建 MemoryCache 类的多个实例,以便在同一个应用程序和同一个 AppDomain 实例中使用

【讨论】:

  • 实例化所有这些新的 MemoryCache 实例的最佳位置在哪里?是否有可以管理所有这些实例的 MemoryCache 提供程序?
  • @HenleyChiu 我认为基础库中没有任何内容。只需使用共享状态的标准方法,例如一个静态的、全局可见的ConcurrentDictionary<string, MemoryCache>
  • 在某些情况下使用多个MemoryCache 实例可能会降低缓存的有效性。见:stackoverflow.com/questions/8463962/…
  • 使用 IMemoryCache 的依赖注入被设计为用作单例。
【解决方案3】:

另一种方法是在 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

【讨论】:

    【解决方案4】:

    我最近才遇到这个问题。我知道这是一个老问题,但也许这对某些人有用。这是我对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。但是当条目数超过 10k 时,dispose 方法运行缓慢。我的猜测是这段代码SignaledChangeMonitor.Signaled -= OnSignalRaised 通过进行线性搜索从调用列表中删除了一个委托。因此,当您删除大量条目时,它会变得很慢。我决定使用 ConcurrentDictionary 而不是事件。希望处置变得更快。我进行了一些基本的性能测试,结果如下:

    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
    

    我不确定我的代码是否存在一些严重缺陷。我想知道是不是这样。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-07-22
      • 2012-06-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多