asp.netでキャッシュをロックするための最良の方法は何ですか?


80

長時間実行されるプロセスなどの特定の状況では、ASP.NETキャッシュをロックして、そのリソースに対する別のユーザーによる後続の要求が、キャッシュにアクセスする代わりに長いプロセスを再度実行しないようにすることが重要であることを知っています。

ASP.NETでキャッシュロックを実装するためのC#の最良の方法は何ですか?

回答:


114

基本的なパターンは次のとおりです。

  • キャッシュで値を確認し、利用可能な場合は返します
  • 値がキャッシュにない場合は、ロックを実装します
  • ロック内で、キャッシュをもう一度確認してください。ブロックされている可能性があります
  • 値の検索を実行してキャッシュします
  • ロックを解除します

コードでは、次のようになります。

private static object ThisLock = new object();

public string GetFoo()
{

  // try to pull from cache here

  lock (ThisLock)
  {
    // cache was empty before we got the lock, check again inside the lock

    // cache is still empty, so retreive the value here

    // store the value in the cache here
  }

  // return the cached value here

}

4
キャッシュの最初のロードに数分かかる場合でも、すでにロードされているエントリにアクセスする方法はありますか?GetFoo_AmazonArticlesByCategory(string categoryKey)がある場合にしましょう。categoryKeyごとのロックのようなものだと思います。
マティアスF

5
「ダブルチェックロック」と呼ばれます。en.wikipedia.org/wiki/Double-checked_locking
Brad Gagne

32

完全を期すために、完全な例は次のようになります。

private static object ThisLock = new object();
...
object dataObject = Cache["globalData"];
if( dataObject == null )
{
    lock( ThisLock )
    {
        dataObject = Cache["globalData"];

        if( dataObject == null )
        {
            //Get Data from db
             dataObject = GlobalObj.GetData();
             Cache["globalData"] = dataObject;
        }
    }
}
return dataObject;

