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

2
私はDarthVaderに同意します...あなたは無駄を省くと思いますReaderWriterLockSlim...しかし、私はこの手法を使用しtry-finally発言を避けます。
poy

1
更新されたバージョンでは、単一のcacheLockでロックするのではなく、キーごとにロックします。これはDictionary<string, object>、キーが自分で使用するのと同じキーMemoryCacheであり、ディクショナリ内のオブジェクトが基本であるObject場合に簡単に実行できます。ただし、そうは言っても、ジョンハンナの回答を読んでおくことをお勧めします。適切なプロファイリングを行わないと、2つのSomeHeavyAndExpensiveCalculation()run インスタンスを実行して1つの結果を破棄するよりも、ロックによってプログラムの速度が低下する可能性があります。
スコットチェンバレン

1
キャッシュに高価な値を取得した後にCacheItemPolicyを作成する方が正確であるように思えます。「高価な文字列」(PDFレポートのファイル名が含まれている可能性があります)を返すのに21分かかるサマリーレポートの作成などの最悪のシナリオでは、返される前にすでに「期限切れ」になっています。
Wonderbird 2014

1
@Wonderbird良い点、私はそれを行うために私の答えを更新しました。
スコットチェンバレン2014年

回答:


91

これは私のコードの2回目の反復です。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上記のコードはどのように機能しませんか?また、これは厳密に「ダブルチェックロック」ではなく、同様のパターンに従っているだけで、それを説明するのに最良の方法でした。これが、一種のダブルチェックロックであると私が言った理由です。
スコットチェンバレン

私はあなたのコードについてコメントしませんでした。Double Check Lockingが機能しないとコメントしていました。あなたのコードは大丈夫です。
DarthVader 2014年

1
この種のロックとこの種のストレージがどのような状況で意味をなすかを理解するのは難しいと思います:MemoryCacheチャンスに入るすべての値の作成をロックしている場合、これらの2つのことの少なくとも1つが間違っていました。
Jon Hanna

@ScottChamberlainはこのコードを見ているだけで、ロックの取得とtryブロックの間にスローされる例外の影響を受けやすいのではありませんか。C#In a Nutshellの作者がこれについてここで説明しています。albahari.com
threading / part2.aspx

9
このコードの欠点は、両方ともまだキャッシュされていない場合、CacheKey "A"がCacheKey "B"への要求をブロックすることです。これを解決するには、concurrentDictionary <string、object>を使用して、キャッシュキーを保存してロックします
MichaelD

44

オープンソースライブラリがあります[免責事項:私が書いたもの]:IMOが2行のコードで要件をカバーするLazyCache

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

デフォルトでロックが組み込まれているため、キャッシュ可能なメソッドはキャッシュミスごとに1回だけ実行され、ラムダを使用するため、一度に「取得または追加」を実行できます。デフォルトでは、20分のスライド有効期限です。

NuGetパッケージさえあります ;)


4
キャッシュのダッパー。
Charles Burns

3
これは私が怠惰な開発者になることを可能にし、これが最良の答えです!
jdnew18 2017年

LazyCacheのgithubページが参照している記事に言及する価値は、その背後にある理由のために非常に良い読み物です。alastaircrabtree.com/...
ラファエルマーリン

2
キーごとまたはキャッシュごとにロックしますか?
jjxtra

1
@DirkBoerいいえ、ロックとレイジーがレイジーキャッシュで使用されているため、ブロックされません
alastairtree

30

この問題は、MemoryCacheでAddOrGetExistingメソッドを使用し、遅延初期化を使用することで解決しました。

基本的に、私のコードは次のようになります。

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オブジェクトを2回作成することです。しかし、それはかなり簡単です。オブジェクトのAddOrGetExistingインスタンスが1つだけ取得されることが保証されるLazyため、高価な初期化メソッドを1回だけ呼び出すことが保証されます。


4
このタイプのアプローチの問題は、無効なデータを挿入できることです。SomeHeavyAndExpensiveCalculationThatResultsAString()例外をスローした場合、それはキャッシュに残っています。一時的な例外でもキャッシュされLazy<T>ます:msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner

2
Lazy <T>が初期化例外が失敗した場合にエラーを返す可能性があることは事実ですが、これは検出するのが非常に簡単です。次に、エラーを解決するLazy <T>をキャッシュから削除し、新しいLazy <T>を作成して、それをキャッシュに入れて解決します。私たち自身のコードでも、同様のことをしています。エラーをスローする前に、設定された回数再試行します。
Keith

