.NET MemoryCache의 적절한 사용을위한 잠금 패턴


115

이 코드에는 동시성 문제가 있다고 가정합니다.

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        expensiveString = MemoryCache.Default[CacheKey] as string;
    }
    else
    {
        CacheItemPolicy cip = new CacheItemPolicy()
        {
            AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
        };
        expensiveString = SomeHeavyAndExpensiveCalculation();
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    }
    return expensiveString;
}

동시성 문제의 이유는 여러 스레드가 null 키를 얻은 다음 캐시에 데이터를 삽입하려고 할 수 있기 때문입니다.

이 코드 동시성 증명을 만드는 가장 짧고 깨끗한 방법은 무엇입니까? 캐시 관련 코드에서 좋은 패턴을 따르고 싶습니다. 온라인 기사에 대한 링크는 큰 도움이 될 것입니다.

최신 정보:

@Scott Chamberlain의 답변을 기반 으로이 코드를 생각해 냈습니다. 누구든지 이것으로 성능이나 동시성 문제를 찾을 수 있습니까? 이것이 작동하면 많은 코드 줄과 오류를 줄일 수 있습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
        }

        private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
        private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}

        public static class MemoryCacheHelper
        {
            public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
                where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                {
                    return cachedData;
                }

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                    {
                        return cachedData;
                    }

                    //The value still did not exist so we now write it in to the cache.
                    CacheItemPolicy cip = new CacheItemPolicy()
                    {
                        AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
                    };
                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, cip);
                    return cachedData;
                }
            }
        }
    }
}

3
왜 사용하지 ReaderWriterLockSlim않습니까?
DarthVader

2
나는 DarthVader에 동의합니다 ... 나는 당신이 기댈 것이라고 생각합니다 ReaderWriterLockSlim. 그러나 나는 또한 진술 을 피하기 위해이 기술을 사용할 것 try-finally입니다.
poy

1
업데이트 된 버전의 경우 더 이상 단일 cacheLock을 잠그지 않고 대신 키별로 잠급니다. 이것은 Dictionary<string, object>키가 당신이 사용하는 것과 같은 키 MemoryCache이고 사전의 객체가 Object당신이 잠그는 기본 일 뿐이라면 쉽게 할 수 있습니다 . 그러나 그 말을 듣고 Jon Hanna의 답변을 읽는 것이 좋습니다. 적절한 프로파일 링이 없으면 두 인스턴스를 SomeHeavyAndExpensiveCalculation()실행하고 하나의 결과를 버리는 것보다 잠금을 사용하여 프로그램 속도를 더 느리게 할 수 있습니다 .
Scott Chamberlain

1
값 비싼 값을 캐시에 얻은 후 CacheItemPolicy를 만드는 것이 더 정확할 것 같습니다. "비싼 문자열"(PDF 보고서의 파일 이름을 포함 할 수 있음)을 반환하는 데 21 분이 걸리는 요약 보고서를 생성하는 것과 같은 최악의 시나리오에서는 반환되기 전에 이미 "만료"되었을 것입니다.
Wonderbird

1
@Wonderbird 좋은 지적, 나는 그것을하기 위해 내 대답을 업데이트했습니다.
Scott Chamberlain

답변:


91

이것은 코드의 두 번째 반복입니다. MemoryCache스레드로부터 안전 하기 때문에 초기 읽기를 잠글 필요가 없기 때문에 읽기만 할 수 있으며 캐시가 null을 반환하면 잠금 검사를 수행하여 문자열을 만들어야하는지 확인합니다. 코드를 크게 단순화합니다.

const string CacheKey = "CacheKey";
static readonly object cacheLock = new object();
private static string GetCachedData()
{

    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

    if (cachedString != null)
    {
        return cachedString;
    }

    lock (cacheLock)
    {
        //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
        cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The value still did not exist so we now write it in to the cache.
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        CacheItemPolicy cip = new CacheItemPolicy()
                              {
                                  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
                              };
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
        return expensiveString;
    }
}

