在asp.net中锁定缓存的最佳方法是什么?

81
我知道在某些情况下,比如长时间运行的进程中,锁定ASP.NET缓存是很重要的,以避免其他用户对该资源的后续请求再次执行长时间进程而不是命中缓存。
在C#中实现ASP.NET缓存锁定的最佳方法是什么?
11个回答

117

以下是基本模式:

  • 检查缓存中是否有值,如果有则返回
  • 如果缓存中没有该值,则实现锁定
  • 在锁定内部再次检查缓存,您可能已被阻止
  • 执行值的查找并将其缓存
  • 释放锁定

在代码中,它看起来像这样:

private static object ThisLock = new object();

public string GetFoo()
{

  // try to pull from cache here

  lock (ThisLock)
  {
    // cache was empty before we got the lock, check again inside the lock

    // cache is still empty, so retreive the value here

    // store the value in the cache here
  }

  // return the cached value here

}

4
如果缓存的第一次加载需要几分钟,是否仍然有办法访问已经加载的条目?比如说,如果我有一个名为GetFoo_AmazonArticlesByCategory(string categoryKey)的方法,那么可能会采用每个categoryKey一个锁的方式。 - Mathias F
5
被称为“双重检查锁定”。 - Brad Gagne
这个答案是非常糟糕的建议,而且自2008年以来一直在被实施。解释:https://dev59.com/z3VD5IYBdhLWcg3wQJOT#75132952 - Mark

32

为了完整性,一个完整的示例可能如下所示。

private static object ThisLock = new object();
...
object dataObject = Cache["globalData"];
if( dataObject == null )
{
    lock( ThisLock )
    {
        dataObject = Cache["globalData"];

        if( dataObject == null )
        {
            //Get Data from db
             dataObject = GlobalObj.GetData();
             Cache["globalData"] = dataObject;
        }
    }
}
return dataObject;

7
如果( dataObject == null ) { 锁定(ThisLock) { 如果( dataObject == null ) // 当然还是为空! - Constantin
30
@Constantin: 并不完全如此,当你等待获取lock()时,有人可能已经更新了缓存。 - Tudor Olariu
12
在锁语句之后,你需要再次尝试从缓存中获取对象! - Pavel Nikolov
3
代码有误(请阅读其他评论),为什么不修复它呢?人们可能会尝试使用您的示例。 - orip
11
这段代码实际上仍然有误。你在一个实际上不存在的作用域中返回了globalObject。正确的做法是应该在最终的空值检查中使用dataObject,而globalObject根本不需要存在。 - Scott Arrington

25

无需锁定整个缓存实例,只需要锁定要插入的特定键即可。也就是说,使用男性洗手间时无需封锁女性洗手间的通道 :)

以下实现允许使用并发字典锁定特定的缓存键。这样,您可以同时运行两个不同键的GetOrAdd()方法,但不能同时针对同一键运行。

using System;
using System.Collections.Concurrent;
using System.Web.Caching;

public static class CacheExtensions
{
    private static ConcurrentDictionary<string, object> keyLocks = new ConcurrentDictionary<string, object>();

    /// <summary>
    /// Get or Add the item to the cache using the given key. Lazily executes the value factory only if/when needed
    /// </summary>
    public static T GetOrAdd<T>(this Cache cache, string key, int durationInSeconds, Func<T> factory)
        where T : class
    {
        // Try and get value from the cache
        var value = cache.Get(key);
        if (value == null)
        {
            // If not yet cached, lock the key value and add to cache
            lock (keyLocks.GetOrAdd(key, new object()))
            {
                // Try and get from cache again in case it has been added in the meantime
                value = cache.Get(key);
                if (value == null && (value = factory()) != null)
                {
                    // TODO: Some of these parameters could be added to method signature later if required
                    cache.Insert(
                        key: key,
                        value: value,
                        dependencies: null,
                        absoluteExpiration: DateTime.Now.AddSeconds(durationInSeconds),
                        slidingExpiration: Cache.NoSlidingExpiration,
                        priority: CacheItemPriority.Default,
                        onRemoveCallback: null);
                }

                // Remove temporary key lock
                keyLocks.TryRemove(key, out object locker);
            }
        }

        return value as T;
    }
}

keyLocks.TryRemove(key, out locker) <= 它的作用是什么? - iMatoria
2
这很棒。锁定缓存的整个重点是避免重复获取特定键的值所做的工作。锁定整个缓存,甚至按类别锁定其部分是愚蠢的。你需要的正是这个 - 一把锁,它表示“我正在获取<key>的值,其他人请等待我。”扩展方法也很棒。两个伟大的想法结合在一起!这应该是人们找到的答案。谢谢。 - DanO
1
@iMatoria,一旦缓存中有该键的内容,就没有必要保留锁对象或键字典中的条目 - 这是一个尝试删除,因为锁可能已经被先到达的另一个线程从字典中删除 - 所有等待该键的线程现在都在那个代码部分中,他们只需从缓存中获取值,但不再需要删除锁。 - DanO
我更喜欢这种方法,比起被接受的答案。不过需要注意的是:你首先使用了cache.Key,然后在后面你使用了HttpRuntime.Cache.Get。 - staccata
@MindaugasTvaronavicius 很好的发现,你是正确的,这是一个边缘情况,其中T2和T3同时执行“factory”方法。只有在T1之前执行了返回null的“factory”(因此值未被缓存)时才会出现这种情况。否则,T2和T3将同时获取缓存的值(这应该是安全的)。我想简单的解决方案是删除“keyLocks.TryRemove(key, out locker)”,但是如果使用大量不同的键,则ConcurrentDictionary可能会成为内存泄漏。否则,在删除之前添加一些逻辑来计算密钥的锁定次数,也许可以使用信号量? - cwills
好的解决方案。对于新开发人员需要注意的唯一警告是要注意键碰撞与不同类型的返回值。您可以在“返回值作为T”的周围尝试捕获。如果没有这个扩展方法,您将遇到问题,因此您可能希望在传递的键前缀中加上唯一值(例如guid的子字符串)。 - Charles Byrne

13

只是为了echo Pavel所说的话,我相信这是最安全的编写方式。

private T GetOrAddToCache<T>(string cacheKey, GenericObjectParamsDelegate<T> creator, params object[] creatorArgs) where T : class, new()
    {
        T returnValue = HttpContext.Current.Cache[cacheKey] as T;
        if (returnValue == null)
        {
            lock (this)
            {
                returnValue = HttpContext.Current.Cache[cacheKey] as T;
                if (returnValue == null)
                {
                    returnValue = creator(creatorArgs);
                    if (returnValue == null)
                    {
                        throw new Exception("Attempt to cache a null reference");
                    }
                    HttpContext.Current.Cache.Add(
                        cacheKey,
                        returnValue,
                        null,
                        System.Web.Caching.Cache.NoAbsoluteExpiration,
                        System.Web.Caching.Cache.NoSlidingExpiration,
                        CacheItemPriority.Normal,
                        null);
                }
            }
        }

        return returnValue;
    }

7
"lock(this)"是很糟糕的。你应该使用一个专门的锁对象,这个对象对于你的类的用户来说是不可见的。假设某个人决定在将来使用缓存对象进行锁定,他们将不知道它被用于内部的锁定目的,这可能会导致问题。 - spender

2
我想出了以下扩展方法:

private static readonly object _lock = new object();

public static TResult GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action, int duration = 300) {
    TResult result;
    var data = cache[key]; // Can't cast using as operator as TResult may be an int or bool

    if (data == null) {
        lock (_lock) {
            data = cache[key];

            if (data == null) {
                result = action();

                if (result == null)
                    return result;

                if (duration > 0)
                    cache.Insert(key, result, null, DateTime.UtcNow.AddSeconds(duration), TimeSpan.Zero);
            } else
                result = (TResult)data;
        }
    } else
        result = (TResult)data;

    return result;
}

