Nice programing

MemoryCache를 사용하여 비용이 많이 드는 건물 작업을 처리하는 방법은 무엇입니까?

nicepro 2020. 12. 12. 12:27
반응형

MemoryCache를 사용하여 비용이 많이 드는 건물 작업을 처리하는 방법은 무엇입니까?


ASP.NET MVC 프로젝트에는 많은 양의 리소스와 빌드 시간이 필요한 데이터 인스턴스가 여러 개 있습니다. 우리는 그것들을 캐시하고 싶습니다.

MemoryCache특정 수준의 스레드 안전성을 제공하지만 빌드 코드의 여러 인스턴스를 병렬로 실행하는 것을 피할만큼 충분하지 않습니다. 다음은 그 예입니다.

var data = cache["key"];
if(data == null)
{
  data = buildDataUsingGoodAmountOfResources();
  cache["key"] = data;
}

바쁜 웹 사이트에서 볼 수 있듯이 수백 개의 스레드가 데이터가 빌드 될 때까지 동시에 if 문에 들어갈 수 있으며 빌드 작업이 더 느려져 불필요하게 서버 리소스를 소비하게됩니다.

AddOrGetExistingMemoryCache 에는 원자 적 구현이 있지만 "설정할 값을 검색하는 코드"대신 "설정할 값"이 잘못 필요합니다. 이는 주어진 메서드를 거의 완전히 쓸모 없게 만든다고 생각합니다.

우리는 MemoryCache 주변에서 우리 자신의 임시 스캐 폴딩을 사용해 왔지만 명시적인 locks 가 필요합니다 . 항목 별 잠금 개체를 사용하는 것은 번거롭고 일반적으로 이상적이지 않은 잠금 개체를 공유하여 벗어날 수 있습니다. 그래서 그러한 관습을 피해야하는 이유가 의도적 일 수 있다고 생각했습니다.

그래서 두 가지 질문이 있습니다.

  • lock코드를 작성 하지 않는 것이 더 나은 방법 입니까? (그것이 한 사람에게 더 반응하는 것으로 입증되었을 수 있습니다.

  • 이러한 잠금에 대해 MemoryCache에 대한 항목 별 잠금을 달성하는 올바른 방법은 무엇입니까? key문자열을 잠금 개체로 사용하려는 강한 충동 은 ".NET locking 101"에서 해제됩니다.


우리는 결합하여이 문제를 해결 Lazy<T>AddOrGetExisting완전히 락 객체에 대한 필요성을 피하기 위해. 다음은 무한 만료를 사용하는 샘플 코드입니다.

public T GetFromCache<T>(string key, Func<T> valueFactory) 
{
    var newValue = new Lazy<T>(valueFactory);
    // the line belows returns existing item or adds the new value if it doesn't exist
    var value = (Lazy<T>)cache.AddOrGetExisting(key, newValue, MemoryCache.InfiniteExpiration);
    return (value ?? newValue).Value; // Lazy<T> handles the locking itself
}

완전하지 않습니다. "예외 캐싱"과 같은 문제가 있으므로 valueFactory가 예외를 throw 할 경우 수행 할 작업을 결정해야합니다. 그러나 장점 중 하나는 null 값을 캐시하는 기능입니다.


조건부 추가 요구 사항의 경우 항상 개체를 빌드해야하는 경우 실행할 대리자를 허용 ConcurrentDictionary하는 오버로드 된 GetOrAdd메서드가있는를 사용합니다.

ConcurrentDictionary<string, object> _cache = new
  ConcurrenctDictionary<string, object>();

public void GetOrAdd(string key)
{
  return _cache.GetOrAdd(key, (k) => {
    //here 'k' is actually the same as 'key'
    return buildDataUsingGoodAmountOfResources();
  });
}

실제로 저는 거의 항상 static동시 사전을 사용 합니다. 나는 ReaderWriterLockSlim인스턴스에 의해 보호되는 '일반적인'사전을 가지고 있었지만 .Net 4로 전환하자마자 (그 이후에만 사용할 수 있음) 내가 발견 한 사전을 변환하기 시작했습니다.

ConcurrentDictionary의 성능은 적어도 말하면 감탄할 만합니다 :)