편집하다 : 아래 코드는 불필요하지만 원래 방법을 보여주기 위해 남겨두고 싶었습니다. 스레드 안전 읽기는 있지만 스레드 안전 쓰기가 아닌 다른 컬렉션을 사용하는 미래의 방문자에게 유용 할 수 있습니다 ( System.Collections네임 스페이스 아래의 거의 모든 클래스 가 비슷합니다).

다음은 ReaderWriterLockSlim액세스를 보호하기 위해 사용하는 방법 입니다. 잠그기 를 기다리는 동안 다른 사람이 캐시 된 항목을 생성했는지 확인하려면 일종의 " 이중 확인 잠금 "을 수행해야합니다.

const string CacheKey = "CacheKey";
static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
static string GetCachedData()
{
    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    {
        //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }
    }
    finally
    {
        cacheLock.ExitReadLock();
    }

    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    {
        //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The entry still does not exist so we need to create it and enter the write lock
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
        try
        {
            CacheItemPolicy cip = new CacheItemPolicy()
            {
                AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
            };
            MemoryCache.Default.Set(CacheKey, expensiveString, cip);
            return expensiveString;
        }
        finally 
        {
            cacheLock.ExitWriteLock();
        }
    }
    finally
    {
        cacheLock.ExitUpgradeableReadLock();
    }
}

1
@DarthVader 위의 코드는 어떤 식으로 작동하지 않습니까? 또한 이것은 엄격하게 "이중 체크 잠금"이 아닙니다. 나는 단지 비슷한 패턴을 따르고 있으며 그것을 설명 할 수있는 가장 좋은 방법이었습니다. 그것이 내가 일종의 이중 체크 잠금 이라고 말한 이유 입니다.
Scott Chamberlain

나는 당신의 코드에 대해 언급하지 않았습니다. Double Check Locking이 작동하지 않는다고 언급했습니다. 귀하의 코드는 괜찮습니다.
DarthVader

1
이런 종류의 잠금과 이런 종류의 저장이 어떤 상황에서 이치에 맞는지보기가 어렵습니다. 만약 당신이 모든 가치 창조물을 잠그고 있다면 MemoryCache기회가되는 것은이 두 가지 중 적어도 하나가 잘못된 것입니다.
Jon Hanna

@ScottChamberlain은이 코드를보기 만하면 잠금 획득과 try 블록 사이에 예외가 발생하기 쉽습니다. C # In a Nutshell의 저자는 여기에서 이에
BrutalSimplicity

9
이 코드의 단점은 CacheKey "A"가 둘 다 아직 캐시되지 않은 경우 CacheKey "B"에 대한 요청을 차단한다는 것입니다. 이 문제를 해결하기 위해 당신은 잠글 캐시 키를 저장하는 concurrentDictionary <string, object>를 사용할 수 있습니다
MichaelD

44

오픈 소스 라이브러리가 있습니다 [면책 조항 : 내가 작성한] : IMO가 두 줄의 코드로 귀하의 요구 사항을 처리 하는 LazyCache :

IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey", 
  () => SomeHeavyAndExpensiveCalculation());

기본적으로 잠금 기능이 내장되어 있으므로 캐시 가능한 메서드는 캐시 미스 당 한 번만 실행되며 람다를 사용하므로 한 번에 "가져 오기 또는 추가"를 수행 할 수 있습니다. 기본값은 20 분 슬라이딩 만료입니다.

심지어있다 NuGet 패키지 )


4
캐싱의 Dapper.
Charles Burns

3
이것은 나를 게으른 개발자가 될 수있게하여 이것이 최선의 대답이되도록합니다!
jdnew18

LazyCache에 대한 github 페이지가 가리키는 기사를 언급 할 가치가있는 것은 그 뒤에있는 이유 때문에 꽤 좋은 읽기입니다. alastaircrabtree.com/...
라파엘 멀린

2
키 또는 캐시별로 잠기나요?
jjxtra

1
@DirkBoer 아니 그것은 lazycache에서 잠금과 lazy가 사용되는 방식 때문에 차단되지 않을 것입니다
alastairtree

30

MemoryCache 에서 AddOrGetExisting 메서드를 사용하고 Lazy 초기화를 사용 하여이 문제를 해결했습니다 .

기본적으로 내 코드는 다음과 같습니다.

