web-dev-qa-db-ja.com

MemoryCacheを使用してコストのかかるビルド操作に対処する方法は?

ASP.NET MVCプロジェクトには、構築するのに十分なリソースと時間を必要とするデータのインスタンスがいくつかあります。それらをキャッシュしたい。

MemoryCacheは、一定レベルのスレッドセーフを提供しますが、ビルドコードの複数のインスタンスを並行して実行することを回避するには不十分です。次に例を示します。

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

忙しいWebサイトを見るとわかるように、データが構築されるまで数百のスレッドが同時にifステートメント内に入り、構築操作をさらに遅くし、サーバーリソースを不必要に消費する可能性があります。

MemoryCacheにはアトミックAddOrGetExisting実装がありますが、「設定する値を取得するコード」ではなく、「設定する値」が誤って必要であり、指定されたメソッドをほぼ完全に役に立たないと思います。

私たちは、それを正しくするために、MemoryCacheの周りに独自のアドホック足場を使用していますが、明示的なlocksが必要です。エントリごとのロックオブジェクトを使用するのは面倒であり、通常、理想とはほど遠いロックオブジェクトを共有することで回避できます。そのため、そのような慣習を回避する理由は意図的なものである可能性があると思いました。

だから私は2つの質問があります:

  • コードをlock構築しない方が良いでしょうか? (それは1つの応答性が高いことが証明されている可能性があります、私は疑問に思う)

  • そのようなロックのためにMemoryCacheのエントリごとのロックを実現する正しい方法は何ですか?ロックオブジェクトは ".NETロッキング101"で却下されるため、key文字列を使用する強い衝動。

58
Sedat Kapanoglu

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が例外をスローした場合に何をしたいかを決める必要があります。ただし、利点の1つは、null値もキャッシュできることです。

69
Sedat Kapanoglu

条件付き追加の要件として、私は常に 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のパフォーマンスは控えめに言っても見事です:)

Update有効期限のセマンティクスを年齢のみに基づいた単純な実装。また、@ usrの提案に従って、個々のアイテムが1回だけ作成されるようにする必要もあります。 再度更新-@usrが示唆したように、単に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; 
  }
}
11
Andras Zoltan

ここにあなたが考えているように思われるものに従うデザインがあります。最初のロックは短時間だけ発生します。 data.Valueへの最後の呼び出しも(下で)ロックされますが、クライアントがブロックするのは、そのうちの2つが同時に同じアイテムを要求している場合だけです。

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;
}
1
Neil

LazyとAddOrGetExistingを組み合わせたSedatのソリューションは刺激的です。このソリューションにはパフォーマンスの問題があることを指摘しなければなりません。これは、キャッシュのソリューションにとって非常に重要なようです。

AddOrGetExisting()の code を見ると、AddOrGetExisting()はロックフリーのメソッドではないことがわかります。ロックフリーのGet()メソッドと比較すると、MemoryCacheの利点の1つが無駄になります。

最初にGet()を使用し、次にAddOrGetExisting()を使用してオブジェクトが複数回作成されないようにすることをお勧めします。

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
}
1
Albert Ma

上の答えを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 });
1
cchamberlain

以下は、MemoryCache拡張メソッドとしての簡単な解決策です。

 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;
     }
 }

使用方法の説明としてテストします。

[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
}
0
Oleg