나이만을 기준으로 만료 의미 체계로 Naive 구현을 업데이트 합니다. 또한 @usr의 제안에 따라 개별 항목이 한 번만 생성되도록해야합니다. 다시 업데이트 -@usr이 제안했듯이-단순히 a를 사용하는 Lazy<T>것이 훨씬 더 간단 할 것입니다. 동시 사전에 추가 할 때 생성 델리게이트를 여기에 전달할 수 있습니다. 실제로 내 잠금 사전이 어쨌든 작동하지 않았기 때문에 코드를 변경했습니다. 하지만 난 정말 자신을 생각한다 (비록 영국에서 여기 자정 내가 이길거야. 어떤 동정심? 당연하지의 아니오. 개발자이기 때문에, 나는 죽은 사람을 깨워 내 혈관을 통해 충분한 카페인 사냥개를 부리는 사냥을 가지고) .

그래도 IRegisteredObject인터페이스를 구현 한 다음 HostingEnvironment.RegisterObject메서드에 등록하는 것이 좋습니다. 이렇게하면 응용 프로그램 풀이 종료 / 재활용 될 때 폴러 스레드를 더 깔끔하게 종료 할 수 있습니다.

public class ConcurrentCache : IDisposable
{
  private readonly ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>> _cache = 
    new ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>>();

  private readonly Thread ExpireThread = new Thread(ExpireMonitor);

  public ConcurrentCache(){
    ExpireThread.Start();
  }

  public void Dispose()
  {
    //yeah, nasty, but this is a 'naive' implementation :)
    ExpireThread.Abort();
  }

  public void ExpireMonitor()
  {
    while(true)
    {
      Thread.Sleep(1000);
      DateTime expireTime = DateTime.Now;
      var toExpire = _cache.Where(kvp => kvp.First != null &&
        kvp.Item1.Value < expireTime).Select(kvp => kvp.Key).ToArray();
      Tuple<string, Lazy<object>> removed;
      object removedLock;
      foreach(var key in toExpire)
      {
        _cache.TryRemove(key, out removed);
      }
    }
  }

  public object CacheOrAdd(string key, Func<string, object> factory, 
    TimeSpan? expiry)
  {
    return _cache.GetOrAdd(key, (k) => { 
      //get or create a new object instance to use 
      //as the lock for the user code
        //here 'k' is actually the same as 'key' 
        return Tuple.Create(
          expiry.HasValue ? DateTime.Now + expiry.Value : (DateTime?)null,
          new Lazy<object>(() => factory(k)));
    }).Item2.Value; 
  }
}

여기에 당신이 염두에 둔 것처럼 보이는 디자인이 있습니다. 첫 번째 잠금은 짧은 시간 동안 만 발생합니다. data.Value에 대한 최종 호출도 (아래) 잠기지 만 클라이언트는 두 사람이 동시에 동일한 항목을 요청하는 경우에만 차단됩니다.