static string GetCachedData(string key, DateTimeOffset offset)
{
    Lazy<String> lazyObject = new Lazy<String>(() => SomeHeavyAndExpensiveCalculationThatReturnsAString());
    var returnedLazyObject = MemoryCache.Default.AddOrGetExisting(key, lazyObject, offset); 
    if (returnedLazyObject == null)
       return lazyObject.Value;
    return ((Lazy<String>) returnedLazyObject).Value;
}

여기서 최악의 시나리오는 동일한 Lazy개체를 두 번 만드는 것 입니다. 그러나 그것은 매우 사소합니다. AddOrGetExisting보장을 사용 하면 Lazy개체의 인스턴스를 하나만 얻을 수 있으므로 값 비싼 초기화 메서드를 한 번만 호출 할 수 있습니다.


4
이러한 유형의 접근 방식의 문제점은 잘못된 데이터를 삽입 할 수 있다는 것입니다. 경우 SomeHeavyAndExpensiveCalculationThatResultsAString()예외 던졌다는 캐시에 붙어있다. 일시적인 예외도 다음과 Lazy<T>같이 캐시됩니다 . msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner 2014 년

2
Lazy <T>가 초기화 예외가 실패하면 오류를 반환 할 수 있다는 사실은 사실이지만 감지하기 매우 쉽습니다. 그런 다음 캐시에서 오류로 해결되는 모든 Lazy <T>를 제거하고 새 Lazy <T>를 만들고이를 캐시에 넣고 해결할 수 있습니다. 우리 자신의 코드에서도 비슷한 일을합니다. 오류가 발생하기 전에 정해진 횟수만큼 재 시도합니다.
Keith

12
항목이 존재하지 않았다 그래서 만약 당신이 확인하고 그 경우에 lazyObject을 반환해야합니다, 반환 널 (null)을 AddOrGetExisting
지안 마르코

1
LazyThreadSafetyMode.PublicationOnly를 사용하면 예외 캐싱을 피할 수 있습니다.
Clement

2
이 블로그 게시물 의 의견에 따르면 캐시 항목을 초기화하는 데 매우 비용이 많이 드는 경우 PublicationOnly를 사용하는 것보다 예외 (블로그 게시물의 예 참조)에서 제거하는 것이 좋습니다. 스레드는 동시에 이니셜 라이저를 호출 할 수 있습니다.
bcr

15

이 코드에는 동시성 문제가 있다고 가정합니다.

실제로는 개선이 가능하지만 상당히 괜찮습니다.

이제 일반적으로 획득 및 설정되는 값을 잠그지 않기 위해 처음 사용할 때 공유 값을 설정하는 여러 스레드가있는 패턴은 다음과 같습니다.

  1. 재앙-다른 코드는 하나의 인스턴스 만 존재한다고 가정합니다.
  2. 비참함-인스턴스를 얻는 코드는 하나 (또는 ​​특정 소수)의 동시 작업 만 허용 할 수 없습니다.
  3. 비참한-저장 수단은 스레드로부터 안전하지 않습니다 (예 : 사전에 두 개의 스레드가 추가되면 모든 종류의 불쾌한 오류가 발생할 수 있습니다).
  4. 차선-잠금으로 인해 하나의 스레드 만 값을 얻는 작업을 수행 한 경우보다 전체 성능이 더 나쁩니다.
  5. 최적-다중 스레드가 중복 작업을 수행하는 비용은 특히 비교적 짧은 기간 동안 만 발생할 수 있기 때문에이를 방지하는 비용보다 적습니다.

그러나 여기에서 MemoryCache항목을 제거 할 수 있음을 고려하면 다음과 같습니다.

  1. 두 개 이상의 인스턴스를 갖는 것이 재앙이라면 MemoryCache 잘못된 접근 방식입니다.
  2. 동시 생성을 방지해야하는 경우 생성 시점에서해야합니다.
  3. MemoryCache 해당 객체에 대한 액세스 측면에서 스레드로부터 안전하므로 여기서는 문제가되지 않습니다.