我使用了@John Owen和@user378380的答案。我的解决方案允许您在缓存中存储整数和布尔值。
如有任何错误或可以写得更好,请纠正我。

这是默认的缓存长度为5分钟(60 * 5 = 300秒)。 - nfplee
3
非常出色的工作,但我看到一个问题:如果你有多个缓存,它们将共享相同的锁。为了使其更加健壮,使用字典来检索匹配给定缓存的锁。 - JoeCool

2

1

我最近看到了一个叫做正确状态包访问模式的设计模式,似乎涉及到这个问题。

我稍微修改了一下,使其线程安全。

http://weblogs.asp.net/craigshoemaker/archive/2008/08/28/asp-net-caching-and-performance.aspx

private static object _listLock = new object();

public List List() {
    string cacheKey = "customers";
    List myList = Cache[cacheKey] as List;
    if(myList == null) {
        lock (_listLock) {
            myList = Cache[cacheKey] as List;
            if (myList == null) {
                myList = DAL.ListCustomers();
                Cache.Insert(cacheKey, mList, null, SiteConfig.CacheDuration, TimeSpan.Zero);
            }
        }
    }
    return myList;
}

两个线程都可能得到(myList==null)的真结果吗?然后,这两个线程都会调用DAL.ListCustomers()并将结果插入缓存。 - frankadelic
4
解锁后,您需要再次检查缓存,而不是本地的myList变量。 - orip
1
在你的编辑之前,这其实是可以的。如果使用Insert来防止异常,则不需要锁定,只有在确保调用DAL.ListCustomers一次时才需要锁定(尽管如果结果为null,则每次都会调用)。 - marapet

0
我修改了@user378380的代码以增加灵活性。现在,TResult不再返回特定类型,而是返回对象,以接受不同类型的顺序。同时添加了一些参数以增加灵活性。所有的想法都归功于@user378380。
 private static readonly object _lock = new object();


//If getOnly is true, only get existing cache value, not updating it. If cache value is null then      set it first as running action method. So could return old value or action result value.
//If getOnly is false, update the old value with action result. If cache value is null then      set it first as running action method. So always return action result value.
//With oldValueReturned boolean we can cast returning object(if it is not null) appropriate type on main code.


 public static object GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action,
    DateTime absoluteExpireTime, TimeSpan slidingExpireTime, bool getOnly, out bool oldValueReturned)
{
    object result;
    var data = cache[key]; 

    if (data == null)
    {
        lock (_lock)
        {
            data = cache[key];

            if (data == null)
            {
                oldValueReturned = false;
                result = action();

                if (result == null)
                {                       
                    return result;
                }

                cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
            }
            else
            {
                if (getOnly)
                {
                    oldValueReturned = true;
                    result = data;
                }
                else
                {
                    oldValueReturned = false;
                    result = action();
                    if (result == null)
                    {                            
                        return result;
                    }

                    cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
                }
            }
        }
    }
    else
    {
        if(getOnly)
        {
            oldValueReturned = true;
            result = data;
        }
        else
        {
            oldValueReturned = false;
            result = action();
            if (result == null)
            {
                return result;
            }

            cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
        }            
    }

    return result;
}

0
这篇来自CodeGuru的文章解释了各种缓存锁定场景以及ASP.NET缓存锁定的一些最佳实践。
在ASP.NET中同步缓存访问的方法请参考《在ASP.NET中同步缓存访问》

0

我写了一个库来解决这个特定的问题:Rocks.Caching

此外,我还详细介绍了这个问题,并解释了为什么它很重要 here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接