Правильно делать блокировки в ASP.NET - PullRequest
7 голосов
/ 07 апреля 2011

У меня есть сайт 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!";
    }
}

Допустим, посетитель А выполняет поиск.Кеш пуст, блокировка установлена ​​и результат запрашивается из базы данных.Теперь посетитель B приходит с другим поиском: разве посетителю B не придется ждать у замка, пока поиск посетителя A не завершится?Я действительно хотел, чтобы B немедленно вызвал базу данных, потому что результаты будут другими, и база данных сможет обрабатывать несколько запросов - я просто не хочу повторять дорогостоящие ненужные запросы.

Что было бы правильнымподходить по этому сценарию?

Ответы [ 3 ]

25 голосов
/ 07 апреля 2011

Если вы не абсолютно уверены, что крайне важно не иметь избыточных запросов, тогда я бы вообще избежал блокировки.Кэш 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>();
8 голосов
/ 07 апреля 2011

Ваш код имеет потенциальное состояние гонки:

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;
0 голосов
/ 07 апреля 2011

Ваш код правильный . Вы также используете 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();
    }

Если это медленно, ваша проблема в другом месте.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...