물론이 두 가지 가능성을 모두 고려해야하지만 동일한 문자열의 두 인스턴스가 존재하는 유일한 경우는 여기에 적용되지 않는 매우 특별한 최적화를 수행하는 경우입니다 *.

따라서 다음과 같은 가능성이 있습니다.

  1. 중복 호출 비용을 피하는 것이 더 저렴합니다. SomeHeavyAndExpensiveCalculation() .
  2. 에 대한 중복 호출 비용을 피하지 않는 것이 더 저렴합니다 SomeHeavyAndExpensiveCalculation().

그리고 그것을 해결하는 것은 어려울 수 있습니다 (실제로, 그것을 해결할 수 있다고 가정하는 것보다 프로파일 링 할 가치가있는 일종의 것입니다). 인서트 잠금의 가장 명백한 방법이 모든 것을 방지 할 수 있지만 여기서 고려할 가치가 있습니다. 은 관련이없는 것을 포함하여 캐시에 대한 추가를 .

즉, 50 개의 서로 다른 값을 설정하려는 50 개의 스레드가있는 경우 동일한 계산을 수행하지 않더라도 50 개의 스레드가 모두 서로를 대기하도록해야합니다.

따라서 경쟁 조건을 피하는 코드보다 보유한 코드를 사용하는 것이 더 낫습니다. 경쟁 조건이 문제인 경우 다른 곳에서 처리해야하거나 다른 코드가 필요합니다. 오래된 항목을 제거하는 것보다 캐싱 전략 †.

내가 바꾸고 싶은 것은 전화를 Set()하나로 바꾸는 것입니다.AddOrGetExisting() . 위에서부터 이것이 필요하지 않을 수도 있지만 새로 얻은 항목을 수집하여 전체 메모리 사용을 줄이고 저 세대 대 고 세대 컬렉션의 비율을 높일 수 있다는 것이 분명합니다.

예, 동시성을 방지하기 위해 이중 잠금을 사용할 수 있지만 동시성이 실제로 문제가되지 않거나 값을 잘못된 방식으로 저장하거나 저장소에 이중 잠금을 사용하는 것이 문제를 해결하는 가장 좋은 방법이 아닙니다. .

* 각 문자열 세트가 하나만 존재한다는 것을 알고 있다면 동등성 비교를 최적화 할 수 있습니다. 이는 문자열의 사본이 두 개있는 유일한 시간이 차선책이 아닌 부정확 할 수 있지만 그렇게하고 싶을 것입니다. 매우 다른 유형의 캐싱이 의미가 있습니다. 예를 들어 정렬 XmlReader은 내부적으로 수행됩니다.

† 무기한 저장하거나 약한 참조를 사용하여 기존 사용이없는 경우에만 항목을 제거 할 가능성이 높습니다.


1

전역 잠금을 방지하려면 SingletonCache를 사용하여 메모리 사용량을 늘리지 않고 키당 하나의 잠금을 구현할 수 있습니다 (잠금 개체는 더 이상 참조되지 않을 때 제거되고 획득 / 해제는 스레드로부터 안전하여 비교를 통해 1 개의 인스턴스 만 사용됨을 보장합니다). 스왑).

사용하면 다음과 같습니다.

SingletonCache<string, object> keyLocks = new SingletonCache<string, object>();

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        return MemoryCache.Default[CacheKey] as string;
    }

    // double checked lock
    using (var lifetime = keyLocks.Acquire(url))
    {
        lock (lifetime.Value)
        {
           if (MemoryCache.Default.Contains(CacheKey))
           {
              return MemoryCache.Default[CacheKey] as string;
           }

           cacheItemPolicy cip = new CacheItemPolicy()
           {
              AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
           };
           expensiveString = SomeHeavyAndExpensiveCalculation();
           MemoryCache.Default.Set(CacheKey, expensiveString, cip);
           return expensiveString;
        }
    }      
}

코드는 GitHub에 있습니다 : https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

또한 MemoryCache보다 가벼운 LRU 구현이 있으며 더 빠른 동시 읽기 및 쓰기, 제한된 크기, 백그라운드 스레드 없음, 내부 성능 카운터 등 여러 가지 이점이 있습니다 (면책 조항, 내가 작성했습니다).