7
if(dataObject == null){lock(ThisLock){if(dataObject == null)//もちろんそれはまだnullです!
コンスタンティン

30
@Constantin:実際には、lock()の取得を待っている間に誰かがキャッシュを更新した可能性があります
Tudor Olariu 2009

12
@John Owen-lockステートメントの後、キャッシュからオブジェクトを再度取得する必要があります。
Pavel Nikolov

3
-1、コードが間違っています(他のコメントを読んでください)、なぜあなたはそれを修正しませんか?人々はあなたの例を使おうとするかもしれません。
orip 2010

11
このコードは実際にはまだ間違っています。globalObject実際には存在しないスコープに戻ってきます。何が起こるかというと、それ dataObjectは最終的なnullチェック内で使用されるべきであり、globalObjectはイベントが存在する必要はまったくありません。
スコットアンダーソン

22

キャッシュインスタンス全体をロックする必要はありません。挿入する特定のキーをロックするだけで済みます。つまり、男性用トイレを使用している間、女性用トイレへのアクセスをブロックする必要はありません:)

以下の実装では、並行辞書を使用して特定のキャッシュキーをロックできます。このようにして、2つの異なるキーに対して同時にGetOrAdd()を実行できますが、同じキーに対して同時に実行することはできません。

using System;
using System.Collections.Concurrent;
using System.Web.Caching;

public static class CacheExtensions
{
    private static ConcurrentDictionary<string, object> keyLocks = new ConcurrentDictionary<string, object>();

    /// <summary>
    /// Get or Add the item to the cache using the given key. Lazily executes the value factory only if/when needed
    /// </summary>
    public static T GetOrAdd<T>(this Cache cache, string key, int durationInSeconds, Func<T> factory)
        where T : class
    {
        // Try and get value from the cache
        var value = cache.Get(key);
        if (value == null)
        {
            // If not yet cached, lock the key value and add to cache
            lock (keyLocks.GetOrAdd(key, new object()))
            {
                // Try and get from cache again in case it has been added in the meantime
                value = cache.Get(key);
                if (value == null && (value = factory()) != null)
                {
                    // TODO: Some of these parameters could be added to method signature later if required
                    cache.Insert(
                        key: key,
                        value: value,
                        dependencies: null,
                        absoluteExpiration: DateTime.Now.AddSeconds(durationInSeconds),
                        slidingExpiration: Cache.NoSlidingExpiration,
                        priority: CacheItemPriority.Default,
                        onRemoveCallback: null);
                }

                // Remove temporary key lock
                keyLocks.TryRemove(key, out object locker);
            }
        }

        return value as T;
    }
}

keyLocks.TryRemove(key, out locker)<=それの用途は何ですか?
iMatoria 2017年

2
これは素晴らしい。キャッシュをロックすることの全体的なポイントは、その特定のキーの値を取得するために行われた作業の重複を避けることです。キャッシュ全体またはその一部をクラスごとにロックするのはばかげています。あなたはまさにこれを望んでいます-「私は<key>の値を取得しています。他のみんなは私を待っているだけです」というロック。拡張方法も巧妙です。1つに2つの素晴らしいアイデア!これは人々が見つける答えでなければなりません。ありがとう。
DanO 2018年

1
@iMatoria、そのキーのキャッシュに何かがあると、そのロックオブジェクトやキーのディクショナリのエントリを保持する意味がありません-ロックが別の人によってディクショナリからすでに削除されている可能性があるため、削除してみてください最初に来たスレッド-そのキーを待ってロックされたすべてのスレッドは、キャッシュから値を取得するだけのコードセクションにありますが、削除するロックはもうありません。
DanO 2018年

私はこのアプローチが受け入れられた答えよりもはるかに好きです。ただし、注意点:最初にcache.Keyを使用し、次にHttpRuntime.Cache.Getを使用します。
staccata 2018

@MindaugasTvaronaviciusいいですね、正解です。T2とT3がfactoryメソッドを同時に実行しているエッジケースです。T1が以前factoryにnullを返したものを実行した場合のみ(したがって、値はキャッシュされません)。それ以外の場合、T2とT3はキャッシュされた値を同時に取得します(これは安全なはずです)。簡単な解決策は削除することkeyLocks.TryRemove(key, out locker)だと思いますが、多数の異なるキーを使用すると、ConcurrentDictionaryがメモリリークになる可能性があります。それ以外の場合は、おそらくセマフォを使用して、削除する前にキーのロックをカウントするロジックを追加しますか?
cwills

13

Pavelが言ったことをエコーするだけで、これが最もスレッドセーフな書き方だと思います

private T GetOrAddToCache<T>(string cacheKey, GenericObjectParamsDelegate<T> creator, params object[] creatorArgs) where T : class, new()
    {
        T returnValue = HttpContext.Current.Cache[cacheKey] as T;
        if (returnValue == null)
        {
            lock (this)
            {
                returnValue = HttpContext.Current.Cache[cacheKey] as T;
                if (returnValue == null)
                {
                    returnValue = creator(creatorArgs);
                    if (returnValue == null)
                    {
                        throw new Exception("Attempt to cache a null reference");
                    }
                    HttpContext.Current.Cache.Add(
                        cacheKey,
                        returnValue,
                        null,
                        System.Web.Caching.Cache.NoAbsoluteExpiration,
                        System.Web.Caching.Cache.NoSlidingExpiration,
                        CacheItemPriority.Normal,
                        null);
                }
            }
        }

        return returnValue;
    }

7
'lock(this) `は悪いです。クラスのユーザーには表示されない専用のロックオブジェクトを使用する必要があります。将来、誰かがキャッシュオブジェクトを使用してロックすることにしたとします。彼らは、それがロックの目的で内部的に使用されていることに気付かないでしょう。
支出者2015


2

私は次の拡張方法を考え出しました:

private static readonly object _lock = new object();

public static TResult GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action, int duration = 300) {
    TResult result;
    var data = cache[key]; // Can't cast using as operator as TResult may be an int or bool

    if (data == null) {
        lock (_lock) {
            data = cache[key];

            if (data == null) {
                result = action();

                if (result == null)
                    return result;

                if (duration > 0)
                    cache.Insert(key, result, null, DateTime.UtcNow.AddSeconds(duration), TimeSpan.Zero);
            } else
                result = (TResult)data;
        }
    } else
        result = (TResult)data;

    return result;
}