12
AddOrGetExistingは、アイテムが存在しない場合はnullを返すため、その場合はlazyObjectをチェックして返す必要があります
Gian Marco

1
LazyThreadSafetyMode.PublicationOnlyを使用すると、例外のキャッシュが回避されます。
クレメント

2
このブログ投稿のコメントによると、キャッシュエントリの初期化に非常にコストがかかる場合、すべての可能性があるため、PublicationOnlyを使用するよりも、(ブログ投稿の例に示すように)例外を排除する方が良いです。スレッドは同時にイニシャライザを呼び出すことができます。
bcr 2015

15

このコードには同時実行の問題があると思います:

実際には、改善の可能性はありますが、問題はないでしょう。

ここで、一般に、取得および設定される値をロックしないように、複数のスレッドが最初の使用時に共有値を設定するパターンは、次のとおりです。

  1. 悲惨-他のコードは、インスタンスが1つだけ存在すると想定します。
  2. 悲惨-インスタンスを取得するコードは、1つ(またはおそらく特定の少数)の同時操作しか許容できません。
  3. 悲惨-ストレージの手段はスレッドセーフではありません(たとえば、2つのスレッドを辞書に追加すると、あらゆる種類の厄介なエラーが発生する可能性があります)。
  4. 次善-全体的なパフォーマンスは、ロックによって1つのスレッドのみが値を取得する作業を実行した場合よりも劣ります。
  5. 最適-複数のスレッドが冗長な作業を実行するコストは、特にそれが比較的短い期間にのみ発生する可能性があるため、それを防ぐコストよりも低くなります。

ただし、ここでは MemoryCacheエントリを削除可能性がことを。

  1. 複数のインスタンスを持つことが悲惨な場合 MemoryCacheは、間違ったアプローチです。
  2. 同時作成を防止する必要がある場合は、作成時に行う必要があります。
  3. MemoryCache そのオブジェクトへのアクセスに関してスレッドセーフなので、ここでは問題になりません。

もちろん、これらの可能性の両方を考慮する必要がありますが、同じ文字列の2つのインスタンスが存在するのは、ここで適用されない非常に特別な最適化を行っている場合にのみ問題になる可能性があります*。

だから、私たちは可能性を残しています:

  1. への重複呼び出しのコストを回避する方が安価SomeHeavyAndExpensiveCalculation()です。
  2. への重複呼び出しのコストを回避しない方が安上がりSomeHeavyAndExpensiveCalculation()です。

そして、それを解決することは難しい場合があります(実際、それが解決できると想定するのではなく、プロファイリングに値するようなものです)。ここで検討する価値はありますが、挿入をロックする最も明白な方法は、無関係なものも含め、キャッシュへのすべての追加を防ぐことです。

これは、50のスレッドが50の異なる値を設定しようとしている場合、同じ計算を行うことさえできなかったとしても、50のスレッドすべてを互いに待機させる必要があることを意味します。

そのため、競合状態を回避するコードよりも、自分のコードを使用する方が良いでしょう。競合状態が問題である場合は、別の場所で処理するか、別の方法が必要になる可能性があります。古いエントリを追放するよりもキャッシュ戦略†。

私が変更することの1つは、への呼び出しをSet()1への呼び出しに置き換えることですAddOrGetExisting()です。上記から、おそらく必要ではないことは明らかですが、新しく取得したアイテムを収集できるため、全体のメモリ使用量が削減され、低世代と高世代のコレクションの比率が高くなります。

ええ、二重ロックを使用して同時実行性を防ぐことができますが、同時実行性は実際には問題ではないか、値を間違った方法で格納するか、ストアでの二重ロックはそれを解決する最良の方法ではありません。

*文字列のセットがそれぞれ1つだけ存在することがわかっている場合は、等価比較を最適化できます。これは、文字列の2つのコピーを保持するのが、準最適ではなく誤っている可能性がある場合に限られますが、実行する必要があります。それを理解するための非常に異なるタイプのキャッシング。たとえば、並べ替えXmlReaderは内部で行います。

†無期限に保存するものか、弱参照を利用するもののいずれかであり、既存の用途がない場合にのみエントリを削除します。


1

グローバルロックを回避するには、シングルトンキャッシュを使用して、メモリ使用量を増やすことなく、キーごとに1つのロックを実装します(ロックオブジェクトは参照されなくなったときに削除され、取得/解放はスレッドセーフなので、比較により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

キャッシュに保存しています!
キャッシュから取得しています!
一部1
一部2

    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

どちらを選ぶかは難しいです。ロックまたは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.