2011-04-07 33 views
7

我有一个具有相当慢的搜索功能的ASP.NET网站,我想通过将查询作为缓存键添加到缓存一小时来提高性能:在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带有不同的搜索:在访问者A的搜索完成之前,访问者B是否必须等待锁定?我真正想要的是B立即调用数据库,因为结果会不同,数据库可以处理多个请求 - 我只是不想重复昂贵的不必要的查询。

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

+2

是真正的查询那么贵和/或网站那么忙,你买不起几多余的重复查询每小时一次? (并且只有在两次或更多次查询几乎同时在缓存过期时同时触发您的方法时,才会出现这种情况。) – LukeH 2011-04-07 09:22:47

+0

如果您的数据库不支持多个读取访问,则可以实施消息查询,以便DB服务A,然后DB服务B ...在服务时检查缓存。 – Winfred 2011-04-07 09:28:05

+0

@LukeH,在那个特定的数据库中发生了很多事情,所以我们可以减轻负担是值得的。 – 2011-04-07 09:28:43

回答

25

除非你是绝对肯定的是,它是有没有多余的疑问,那么我会完全避免锁定关键。 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<string, object> _miniLocks = 
            new ConcurrentDictionary<string, object>(); 
+0

太棒了。我会看看是否可以为.NET 3.5做类似的工作(ConcurrentDictionary仅在.NET 4中受支持)。但是我可能会在第一时间提出你的第一个建议,直到我们升级。谢谢。 :) – 2011-04-07 12:35:15

+0

@LukeH除了空间以外,如果有很多不同的查询,是否真的需要从_miniLocks中删除? – eglasius 2012-01-25 21:29:13

+0

@eglasius:不,这只是一个释放空间的尝试。 – LukeH 2012-01-26 00:48:18

0

您的代码是正确。您还在使用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(); 
    } 

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

+0

谢谢。但是你确定访客B不必等到访客A完成了吗? – 2011-04-07 09:32:11

+0

请参阅我的更新。 – Aliostad 2011-04-07 09:32:30

+1

不会将GetResultsFromSlowDb移动到锁外面,否则会破坏双缓存检查的目的?多个下巴可以开始相同的查询,如果他们在第一个访问者完成之前输入的话。 – 2011-04-07 09:36:53

8

代码有潜在的竞争条件:

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; 
+1

+1用于突出显示竞争条件。如果缓存项目过期,也可能发生 – 2012-08-10 00:50:30