0

콘솔 예제MemoryCache , "저장 방법 / 간단한 클래스 개체를 얻을"

출력 시작하고 누른 후 Any key제외Esc :

캐시에 저장 중!
캐시에서 가져 오기!
Some1
Some2

    class Some
    {
        public String text { get; set; }

        public Some(String text)
        {
            this.text = text;
        }

        public override string ToString()
        {
            return text;
        }
    }

    public static MemoryCache cache = new MemoryCache("cache");

    public static string cache_name = "mycache";

    static void Main(string[] args)
    {

        Some some1 = new Some("some1");
        Some some2 = new Some("some2");

        List<Some> list = new List<Some>();
        list.Add(some1);
        list.Add(some2);

        do {

            if (cache.Contains(cache_name))
            {
                Console.WriteLine("Getting from cache!");
                List<Some> list_c = cache.Get(cache_name) as List<Some>;
                foreach (Some s in list_c) Console.WriteLine(s);
            }
            else
            {
                Console.WriteLine("Saving to cache!");
                cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));                   
            }

        } while (Console.ReadKey(true).Key != ConsoleKey.Escape);

    }

0
public interface ILazyCacheProvider : IAppCache
{
    /// <summary>
    /// Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    /// </summary>
    /// <param name="key"></param>
    /// <param name="getData"></param>
    /// <param name="slidingExpiration"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);
}

/// <summary>
/// Initialize LazyCache in runtime
/// </summary>
public class LazzyCacheProvider: CachingService, ILazyCacheProvider
{
    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();  


    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    {
        var currentPrincipal = Thread.CurrentPrincipal;
        if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
        {
            _hash[dataKey] = null;
            _logger.Debug($"{dataKey} - first start");
            _lastLoad[dataKey] = DateTime.Now;
            _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
            _lastLoad[dataKey] = DateTime.Now;
           _logger.Debug($"{dataKey} - first");
        }
        else
        {
            if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
                Task.Run(() =>
                {
                    if (_reloader.Contains(dataKey)) return;
                    lock (_reloader)
                    {
                        if (ObjectCache.Contains(dataKey))
                        {
                            if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
                                return;
                            _lastLoad[dataKey] = DateTime.Now;
                            Remove(dataKey);
                        }
                        _reloader.Add(dataKey);
                        Thread.CurrentPrincipal = currentPrincipal;
                        _logger.Debug($"{dataKey} - reload start");
                        _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
                        _logger.Debug($"{dataKey} - reload");
                        _reloader.Remove(dataKey);
                    }
                });
        }
        if (_hash[dataKey] != null) return (T) (_hash[dataKey]);

        _logger.Debug($"{dataKey} - dummy start");
        var data = GetOrAdd(dataKey, getData, slidingExpiration);
        _logger.Debug($"{dataKey} - dummy");
        return (T)((object)data).CloneObject();
    }
}

매우 빠른 LazyCache :) REST API 저장소에 대해이 코드를 작성했습니다.
art24war

0

그러나 조금 늦었지만 ... 전체 구현 :

    [HttpGet]
    public async Task<HttpResponseMessage> GetPageFromUriOrBody(RequestQuery requestQuery)
    {
        log(nameof(GetPageFromUriOrBody), nameof(requestQuery));
        var responseResult = await _requestQueryCache.GetOrCreate(
            nameof(GetPageFromUriOrBody)
            , requestQuery
            , (x) => getPageContent(x).Result);
        return Request.CreateResponse(System.Net.HttpStatusCode.Accepted, responseResult);
    }
    static MemoryCacheWithPolicy<RequestQuery, string> _requestQueryCache = new MemoryCacheWithPolicy<RequestQuery, string>();

다음은 getPageContent서명입니다.

async Task<string> getPageContent(RequestQuery requestQuery);

그리고 다음은 MemoryCacheWithPolicy구현입니다.

