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


80

我知道在某些情况下,例如长时间运行的进程,锁定ASP.NET缓存很重要,这样可以避免其他用户对该资源的后续请求再次执行该长时间进程,而不是访问缓存。

用C#在ASP.NET中实现缓存锁定的最佳方法是什么?

Answers:


114

这是基本模式:

  • 检查缓存中的值,如果可用则返回
  • 如果该值不在高速缓存中,则实施锁定
  • 在锁内,再次检查缓存,您可能已被阻止
  • 执行值查找并将其缓存
  • 释放锁

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

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


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
if(dataObject == null){lock(ThisLock){if(dataObject == null)//当然,它仍然为null!
康斯坦丁

30
@康斯坦丁:并非如此,有人在您等待获取lock()时更新了缓存
Tudor Olariu 2009年

12
@John Owen-在lock语句之后,您必须尝试再次从缓存中获取对象!
帕维尔·尼科洛夫

3
-1,代码错误(请阅读其他注释),为什么不修复它?人们可能会尝试使用您的示例。
orip 2010年

11
此代码实际上仍然是错误的。您返回globalObject的范围实际上并不存在。应该发生的是 dataObject应该在最终的null检查中使用它,而globalObject根本不需要事件存在。
Scott Anderson

21

不需要锁定整个缓存实例,而是只需要锁定要插入的特定键。即当您使用公厕时无需阻塞进入女厕的时间:)

下面的实现允许使用并发字典锁定特定的缓存键。这样,您可以同时为两个不同的键运行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先前执行的地方factory返回了null(因此不缓存该值)。否则,T2和T3将仅同时获取缓存的值(应该是安全的)。我想简单的解决方案是删除,keyLocks.TryRemove(key, out locker)但是如果使用大量不同的键,那么ConcurrentDictionary可能会成为内存泄漏。否则,在删除之前添加一些逻辑以计算键的锁,也许使用信号灯?
cwills

13

只是为了回应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)`是坏的。您应该使用一个专用的锁对象,该锁对象对您的类用户不可见。假设在路上,有人决定使用缓存对象进行锁定。他们不会意识到它在内部用于锁定目的,这可能会导致不良后果。
支出者


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的答案。我的解决方案允许您在缓存中存储int和bool值。

如果有任何错误或是否可以写得更好,请更正我。


这是默认的缓存长度为5分钟(60 * 5 = 300秒)。
2014年

3
很好,但是我看到一个问题:如果您有多个缓存,它们将共享相同的锁。为了使其更健壮,请使用字典来检索与给定缓存匹配的锁。
JoeCool 2014年

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 2010年

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;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.