正确锁定ASP.NET

Jak*_*ade 7 .net c# asp.net caching locking

我有一个具有相当慢的搜索功能的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!";
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Luk*_*keH 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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>();
Run Code Online (Sandbox Code Playgroud)

  • +1用于强调几乎每个缓存锁定示例似乎忽略的内容:正确的锁定选择.它可以访问您要锁定的一个特定缓存项,而不是整个缓存! (3认同)

Joe*_*Joe 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();         
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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;
Run Code Online (Sandbox Code Playgroud)