.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.