asp.net에서 캐시를 잠그는 가장 좋은 방법은 무엇입니까?


80

장기 실행 프로세스와 같은 특정 상황에서는 해당 리소스에 대한 다른 사용자의 후속 요청이 캐시에 도달하는 대신 장기 프로세스를 다시 실행하지 못하도록 ASP.NET 캐시를 잠그는 것이 중요하다는 것을 알고 있습니다.

ASP.NET에서 캐시 잠금을 구현하는 C #의 가장 좋은 방법은 무엇입니까?

답변:


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

5
"이중 검사 잠금"이라고합니다. en.wikipedia.org/wiki/Double-checked_locking
Brad Gagne

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입니다!
Constantin

30
정말, 누군가가 당신이 락의 취득을 대기하는 동안 캐시를 업데이트 할 수 없습니다 () : @Constantin
튜더 Olariu

12
@John Owen-lock 문 후에 캐시에서 객체를 다시 가져와야합니다!
Pavel Nikolov

3
-1, 코드가 잘못되었습니다 (다른 주석을 읽으십시오). 왜 수정하지 않습니까? 사람들은 당신의 모범을 사용하려고 할 것입니다.
orip

11
이 코드는 실제로 여전히 잘못되었습니다. globalObject실제로 존재하지 않는 범위로 돌아오고 있습니다. 해야 할 일은 dataObject최종 null 검사 내에서 사용해야하며 globalObject는 이벤트가 전혀 존재할 필요가 없다는 것입니다.
스콧 앤더슨

22

전체 캐시 인스턴스를 잠글 필요가 없으며 삽입하려는 특정 키만 잠그면됩니다. 즉, 남성 화장실 이용시 여성 화장실 출입을 차단할 필요가 없습니다. :)

아래 구현에서는 동시 사전을 사용하여 특정 캐시 키를 잠글 수 있습니다. 이렇게하면 동시에 두 개의 다른 키에 대해 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이 이전에 factorynull을 반환 한 경우에만 해당 값이 캐시되지 않습니다. 그렇지 않으면 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 초)입니다.
nfplee

3
훌륭합니다.하지만 한 가지 문제가 있습니다. 캐시가 여러 개인 경우 모두 동일한 잠금을 공유합니다. 보다 강력하게하려면 사전을 사용하여 주어진 캐시와 일치하는 잠금을 검색하십시오.
JoeCool 2014

1

최근에 올바른 State Bag Access Pattern이라는 패턴을 보았습니다.

스레드로부터 안전하도록 약간 수정했습니다.

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 2011



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;
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.