如何处理使用MemoryCache的代价高昂的build筑操作?

在ASP.NET MVC项目中,我们有几个需要大量资源和时间构build的数据实例。 我们想caching它们。

MemoryCache提供一定程度的线程安全性,但不足以避免并行运行多个构build代码实例。 这里是一个例子:

 var data = cache["key"]; if(data == null) { data = buildDataUsingGoodAmountOfResources(); cache["key"] = data; } 

正如您在繁忙的网站上看到的,数百个线程可能会同时进入if语句,直到数据build立,并使build设操作更慢,不必要地占用服务器资源。

在MemoryCache中有一个primefacesAddOrGetExisting实现,但它错误地要求“设置值”而不是“代码来检索设置的值”,我认为呈现给定的方法几乎完全无用。

我们一直在使用我们自己的专用脚手架来绕过MemoryCache,但是它需要明确的lock 。 使用per-entry锁对象很麻烦,我们通常通过共享远离理想的锁对象而逃脱。 这使我认为避免这种惯例的理由可能是有意的。

所以我有两个问题:

  • lockbuild筑物代码是否更好? (这可能已经被certificate对一个更加敏感,我想知道)

  • 什么是正确的方式来实现每个条目lockingMemoryCache这样的锁? 在“.NETlocking101”中,强烈要求使用keystring作为locking对象。

