2011-04-07 5 views
7

J'ai un site ASP.NET avec une fonction de recherche assez lente, et je veux améliorer les performances en ajoutant les résultats au cache pendant une heure en utilisant la requête comme clé de cache:Effectuer le verrouillage dans ASP.NET correctement

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!"; 
    } 
} 

Supposons que le visiteur A effectue une recherche. Le cache est vide, le verrou est défini et le résultat est demandé dans la base de données. Maintenant, le visiteur B vient avec une recherche différente: le visiteur B ne devra-t-il pas attendre à la serrure jusqu'à ce que la recherche du visiteur A soit terminée? Ce que je voulais vraiment, c'est que B appelle immédiatement la base de données, car les résultats seront différents et la base de données pourra gérer plusieurs requêtes - je ne veux tout simplement pas répéter des requêtes inutiles coûteuses.

Quelle serait la bonne approche pour ce scénario?

+2

-ce que les requêtes vraiment si cher et/ou votre site si occupé que vous ne pouvez pas se permettre quelques redondantes requêtes en double une fois par heure? (Et cette situation ne se produirait que si deux ou plusieurs requêtes atteignaient votre méthode presque simultanément une fois le cache expiré.) – LukeH

+0

Si votre base de données ne prend pas en charge l'accès en lecture multiple, vous pouvez implémenter une requête de message. A, alors DB sert B ... tout en servant, vérifiez le cache. – Winfred

+0

@LukeH, Il y a tellement de choses dans cette base de données particulière, donc toute charge que nous pouvons enlever en vaut la peine. –

Répondre

25

Sauf si vous êtes absolument certain qu'il est essentiel de ne pas avoir des requêtes redondantes alors je ne pas bloquer complètement. Le cache ASP.NET est intrinsèquement thread-safe, de sorte que le seul inconvénient du code ci-dessous est que vous pouvez voir temporairement la course quelques requêtes redondantes l'autre à l'expiration de leur entrée dans le cache associé:

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; 
} 

Si vous décidez que vous devez vraiment éviter toutes les requêtes redondantes, vous pouvez utiliser un ensemble de verrous plus granulaire, un verrou par requête:

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

Awesome. Je vais voir si je peux cuisiner quelque chose de similaire pour .NET 3.5 (ConcurrentDictionary est seulement supporté dans .NET 4). Mais je vais probablement aller avec votre première suggestion jusqu'à ce que nous améliorons. Merci. :) –

+0

@LukeH autre que l'espace s'il y a des tonnes de requêtes différentes, est-il vraiment nécessaire de supprimer _miniLocks? – eglasius

+0

@ Eglasius: Non, c'est juste une tentative de libérer de l'espace. – LukeH

0

Votre code est correct. Vous utilisez également double-if-sandwitching-lock qui empêchera conditions de course qui est un piège commun lorsqu'il n'est pas utilisé. Cela ne verrouillera pas l'accès aux éléments existants dans le cache.

Le seul problème est alors que de nombreux clients insèrent dans le cache en même temps, et ils en file d'attente derrière le verrou, mais ce que je ferais est de mettre le results = GetResultsFromSlowDb(query); en dehors de la serrure:

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(); 
    } 

Si c'est lent, ton problème est ailleurs.

+0

Merci. Mais êtes-vous sûr que le visiteur B n'aura pas à attendre que le visiteur A soit fini? –

+0

Voir mes mises à jour s'il vous plaît. – Aliostad

+1

Le fait de déplacer le GetResultsFromSlowDb en dehors du verrou ne va-t-il pas à l'encontre de l'objectif du double-cache-check? Les visuels multiples peuvent démarrer la même requête, s'ils entrent avant que le premier visiteur soit terminé. –

8

Votre code a une condition de course potentielle:

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();   
} 

En général, je ne voudrais pas utiliser le verrouillage et le ferait comme suit pour éviter la condition de course:

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

Dans ce qui précède Dans le cas contraire, plusieurs threads peuvent tenter de charger des données s'ils détectent un échec de cache à peu près au même moment. En pratique, cela risque d'être rare et, dans la plupart des cas, sans importance, car les données qu'ils chargent seront équivalentes.

Mais si vous voulez utiliser un verrou pour empêcher que vous pouvez le faire comme suit:

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 pour mettre en évidence cette condition de compétition. Peut également se produire si l'élément de cache expire –

Questions connexes