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

Posted

技术标签:

【中文标题】在 asp.net 中锁定缓存的最佳方法是啥?【英文标题】:What is the best way to lock cache in asp.net?在 asp.net 中锁定缓存的最佳方法是什么? 【发布时间】:2010-09-07 12:51:19 【问题描述】:

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

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

【问题讨论】:

【参考方案1】:

这是基本模式:

检查缓存中的值,如果可用则返回 如果值不在缓存中,则执行锁 在锁里面,再检查一下缓存,你可能已经被屏蔽了 执行值查找并缓存它 解除锁定

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

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


【讨论】:

如果缓存的第一次加载需要几分钟,是否还有办法访问已加载的条目?假设我有 GetFoo_AmazonArticlesByCategory(string categoryKey)。我想它会像每个 categoryKey 的锁一样。 称为“双重检查锁定”。 en.wikipedia.org/wiki/Double-checked_locking【参考方案2】:

为了完整起见,一个完整的例子看起来像这样。

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;

【讨论】:

if( dataObject == null ) lock(ThisLock) if( dataObject == null ) // 当然还是 null! @Constantin: 不是真的,可能有人在你等待获取 lock() 时更新了缓存 @John Owen - 在锁定语句之后,您必须再次尝试从缓存中获取对象! -1,代码错误(阅读其他cmets),你为什么不修复它?人们可能会尝试使用您的示例。 这段代码其实还是错的。您在实际不存在的范围内返回 globalObject。应该发生的是 dataObject 应该在最终的 null 检查中使用,并且 globalObject 根本不需要存在事件。【参考方案3】:

不需要锁定整个缓存实例,我们只需要锁定您要插入的特定键。 IE。使用男厕所时无需阻止进入女厕所 :)

下面的实现允许使用并发字典锁定特定的缓存键。这样,您可以同时为两个不同的键运行 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,一旦该键的缓存中有某些内容,就没有必要保留该锁定对象或键字典中的条目 - 这是一个 try 删除,因为锁可能已经被另一个首先出现的线程从字典中删除 - 所有被锁定等待该键的线程现在都在该代码部分中,它们只是从缓存中获取值,但没有- 更长的锁要移除。 我比接受的答案更喜欢这种方法。但是小提示:您首先使用 cache.Key,然后再使用 HttpRuntime.Cache.Get。 @MindaugasTvaronavicius 很好,你是对的,这是 T2 和 T3 同时执行 factory 方法的边缘情况。仅在 T1 先前执行返回 null 的 factory 的地方(因此该值不被缓存)。否则 T2 和 T3 将同时获取缓存值(这应该是安全的)。我想简单的解决方案是删除keyLocks.TryRemove(key, out locker),但是如果使用大量不同的键,ConcurrentDictionary 可能会成为内存泄漏。否则在删除之前添加一些逻辑来计算密钥的锁,也许使用信号量?【参考方案4】:

只是为了呼应 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;
    

【讨论】:

'lock(this)` is is bad。您应该使用对您的班级的用户不可见的专用锁定对象。假设,在路上,有人决定使用缓存对象来锁定。他们不会意识到它在内部被用于锁定目的,这可能会导致错误。【参考方案5】:

Craig Shoemaker 在 asp.net 缓存方面的表现非常出色: http://polymorphicpodcast.com/shows/webperformance/

【讨论】:

【参考方案6】:

我想出了以下扩展方法:

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 秒)的默认缓存长度。 干得好,但我发现一个问题:如果你有多个缓存,它们都将共享同一个锁。为了使其更健壮,请使用字典来检索与给定缓存匹配的锁。【参考方案7】:

我最近看到了一种称为正确状态包访问模式的模式,似乎涉及到这一点。

我对其进行了一些修改以保证线程安全。

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() 并将结果插入缓存。 加锁后需要再次检查缓存,而不是本地的myList变量 在您编辑之前这实际上没问题。如果您使用Insert 来防止异常,则不需要锁定,只要您想确保调用一次DAL.ListCustomers(尽管如果结果为null,则每次都会调用它)。【参考方案8】:

CodeGuru 的这篇文章解释了各种缓存锁定方案以及 ASP.NET 缓存锁定的一些最佳实践:

Synchronizing Cache Access in ASP.NET

【讨论】:

【参考方案9】:

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

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

【讨论】:

【参考方案10】:

我修改了@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;

【讨论】:

以上是关于在 asp.net 中锁定缓存的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

在 ASP.NET 应用程序中捕获所有异常的最佳方法是啥?

在 ASP.NET MVC 中构造视图层次结构的最佳方法是啥?

使用 Twitter Bootstrap 在 ASP.NET MVC 中调用模式对话框的最佳方法是啥?

ASP.NET - 阻止应用程序使用的最佳方法是啥?

对 ASP.NET 2.0 网页进行单元测试的最佳方法是啥? [关闭]

在 ASP.NET MVC 中使用 Universe 数据库处理身份验证的最佳方法是啥?