public class MemoryCacheWithPolicy<TParameter, TResult>
{
    static ILogger _nlogger = new AppLogger().Logger;
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions() 
    {
        //Size limit amount: this is actually a memory size limit value!
        SizeLimit = 1024 
    });

    /// <summary>
    /// Gets or creates a new memory cache record for a main data
    /// along with parameter data that is assocciated with main main.
    /// </summary>
    /// <param name="key">Main data cache memory key.</param>
    /// <param name="param">Parameter model that assocciated to main model (request result).</param>
    /// <param name="createCacheData">A delegate to create a new main data to cache.</param>
    /// <returns></returns>
    public async Task<TResult> GetOrCreate(object key, TParameter param, Func<TParameter, TResult> createCacheData)
    {
        // this key is used for param cache memory.
        var paramKey = key + nameof(param);

        if (!_cache.TryGetValue(key, out TResult cacheEntry))
        {
            // key is not in the cache, create data through the delegate.
            cacheEntry = createCacheData(param);
            createMemoryCache(key, cacheEntry, paramKey, param);

            _nlogger.Warn(" cache is created.");
        }
        else
        {
            // data is chached so far..., check if param model is same (or changed)?
            if(!_cache.TryGetValue(paramKey, out TParameter cacheParam))
            {
                //exception: this case should not happened!
            }

            if (!cacheParam.Equals(param))
            {
                // request param is changed, create data through the delegate.
                cacheEntry = createCacheData(param);
                createMemoryCache(key, cacheEntry, paramKey, param);
                _nlogger.Warn(" cache is re-created (param model has been changed).");
            }
            else
            {
                _nlogger.Trace(" cache is used.");
            }

        }
        return await Task.FromResult<TResult>(cacheEntry);
    }
    MemoryCacheEntryOptions createMemoryCacheEntryOptions(TimeSpan slidingOffset, TimeSpan relativeOffset)
    {
        // Cache data within [slidingOffset] seconds, 
        // request new result after [relativeOffset] seconds.
        return new MemoryCacheEntryOptions()

            // Size amount: this is actually an entry count per 
            // key limit value! not an actual memory size value!
            .SetSize(1)

            // Priority on removing when reaching size limit (memory pressure)
            .SetPriority(CacheItemPriority.High)

            // Keep in cache for this amount of time, reset it if accessed.
            .SetSlidingExpiration(slidingOffset)

            // Remove from cache after this time, regardless of sliding expiration
            .SetAbsoluteExpiration(relativeOffset);
        //
    }
    void createMemoryCache(object key, TResult cacheEntry, object paramKey, TParameter param)
    {
        // Cache data within 2 seconds, 
        // request new result after 5 seconds.
        var cacheEntryOptions = createMemoryCacheEntryOptions(
            TimeSpan.FromSeconds(2)
            , TimeSpan.FromSeconds(5));

        // Save data in cache.
        _cache.Set(key, cacheEntry, cacheEntryOptions);

        // Save param in cache.
        _cache.Set(paramKey, param, cacheEntryOptions);
    }
    void checkCacheEntry<T>(object key, string name)
    {
        _cache.TryGetValue(key, out T value);
        _nlogger.Fatal("Key: {0}, Name: {1}, Value: {2}", key, name, value);
    }
}

nlogger행동 nLog을 추적하는 객체 일뿐 MemoryCacheWithPolicy입니다. 요청 객체 ( RequestQuery requestQuery)가 델리게이트 ( Func<TParameter, TResult> createCacheData)를 통해 변경 되면 메모리 캐시를 다시 생성하거나 슬라이딩 또는 절대 시간이 한계에 도달했을 때 다시 생성합니다. 모든 것이 비동기 적이라는 점에 유의하십시오.)


아마도 귀하의 답변은 다음 질문과 더 관련이있을 수 있습니다. Async threadsafe Get from MemoryCache
Theodor Zoulias

나는 그렇게 생각하지만 여전히 유용한 경험 교환;)
Sam Saarian

0

어느 것이 더 나은지 선택하는 것은 어렵습니다. lock 또는 ReaderWriterLockSlim. 읽기 및 쓰기 숫자 및 비율 등에 대한 실제 통계가 필요합니다.

그러나 "잠금"을 사용하는 것이 올바른 방법이라고 생각한다면. 그런 다음 다양한 요구에 맞는 다른 솔루션이 있습니다. 또한 Allan Xu의 솔루션을 코드에 포함합니다. 둘 다 다른 요구에 필요할 수 있기 때문입니다.

