【问题标题】:Doing locking in ASP.NET correctly正确锁定 ASP.NET
【发布时间】:2011-07-31 12:40:02
【问题描述】:

我有一个搜索功能相当慢的 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 带来了不同的搜索:访问者 B 不是必须等待访问者 A 的搜索完成吗?我真正想要的是让 B 立即调用数据库,因为结果会有所不同,并且数据库可以处理多个请求——我只是不想重复昂贵的不必要的查询。

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

【问题讨论】:

  • 查询真的太昂贵和/或您的网站太忙以至于您无法负担每小时一次的重复查询吗? (只有当且仅当缓存过期后,两个或多个查询几乎同时命中您的方法时,才会出现这种情况。)
  • 如果你的数据库不支持多读,可以实现一个消息查询,DB服务A,然后DB服务B……服务的时候,检查缓存。
  • @LukeH,在那个特定的数据库中发生了很多事情,所以我们可以减轻任何负载都是值得的。
  • 只有一台网络服务器吗?如果没有,您可以使用分布式缓存来获得更大的效果。
  • @Chris,到目前为止只有一个网络服务器。

标签: c# .net asp.net caching locking


【解决方案1】:

除非您绝对确定没有冗余查询至关重要,否则我会完全避免锁定。 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>();

【讨论】:

  • 太棒了。我会看看我是否可以为 .NET 3.5 做类似的事情(ConcurrentDictionary 仅在 .NET 4 中受支持)。但在我们升级之前,我可能会接受你的第一个建议。谢谢。 :)
  • @LukeH 除了空格,如果有大量不同的查询,真的需要从 _miniLocks 中删除吗?
  • @eglasius:不,这只是试图释放空间。
  • +1 用于强调几乎每个缓存锁定示例似乎都忽略了:正确的锁定选择。它是对您要锁定的特定缓存项的访问,而不是整个缓存!
  • 请注意,GetOrAdd 方法有一个带有委托的重载和一个带有对象的重载。似乎委托版本具有不同的线程安全锁定规则。参考:msdn.microsoft.com/en-us/library/…
【解决方案2】:

您的代码有潜在的竞争条件:

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 (results == 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 用于突出显示该竞争条件。如果缓存项过期也可能发生
【解决方案3】:

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

如果这很慢,那么您的问题出在其他地方。

【讨论】:

  • 谢谢。但是您确定访客 B 不必等到访客 A 结束吗?
  • 将 GetResultsFromSlowDb 移到锁外不会破坏双重缓存检查的目的吗?多个访问者可以启动同一个查询,如果他们在第一个访问者完成之前进入。
  • 没有。您的缓存会在那里停留一个小时。如果两个客户端尝试在锁的内部或外部获取完全相同的相同的密钥,他们就会等待,只有你的数据库被调用得更少。但这将允许客户愉快地获得不同的密钥,而不必等待。
  • @Aliostad:锁定在您的编辑中没有任何用处。所发生的一切是您在运行冗余查询后丢弃了它们的结果。您最好摆脱锁定,让冗余查询在完成时全部更新缓存。 (ASP.NET 缓存是线程安全的。)
  • @Aliostad:没错。那么没有锁就不会发生锁的作用。我能看到的唯一区别是,在所有赛车查询完成后,“foo”的第一个结果将在缓存中。如果没有锁,那么在所有竞速查询完成后,“foo”的最后一个结果将在缓存中。 (在第二种情况下,每个结果将在完成时添加到缓存中,并一直保留在那里直到下一个结果覆盖它,但这一切都将以原子、线程安全的方式发生。)
猜你喜欢
  • 1970-01-01
  • 2019-10-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-08
  • 1970-01-01
  • 2022-01-12
相关资源
最近更新 更多