2011-04-07 63 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