我有一个缓存(由一个web应用程序使用),它在内部使用两个缓存--一个短期缓存(仅在请求中使用)和一个“永久”使用(跨请求)的长期缓存。
我有以下代码,请注意,所有底层数据结构都是线程安全的。
public TCache Get(CacheDependency cachdeDependancy, Func<CacheDependency, TCache> cacheItemCreatorFunc)
{
TCache cacheItem;
if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem))
{
return cacheItem;
}
DateTime cacheDependancyLastModified;
if (longTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem)
&& IsValid(cachdeDependancy, cacheItem, out cacheDependancyLastModified))
{
cacheItem.CacheTime = cacheDependancyLastModified;
shortTermCache[cachdeDependancy.Id] = cacheItem;
return cacheItem;
}
cacheItem = cacheItemCreatorFunc(cachdeDependancy);
longTermCache.Add(cachdeDependancy.Id, cacheItem);
shortTermCache[cachdeDependancy.Id] = cacheItem;
return cacheItem;
}显然,当运行并发(即多个web请求)时,上面的代码仍然可能(甚至可能)是不一致的。然而,我写了一些单元测试,我看到的是,从来没有“异常”发生。可能发生的情况是,即使已经存在,也会再次添加相同的项。
尽管如此,我仍然认为,如果有一个解决方案总是正确和一致的,那就太好了。
因此,我使用简单的双重检查锁机制重写了这段代码(也许这样做更好,为另一个缓存添加另一个/秒锁?):
public TCache Get(CacheDependency cachdeDependancy, Func<CacheDependency, TCache> cacheItemCreatorFunc)
{
TCache cacheItem;
if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem))
{
return cacheItem;
}
lock (_lockObj)
{
if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem))
{
return cacheItem;
}
DateTime cacheDependancyLastModified;
if (longTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem)
&& IsValid(cachdeDependancy, cacheItem, out cacheDependancyLastModified))
{
cacheItem.CacheTime = cacheDependancyLastModified;
shortTermCache[cachdeDependancy.Id] = cacheItem;
return cacheItem;
}
cacheItem = cacheItemCreatorFunc(cachdeDependancy);
longTermCache.Add(cachdeDependancy.Id, cacheItem);
shortTermCache[cachdeDependancy.Id] = cacheItem;
return cacheItem;
}
}我认为这段代码现在在多线程环境中正确工作。
然而,我不确定的是:这会不会非常慢,因此也会破坏缓存的用途?如果缓存有时会有“不一致”的行为,那么接受这个问题会更好吗?因为如果同时有1000个web请求,所有这些请求都必须等待,直到它们能够进入锁定区域。或者这根本不是一个问题,因为一个CPU一次只有特定数量的内核(因此是“真正的”并行线程),而这个性能损失总是很小的吗?
发布于 2015-10-20 09:36:59
如果您使用ConcurrentDictionary,您已经有了一种方法来做您想做的事情--您可以简单地使用GetOrAdd方法:
shortTermCache[cacheDependency.Id] =
longTermCache.GetOrAdd(cacheDependency.Id, _ => cacheItemCreatorFunc(cachdeDependancy));(既快又容易:)
您甚至可以将其扩展为包括短期缓存检查:
return
shortTermCache.GetOrAdd
(
cacheDependency.Id,
_ =>
{
return longTermCache
.GetOrAdd(cacheDependency.Id, __ => cacheItemCreatorFunc(cacheDependency));
}
);虽然对每个请求缓存使用ConcurrentDictionary是不必要的,但它实际上并不一定是线程安全的。
至于你的原始代码,是的,它坏了。在测试过程中,您没有发现这一点并不令人惊讶--多线程问题通常很难重现。这就是为什么您希望代码是正确的,首先也是最重要的--这意味着您必须了解到底发生了什么,以及可能发生什么样的并发问题。在您的示例中,有两个共享引用:longTermCache和cacheItem本身。即使您正在处理的所有对象都是线程安全的,您也无法保证您的代码也是线程安全的--在您的情况下,可能会有关于cacheItem (线程安全程度如何?)的争论,或者在此期间可能有人添加了相同的缓存项。
这种中断的确切程度在很大程度上取决于实际的实现--例如,如果一个Id相同的项已经存在,或者它可能没有出现,Add可能会抛出一个异常。您的代码可能希望所有缓存项都是相同的引用,也可能不会。cacheItemCreatorFunc可能有可怕的副作用,或者运行起来很昂贵,或者没有。
您添加的lock更新确实解决了这些问题。但是,它不能处理您在整个地方泄漏cacheItem的方式,例如。除非cacheItem也是完全线程安全的,否则您可能会遇到一些难以跟踪的bug。而且我们已经知道它也不是一成不变的--至少,您正在改变缓存时间。
https://stackoverflow.com/questions/33232384
复制相似问题