正确锁定ASP.NET

我有一个搜索function相当慢的ASP.NET站点,我想通过使用查询作为缓存键将结果添加到缓存一小时来提高性能:

using System; using System.Web; using System.Web.Caching; public class Search { private static object _cacheLock = new object(); public static string DoSearch(string query) { string results = ""; if (HttpContext.Current.Cache[query] == null) { lock (_cacheLock) { if (HttpContext.Current.Cache[query] == null) { results = GetResultsFromSlowDb(query); HttpContext.Current.Cache.Add(query, results, null, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); } else { results = HttpContext.Current.Cache[query].ToString(); } } } else { results = HttpContext.Current.Cache[query].ToString(); } return results; } private static string GetResultsFromSlowDb(string query) { return "Hello World!"; } } 

假设访问者A进行搜索。 缓存为空,设置锁定并从数据库请求结果。 现在访客B出现了不同的搜索:访客B的搜索完成之前,访客B是否必须等待锁定? 我真正想要的是B立即调用数据库,因为结果会有所不同,数据库可以处理多个请求 – 我只是不想重复昂贵的不必要的查询。

这种情况的正确方法是什么?

除非你绝对确定没有冗余查询是至关重要的,否则我会完全避免锁定。 ASP.NET缓存本质上是线程安全的,因此以下代码的唯一缺点是,当关联的缓存条目到期时,您可能会暂时看到一些冗余查询相互竞争:

 public static string DoSearch(string query) { var results = (string)HttpContext.Current.Cache[query]; if (results == null) { results = GetResultsFromSlowDb(query); HttpContext.Current.Cache.Insert(query, results, null, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration); } return results; } 

如果您确定必须避免所有冗余查询,那么您可以使用一组更精细的锁,每个查询一个锁:

 public static string DoSearch(string query) { var results = (string)HttpContext.Current.Cache[query]; if (results == null) { object miniLock = _miniLocks.GetOrAdd(query, k => new object()); lock (miniLock) { results = (string)HttpContext.Current.Cache[query]; if (results == null) { results = GetResultsFromSlowDb(query); HttpContext.Current.Cache.Insert(query, results, null, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration); } object temp; if (_miniLocks.TryGetValue(query, out temp) && (temp == miniLock)) _miniLocks.TryRemove(query); } } return results; } private static readonly ConcurrentDictionary _miniLocks = new ConcurrentDictionary(); 

您的代码有潜在的竞争条件:

 if (HttpContext.Current.Cache[query] == null) { ... } else { // When you get here, another thread may have removed the item from the cache // so this may still return null. results = HttpContext.Current.Cache[query].ToString(); } 

一般情况下我不会使用锁定,并且会按照以下方式执行以避免竞争条件:

 results = HttpContext.Current.Cache[query]; if (HttpContext.Current.Cache[query] == null) { results = GetResultsFromSomewhere(); HttpContext.Current.Cache.Add(query, results,...); } return results; 

在上述情况下,如果多个线程在大约同一时间检测到高速缓存未命中,则它们可能会尝试加载数据。 在实践中,这可能是罕见的,并且在大多数情况下不重要,因为它们加载的数据将是等效的。

但是如果你想使用一个锁来阻止它,你可以这样做:

 results = HttpContext.Current.Cache[query]; if (results == null) { lock(someLock) { results = HttpContext.Current.Cache[query]; if (results == null) { results = GetResultsFromSomewhere(); HttpContext.Current.Cache.Add(query, results,...); } } } return results; 

你的代码是正确的 。 您还使用double-if-sandwitching-lock ,这将防止竞争条件 ,这是不使用时常见的陷阱。 这将不会锁定对缓存中现有内容的访问。

唯一的问题是当许多客户端同时插入缓存时,他们将在锁定后面排队,但我要做的是将results = GetResultsFromSlowDb(query); 锁外:

 public static string DoSearch(string query) { string results = ""; if (HttpContext.Current.Cache[query] == null) { results = GetResultsFromSlowDb(query); // HERE lock (_cacheLock) { if (HttpContext.Current.Cache[query] == null) { HttpContext.Current.Cache.Add(query, results, null, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); } else { results = HttpContext.Current.Cache[query].ToString(); } } } else { results = HttpContext.Current.Cache[query].ToString(); } 

如果这很慢,你的问题就在别处。