在ASP.NET MVC项目中,我们有几个需要大量资源和构建时间的数据实例。我们想要缓存它们。
MemoryCache
提供了一定程度的线程安全性,但不足以避免并行运行多个构建代码实例。这是一个例子:
var data = cache["key"];
if(data == null)
{
data = buildDataUsingGoodAmountOfResources();
cache["key"] = data;
}
正如您在繁忙的网站上看到的那样,数百个线程可以同时进入if语句,直到构建数据并使构建操作更慢,不必要地消耗服务器资源。
MemoryCache中有一个原子AddOrGetExisting
实现,但它错误地要求“值设置”而不是“检索要设置的值的代码”,我认为这使得给定的方法几乎完全没用。
我们一直在MemoryCache周围使用我们自己的ad-hoc脚手架来实现它,但它需要明确的lock
。使用每个条目的锁定对象很麻烦,我们通常会通过共享远离理想的锁定对象来逃避。这让我觉得避免这种惯例的理由可能是故意的。
所以我有两个问题:
最好不要lock
构建代码吗? (我想知道这可能对一个人更具响应性)
对于这样的锁,实现MemoryCache的每个条目锁定的正确方法是什么?使用key
字符串作为锁定对象的强烈冲动在“.NET锁定101”中被忽略。
答案 0 :(得分:69)
我们通过将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
}
那不完整。有一些像“异常缓存”的问题所以你必须决定你想要做什么,以防你的valueFactory抛出异常。但是,其中一个优点是能够缓存空值。
答案 1 :(得分:10)
对于条件添加要求,我总是使用ConcurrentDictionary
,它有一个重载的GetOrAdd
方法,如果需要构建对象,它接受一个委托。
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
的表现令人钦佩:)
更新使用基于年龄的过期语义的简单实现。还应该确保只创建一次单个项目 - 根据@ usr的建议。 再次更新 - 正如@usr建议的那样 - 简单地使用Lazy<T>
会简单得多 - 您可以在将创建委托添加到并发字典时将其转发给它。我改变了代码,因为实际上我的锁字典无论如何都不会有效。但我真的应该想到自己(虽然在英国已经过了午夜,但我已经被击败了。任何同情?当然不是。作为开发者,我有足够的咖啡因在我的血管中咳嗽醒来死者)
我建议使用此实现IRegisteredObject
接口,然后使用HostingEnvironment.RegisterObject
方法注册它 - 这样可以在应用程序池关闭时提供更简洁的方法来关闭轮询器线程-down /回收。
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;
}
}
答案 2 :(得分:1)
这是一个遵循您似乎想到的设计。第一次锁只会发生很短的时间。对data.Value的最终调用也会锁定(在下面),但客户端只会阻止其中两个同时请求相同的项目。
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;
}
答案 3 :(得分:1)
将最常见的答案带入C#7,这是我的实现,它允许从任何源类型T
存储到任何返回类型TResult
。
/// <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 });
答案 4 :(得分:1)
Sedat将Lazy与AddOrGetExisting相结合的解决方案令人鼓舞。我必须指出,该解决方案存在性能问题,这对于缓存解决方案而言似乎非常重要。
如果查看AddOrGetExisting()的code,您会发现AddOrGetExisting()不是非锁定方法。与无锁Get()方法相比,它浪费了MemoryCache的优势之一。
我建议您遵循以下解决方案,先使用Get(),然后再使用AddOrGetExisting()以避免多次创建对象。
public T GetFromCache<T>(string key, Func<T> valueFactory)
{
T value = (T)cache.Get(key);
if (value != null)
{
return value;
}
var newValue = new Lazy<T>(valueFactory);
// the line belows returns existing item or adds the new value if it doesn't exist
var oldValue = (Lazy<T>)cache.AddOrGetExisting(key, newValue, MemoryCache.InfiniteExpiration);
return (oldValue ?? newValue).Value; // Lazy<T> handles the locking itself
}
答案 5 :(得分:0)
这是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;
}
}
并将其作为使用说明进行测试。
[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
}