@JohnOwenと@ user378380の両方の回答を使用しました。私のソリューションでは、int値とbool値をキャッシュ内に格納することもできます。

エラーがあったり、もう少し上手く書けるかどうか訂正してください。


これは、デフォルトのキャッシュ長である5分(60 * 5 = 300秒)です。
nfplee 2014

3
すばらしい作業ですが、1つの問題があります。複数のキャッシュがある場合、それらはすべて同じロックを共有します。より堅牢にするには、辞書を使用して、指定されたキャッシュに一致するロックを取得します。
JoeCool 2014年

1

最近、Correct State Bag Access Patternと呼ばれる1つのパターンを見ましたが、これはこれに触れているようです。

スレッドセーフになるように少し変更しました。

http://weblogs.asp.net/craigshoemaker/archive/2008/08/28/asp-net-caching-and-performance.aspx

private static object _listLock = new object();

public List List() {
    string cacheKey = "customers";
    List myList = Cache[cacheKey] as List;
    if(myList == null) {
        lock (_listLock) {
            myList = Cache[cacheKey] as List;
            if (myList == null) {
                myList = DAL.ListCustomers();
                Cache.Insert(cacheKey, mList, null, SiteConfig.CacheDuration, TimeSpan.Zero);
            }
        }
    }
    return myList;
}

2つのスレッドの両方が(myList == null)の真の結果を得ることができませんでしたか?次に、両方のスレッドがDAL.ListCustomers()を呼び出し、結果をキャッシュに挿入します。
フランカデリック2010年

4
ロック後、ローカルmyList変数ではなく、キャッシュを再度チェックする必要があります
orip 2010

1
これは、編集前は実際には問題ありませんでした。Insert例外を防ぐために使用する場合は、ロックがDAL.ListCustomers1回呼び出されることを確認する場合にのみ、ロックは必要ありません(ただし、結果がnullの場合は、毎回呼び出されます)。
marapet 2011年



0

柔軟性を高めるために@ user378380のコードを変更しました。TResultを返す代わりに、さまざまなタイプを順番に受け入れるためのオブジェクトを返すようになりました。また、柔軟性のためにいくつかのパラメーターを追加します。すべてのアイデアは@ user378380に属しています。

 private static readonly object _lock = new object();


//If getOnly is true, only get existing cache value, not updating it. If cache value is null then      set it first as running action method. So could return old value or action result value.
//If getOnly is false, update the old value with action result. If cache value is null then      set it first as running action method. So always return action result value.
//With oldValueReturned boolean we can cast returning object(if it is not null) appropriate type on main code.


 public static object GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action,
    DateTime absoluteExpireTime, TimeSpan slidingExpireTime, bool getOnly, out bool oldValueReturned)
{
    object result;
    var data = cache[key]; 

    if (data == null)
    {
        lock (_lock)
        {
            data = cache[key];

            if (data == null)
            {
                oldValueReturned = false;
                result = action();

                if (result == null)
                {                       
                    return result;
                }

                cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
            }
            else
            {
                if (getOnly)
                {
                    oldValueReturned = true;
                    result = data;
                }
                else
                {
                    oldValueReturned = false;
                    result = action();
                    if (result == null)
                    {                            
                        return result;
                    }

                    cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
                }
            }
        }
    }
    else
    {
        if(getOnly)
        {
            oldValueReturned = true;
            result = data;
        }
        else
        {
            oldValueReturned = false;
            result = action();
            if (result == null)
            {
                return result;
            }

            cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
        }            
    }

    return result;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.