我有一个缓存(由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一次仅具有特定数量的内核(因此只有“真正的”并行线程),并且这种性能下降总是很小的?
解决方法:
如果使用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可能具有可怕的副作用,或者运行起来很昂贵,或者可能没有.
使用添加的锁进行的更新确实可以解决这些问题.但是,例如,它无法处理您在各处泄漏cacheItem的方式.除非cacheItem也是完全线程安全的,否则您可能会遇到一些难以跟踪的错误.而且我们已经知道它也不是不变的-至少,您正在更改缓存时间.