我有一个缓存(由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请求)时,上述代码仍然可能(甚至可能)不一致。 但是我写了一些单元测试,我看到的是从来没有发生过“异常”。可能发生的是,即使已经存在相同的项目,也会再次添加相同的项目 - &gt;我想你在看代码时可以看到我的意思。
我仍然认为拥有一个始终正确且一致的解决方案会很好。
所以我使用一个简单的双重检查锁机制重写了这段代码(也许这可能更好,通过为另一个缓存添加另一个/第二个锁?):
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只有一个特定数量的内核(因而是“真正的”并行线程),这种性能损失总是很小?
答案 0 :(得分:4)
如果你使用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
本身。即使您正在使用的所有对象都是线程安全的,您也无法保证您的代码也是线程安全的 - 在您的情况下,可能存在对{的争用{1}}(这是多么线程安全?),或者有人可能在此期间添加了相同的缓存项。
这究竟是如何中断在很大程度上取决于实际的实现 - 例如,如果具有相同Id的项目已经存在,或者它可能不存在,cacheItem
可能会抛出异常。您的代码可能希望所有缓存项都是相同的引用,或者它可能不是。 Add
可能会产生可怕的副作用或者运行起来很昂贵,或者可能没有。
添加了cacheItemCreatorFunc
的更新确实解决了这些问题。但是,例如,它无法处理您在整个地方泄漏lock
的方式。除非cacheItem
完全是线程安全的,否则您可能会遇到一些难以跟踪的错误。而且我们已经知道它也不是一成不变的 - 至少,你正在改变缓存时间。