이 솔루션에 대한 요구 사항은 다음과 같습니다.

  1. 어떤 이유로 'GetData'함수를 제공하지 않거나 제공 할 수 없습니다. 아마도 'GetData'함수는 무거운 생성자가있는 다른 클래스에 있으며 인스턴스를 피할 수 없는지 확인하기 전까지는 인스턴스를 만들고 싶지도 않을 것입니다.
  2. 애플리케이션의 다른 위치 / 계층에서 동일한 캐시 된 데이터에 액세스해야합니다. 그리고 그 다른 위치는 같은 사물함에 접근 할 수 없습니다.
  3. 일정한 캐시 키가 없습니다. 예를 들면 다음과 같습니다. sessionId 캐시 키로 일부 데이터를 캐싱해야합니다.

암호:

using System;
using System.Runtime.Caching;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            //Allan Xu's usage
            string xyzData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);

            //My usage
            string sessionId = System.Web.HttpContext.Current.Session["CurrentUser.SessionId"].ToString();
            string yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
            if (string.IsNullOrWhiteSpace(yvz))
            {
                object locker = MemoryCacheHelper.GetLocker(sessionId);
                lock (locker)
                {
                    yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
                    if (string.IsNullOrWhiteSpace(yvz))
                    {
                        DatabaseRepositoryWithHeavyConstructorOverHead dbRepo = new DatabaseRepositoryWithHeavyConstructorOverHead();
                        yvz = dbRepo.GetDataExpensiveDataForSession(sessionId);
                        MemoryCacheHelper.AddDataToCache(sessionId, yvz, 5);
                    }
                }
            }
        }


        private static string SomeHeavyAndExpensiveXYZCalculation() { return "Expensive"; }
        private static string SomeHeavyAndExpensiveABCCalculation() { return "Expensive"; }

        public static class MemoryCacheHelper
        {
            //Allan Xu's solution
            public static T GetCachedDataOrAdd<T>(string cacheKey, object cacheLock, int minutesToExpire, Func<T> GetData) where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                    return cachedData;

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                        return cachedData;

                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, DateTime.Now.AddMinutes(minutesToExpire));
                    return cachedData;
                }
            }

            #region "My Solution"

            readonly static ConcurrentDictionary<string, object> Lockers = new ConcurrentDictionary<string, object>();
            public static object GetLocker(string cacheKey)
            {
                CleanupLockers();

                return Lockers.GetOrAdd(cacheKey, item => (cacheKey, new object()));
            }

            public static T GetCachedData<T>(string cacheKey) where T : class
            {
                CleanupLockers();

                T cachedData = MemoryCache.Default.Get(cacheKey) as T;
                return cachedData;
            }

            public static void AddDataToCache(string cacheKey, object value, int cacheTimePolicyMinutes)
            {
                CleanupLockers();

                MemoryCache.Default.Add(cacheKey, value, DateTimeOffset.Now.AddMinutes(cacheTimePolicyMinutes));
            }

            static DateTimeOffset lastCleanUpTime = DateTimeOffset.MinValue;
            static void CleanupLockers()
            {
                if (DateTimeOffset.Now.Subtract(lastCleanUpTime).TotalMinutes > 1)
                {
                    lock (Lockers)//maybe a better locker is needed?
                    {
                        try//bypass exceptions
                        {
                            List<string> lockersToRemove = new List<string>();
                            foreach (var locker in Lockers)
                            {
                                if (!MemoryCache.Default.Contains(locker.Key))
                                    lockersToRemove.Add(locker.Key);
                            }

                            object dummy;
                            foreach (string lockerKey in lockersToRemove)
                                Lockers.TryRemove(lockerKey, out dummy);

                            lastCleanUpTime = DateTimeOffset.Now;
                        }
                        catch (Exception)
                        { }
                    }
                }

            }
            #endregion
        }
    }

    class DatabaseRepositoryWithHeavyConstructorOverHead
    {
        internal string GetDataExpensiveDataForSession(string sessionId)
        {
            return "Expensive data from database";
        }
    }

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