public DataType GetData()
{      
  lock(_privateLockingField)
  {
    Lazy<DataType> data = cache["key"] as Lazy<DataType>;
    if(data == null)
    {
      data = new Lazy<DataType>(() => buildDataUsingGoodAmountOfResources();
      cache["key"] = data;
    }
  }

  return data.Value;
}

C # 7에 대한 최고의 답변을 취하면 여기에 모든 소스 유형 T에서 모든 반환 유형으로 스토리지를 허용하는 구현이 있습니다 TResult.

/// <summary>
/// Creates a GetOrRefreshCache function with encapsulated MemoryCache.
/// </summary>
/// <typeparam name="T">The type of inbound objects to cache.</typeparam>
/// <typeparam name="TResult">How the objects will be serialized to cache and returned.</typeparam>
/// <param name="cacheName">The name of the cache.</param>
/// <param name="valueFactory">The factory for storing values.</param>
/// <param name="keyFactory">An optional factory to choose cache keys.</param>
/// <returns>A function to get or refresh from cache.</returns>
public static Func<T, TResult> GetOrRefreshCacheFactory<T, TResult>(string cacheName, Func<T, TResult> valueFactory, Func<T, string> keyFactory = null) {
    var getKey = keyFactory ?? (obj => obj.GetHashCode().ToString());
    var cache = new MemoryCache(cacheName);
    // Thread-safe lazy cache
    TResult getOrRefreshCache(T obj) {
        var key = getKey(obj);
        var newValue = new Lazy<TResult>(() => valueFactory(obj));
        var value = (Lazy<TResult>) cache.AddOrGetExisting(key, newValue, ObjectCache.InfiniteAbsoluteExpiration);
        return (value ?? newValue).Value;
    }
    return getOrRefreshCache;
}

용법

/// <summary>
/// Get a JSON object from cache or serialize it if it doesn't exist yet.
/// </summary>
private static readonly Func<object, string> GetJson =
    GetOrRefreshCacheFactory<object, string>("json-cache", JsonConvert.SerializeObject);


var json = GetJson(new { foo = "bar", yes = true });

Lazy와 AddOrGetExisting을 결합하는 Sedat의 솔루션은 고무적입니다. 이 솔루션에는 캐싱 솔루션에 매우 중요한 성능 문제가 있음을 지적해야합니다.

If you look at the code of AddOrGetExisting(), you will find that AddOrGetExisting() is not a lock-free method. Comparing to the lock-free Get() method, it wastes the one of the advantage of MemoryCache.

I would like to recommend to follow solution, using Get() first and then use AddOrGetExisting() to avoid creating object multiple times.

public T GetFromCache<T>(string key, Func<T> valueFactory) 
{
    T value = (T)cache.Get(key);
    if (value != null)
    {
        return value;
    }

    var newValue = new Lazy<T>(valueFactory);
    // the line belows returns existing item or adds the new value if it doesn't exist
    var oldValue = (Lazy<T>)cache.AddOrGetExisting(key, newValue, MemoryCache.InfiniteExpiration);
    return (oldValue ?? newValue).Value; // Lazy<T> handles the locking itself
}

Here is simple solution as MemoryCache extension method.

 public static class MemoryCacheExtensions
 {
     public static T LazyAddOrGetExitingItem<T>(this MemoryCache memoryCache, string key, Func<T> getItemFunc, DateTimeOffset absoluteExpiration)
     {
         var item = new Lazy<T>(
             () => getItemFunc(),
             LazyThreadSafetyMode.PublicationOnly // Do not cache lazy exceptions
         );

         var cachedValue = memoryCache.AddOrGetExisting(key, item, absoluteExpiration) as Lazy<T>;

         return (cachedValue != null) ? cachedValue.Value : item.Value;
     }
 }

And test for it as usage description.

[TestMethod]
[TestCategory("MemoryCacheExtensionsTests"), TestCategory("UnitTests")]
public void MemoryCacheExtensions_LazyAddOrGetExitingItem_Test()
{
    const int expectedValue = 42;
    const int cacheRecordLifetimeInSeconds = 42;

    var key = "lazyMemoryCacheKey";
    var absoluteExpiration = DateTimeOffset.Now.AddSeconds(cacheRecordLifetimeInSeconds);

    var lazyMemoryCache = MemoryCache.Default;

    #region Cache warm up

    var actualValue = lazyMemoryCache.LazyAddOrGetExitingItem(key, () => expectedValue, absoluteExpiration);
    Assert.AreEqual(expectedValue, actualValue);

    #endregion

    #region Get value from cache

    actualValue = lazyMemoryCache.LazyAddOrGetExitingItem(key, () => expectedValue, absoluteExpiration);
    Assert.AreEqual(expectedValue, actualValue);

    #endregion
}

참고URL : https://stackoverflow.com/questions/10559279/how-to-deal-with-costly-building-operations-using-memorycache

반응형