我们通过将Lazy<T>AddOrGetExisting相结合来解决这个问题,以避免完全需要锁对象。 这是一个示例代码(使用无限期满):

 public T GetFromCache<T>(string key, Func<T> valueFactory) { var newValue = new Lazy<T>(valueFactory); // the line belows returns existing item or adds the new value if it doesn't exist var value = (Lazy<T>)cache.AddOrGetExisting(key, newValue, MemoryCache.InfiniteExpiration); return (value ?? newValue).Value; // Lazy<T> handles the locking itself } 

这不完整。 有像“exceptioncaching”的疑难杂症,所以你必须决定你想做什么,以防你的valueFactory抛出exception。 其中一个优点就是能够caching空值。

对于条件添加要求,我总是使用ConcurrentDictionary ,它有一个重载的GetOrAdd方法,如果需要构build对象,它接受一个委托来触发。

 ConcurrentDictionary<string, object> _cache = new ConcurrenctDictionary<string, object>(); public void GetOrAdd(string key) { return _cache.GetOrAdd(key, (k) => { //here 'k' is actually the same as 'key' return buildDataUsingGoodAmountOfResources(); }); } 

实际上,我几乎总是使用static并发字典。 我曾经有一个由' ReaderWriterLockSlim '实例保护的'正常'字典,但是一旦我切换到.Net 4(它只能从此开始),我开始转换我遇到的那些。

ConcurrentDictionary的performance令人钦佩,至less可以说:)

使用仅基于年龄的过期语义更新初始实现。 还应该确保单个项目只创build一次 – 根据@ usr的build议。 再次更新 – 正如@usr所build议的那样 – 简单地使用Lazy<T>会简单得多 – 只需将创build委托转发到并发字典即可。 我改变了代码,因为实际上我的锁字典不会起作用。 但我真的应该想到,我自己(过去在英国的午夜,但我被打败了,任何同情?当然不是,作为一个开发者,我有足够的咖啡因通过我的血pipe来唤醒死亡)

但是,我build议使用IRegisteredObject接口来实现,然后将其注册到HostingEnvironment.RegisterObject方法中 – 这样可以在应用程序池closures/回收时提供更HostingEnvironment.RegisterObject方式来closures轮询线程。

 public class ConcurrentCache : IDisposable { private readonly ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>> _cache = new ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>>(); private readonly Thread ExpireThread = new Thread(ExpireMonitor); public ConcurrentCache(){ ExpireThread.Start(); } public void Dispose() { //yeah, nasty, but this is a 'naive' implementation :) ExpireThread.Abort(); } public void ExpireMonitor() { while(true) { Thread.Sleep(1000); DateTime expireTime = DateTime.Now; var toExpire = _cache.Where(kvp => kvp.First != null && kvp.Item1.Value < expireTime).Select(kvp => kvp.Key).ToArray(); Tuple<string, Lazy<object>> removed; object removedLock; foreach(var key in toExpire) { _cache.TryRemove(key, out removed); } } } public object CacheOrAdd(string key, Func<string, object> factory, TimeSpan? expiry) { return _cache.GetOrAdd(key, (k) => { //get or create a new object instance to use //as the lock for the user code //here 'k' is actually the same as 'key' return Tuple.Create( expiry.HasValue ? DateTime.Now + expiry.Value : (DateTime?)null, new Lazy<object>(() => factory(k))); }).Item2.Value; } } 

这是一个devise,遵循你似乎想​​到的。 第一次锁只发生在短时间内。 对data.Value的最终调用也会locking(底下),但是如果客户端中的两个同时请求同一个项目,客户端将只会被阻塞。

 public DataType GetData() { lock(_privateLockingField) { Lazy<DataType> data = cache["key"] as Lazy<DataType>; if(data == null) { data = new Lazy<DataType>(() => buildDataUsingGoodAmountOfResources(); cache["key"] = data; } } return data.Value; } 

从C#7的顶部答案,这是我的实现,允许从任何源typesT存储到任何返回typesTResult

 /// <summary> /// Creates a GetOrRefreshCache function with encapsulated MemoryCache. /// </summary> /// <typeparam name="T">The type of inbound objects to cache.</typeparam> /// <typeparam name="TResult">How the objects will be serialized to cache and returned.</typeparam> /// <param name="cacheName">The name of the cache.</param> /// <param name="valueFactory">The factory for storing values.</param> /// <param name="keyFactory">An optional factory to choose cache keys.</param> /// <returns>A function to get or refresh from cache.</returns> public static Func<T, TResult> GetOrRefreshCacheFactory<T, TResult>(string cacheName, Func<T, TResult> valueFactory, Func<T, string> keyFactory = null) { var getKey = keyFactory ?? (obj => obj.GetHashCode().ToString()); var cache = new MemoryCache(cacheName); // Thread-safe lazy cache TResult getOrRefreshCache(T obj) { var key = getKey(obj); var newValue = new Lazy<TResult>(() => valueFactory(obj)); var value = (Lazy<TResult>) cache.AddOrGetExisting(key, newValue, ObjectCache.InfiniteAbsoluteExpiration); return (value ?? newValue).Value; } return getOrRefreshCache; } 

用法

 /// <summary> /// Get a JSON object from cache or serialize it if it doesn't exist yet. /// </summary> private static readonly Func<object, string> GetJson = GetOrRefreshCacheFactory<object, string>("json-cache", JsonConvert.SerializeObject); var json = GetJson(new { foo = "bar", yes = true }); 

这里是简单的解决scheme作为MemoryCache扩展方法。

  public static class MemoryCacheExtensions { public static T LazyAddOrGetExitingItem<T>(this MemoryCache memoryCache, string key, Func<T> getItemFunc, DateTimeOffset absoluteExpiration) { var item = new Lazy<T>( () => getItemFunc(), LazyThreadSafetyMode.PublicationOnly // Do not cache lazy exceptions ); var cachedValue = memoryCache.AddOrGetExisting(key, item, absoluteExpiration) as Lazy<T>; return (cachedValue != null) ? cachedValue.Value : item.Value; } } 

并作为使用说明进行testing。

 [TestMethod] [TestCategory("MemoryCacheExtensionsTests"), TestCategory("UnitTests")] public void MemoryCacheExtensions_LazyAddOrGetExitingItem_Test() { const int expectedValue = 42; const int cacheRecordLifetimeInSeconds = 42; var key = "lazyMemoryCacheKey"; var absoluteExpiration = DateTimeOffset.Now.AddSeconds(cacheRecordLifetimeInSeconds); var lazyMemoryCache = MemoryCache.Default; #region Cache warm up var actualValue = lazyMemoryCache.LazyAddOrGetExitingItem(key, () => expectedValue, absoluteExpiration); Assert.AreEqual(expectedValue, actualValue); #endregion #region Get value from cache actualValue = lazyMemoryCache.LazyAddOrGetExitingItem(key, () => expectedValue, absoluteExpiration); Assert.AreEqual(expectedValue, actualValue); #endregion }