Jaki jest najlepszy sposób na zablokowanie pamięci podręcznej w asp.net?

80

Wiem, że w pewnych okolicznościach, takich jak długotrwałe procesy, ważne jest, aby zablokować pamięć podręczną ASP.NET, aby uniknąć kolejnych żądań innego użytkownika dla tego zasobu przed ponownym wykonaniem długiego procesu zamiast uderzania w pamięć podręczną.

Jaki jest najlepszy sposób implementacji blokowania pamięci podręcznej w programie ASP.NET w języku C #?

John Owen
źródło

Odpowiedzi:

114

Oto podstawowy wzór:

  • Sprawdź wartość w pamięci podręcznej, zwróć, jeśli jest dostępna
  • Jeśli wartość nie znajduje się w pamięci podręcznej, zaimplementuj blokadę
  • Wewnątrz zamka sprawdź ponownie pamięć podręczną, mogłeś zostać zablokowany
  • Wyszukaj wartość i zapisz ją w pamięci podręcznej
  • Zwolnij blokadę

W kodzie wygląda to tak:

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

}
a7drew
źródło
4
Jeśli pierwsze ładowanie pamięci podręcznej zajmuje kilka minut, czy nadal istnieje sposób uzyskania dostępu do już załadowanych wpisów? Powiedzmy, czy mam GetFoo_AmazonArticlesByCategory (string categoryKey). Myślę, że byłoby to czymś w rodzaju blokady według kategoriiKlucz.
Mathias F
5
Nazywa się to „podwójnie sprawdzonym blokowaniem”. en.wikipedia.org/wiki/Double-checked_locking
Brad Gagne
32

Dla kompletności pełny przykład wyglądałby mniej więcej tak.

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;
John Owen
źródło
7
if (dataObject == null) {lock (ThisLock) {if (dataObject == null) // oczywiście nadal ma wartość null!
Constantin,
30
@Constantin: niezupełnie, ktoś mógł zaktualizować pamięć podręczną, gdy czekałeś na zakup blokady ()
Tudor Olariu
12
@John Owen - po wyrażeniu blokady musisz ponownie spróbować pobrać obiekt z pamięci podręcznej!
Pavel Nikolov
3
-1, kod jest nieprawidłowy (przeczytaj pozostałe komentarze), dlaczego tego nie naprawisz? Ludzie mogą próbować użyć twojego przykładu.
orip
11
Ten kod jest nadal błędny. Wracasz globalObjectw zakresie, w którym tak naprawdę nie istnieje. Powinno się zdarzyć, że dataObjectpowinno to zostać użyte wewnątrz ostatecznego sprawdzenia null, a globalObject nie musi w ogóle istnieć.
Scott Anderson
22

Nie ma potrzeby blokowania całej instancji pamięci podręcznej, wystarczy tylko zablokować określony klucz, dla którego wstawiasz. To znaczy nie ma potrzeby blokowania dostępu do toalety żeńskiej podczas korzystania z toalety męskiej :)

Poniższa implementacja umożliwia blokowanie określonych kluczy pamięci podręcznej przy użyciu współbieżnego słownika. W ten sposób możesz uruchomić GetOrAdd () dla dwóch różnych kluczy w tym samym czasie - ale nie dla tego samego klucza w tym samym czasie.

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;
    }
}
cwills
źródło
keyLocks.TryRemove(key, out locker)<= jaki jest z tego pożytek?
iMatoria,
2
To jest świetne. Celem blokowania pamięci podręcznej jest uniknięcie powielania pracy wykonanej w celu uzyskania wartości dla tego konkretnego klucza. Blokowanie całej pamięci podręcznej lub nawet jej sekcji według klasy jest głupie. Chcesz właśnie tego - kłódki z napisem „Otrzymuję wartość dla <key>, wszyscy inni po prostu na mnie czekają”. Metoda rozszerzenia też jest sprytna. Dwa świetne pomysły w jednym! To powinna być odpowiedź, którą ludzie znajdują. DZIĘKI.
DanO
1
@iMatoria, gdy w pamięci podręcznej jest coś dla tego klucza, nie ma sensu trzymać tego obiektu blokady w pobliżu lub wpisu w słowniku kluczy - jest to próba usunięcia, ponieważ blokada mogła już zostać usunięta ze słownika przez inny wątek, który pojawił się pierwszy - wszystkie wątki, które zostały zablokowane, czekając na ten klucz, znajdują się teraz w tej sekcji kodu, w której po prostu pobierają wartość z pamięci podręcznej, ale nie ma już blokady do usunięcia.
DanO
Takie podejście podoba mi się znacznie lepiej niż akceptowana odpowiedź. Mała uwaga jednak: najpierw używasz cache.Key, a następnie używasz HttpRuntime.Cache.Get.
staccata
@MindaugasTvaronavicius Dobry chwyt, masz rację, jest to przypadek skrajny, w którym T2 i T3 wykonują factorymetodę jednocześnie. Tylko wtedy, gdy T1 poprzednio wykonał polecenie, factoryktóre zwróciło null (więc wartość nie jest buforowana). W przeciwnym razie T2 i T3 po prostu pobiorą buforowaną wartość jednocześnie (co powinno być bezpieczne). Myślę, że prostym rozwiązaniem jest usunięcie, keyLocks.TryRemove(key, out locker)ale wtedy ConcurrentDictionary może stać się wyciekiem pamięci, jeśli użyjesz dużej liczby różnych kluczy. W przeciwnym razie dodaj trochę logiki, aby policzyć blokady dla klucza przed usunięciem, być może używając semafora?
cwills
13

Aby powtórzyć to, co powiedział Paweł, uważam, że jest to najbardziej bezpieczny sposób pisania

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;
    }
user378380
źródło
7
'lock (this)' jest złe . Powinieneś użyć dedykowanego obiektu blokady, który nie jest widoczny dla użytkowników twojej klasy. Przypuśćmy, że po drodze ktoś zdecyduje się użyć obiektu pamięci podręcznej do zablokowania. Będą nieświadomi, że jest używany wewnętrznie do celów blokowania, co może prowadzić do zła.
wydający
2

Wymyśliłem następującą metodę rozszerzenia:

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

Użyłem zarówno odpowiedzi @John Owen, jak i @ user378380. Moje rozwiązanie umożliwia również przechowywanie wartości int i bool w pamięci podręcznej.

Proszę mnie poprawić, jeśli są jakieś błędy lub czy można to napisać trochę lepiej.

nfplee
źródło
To domyślna długość pamięci podręcznej wynosząca 5 minut (60 * 5 = 300 sekund).
nfplee
3
Świetna robota, ale widzę jeden problem: jeśli masz wiele pamięci podręcznych, wszystkie będą miały tę samą blokadę. Aby uczynić go bardziej niezawodnym, użyj słownika, aby pobrać blokadę dopasowaną do danej pamięci podręcznej.
JoeCool
1

Widziałem ostatnio jeden wzorzec nazwany poprawnym wzorcem dostępu do worka stanu, który zdawał się do tego dotykać.

Zmodyfikowałem go nieco, aby był bezpieczny dla wątków.

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;
}
Seb Nilsson
źródło
Czy dwa wątki nie mogą uzyskać prawdziwego wyniku dla (myList == null)? Następnie oba wątki wywołują DAL.ListCustomers () i wstawiają wyniki do pamięci podręcznej.
frankadelic
4
Po zablokowaniu musisz ponownie sprawdzić pamięć podręczną, a nie myListzmienną lokalną
lubip
1
To było w porządku przed twoją edycją. Nie jest wymagana blokada, jeśli używasz Insertdo zapobiegania wyjątkom, tylko jeśli chcesz się upewnić, że DAL.ListCustomersjest wywoływana raz (chociaż jeśli wynik jest równy null, zostanie wywołany za każdym razem).
marapet
0

Napisałem bibliotekę, która rozwiązuje ten konkretny problem: Rocks.Caching

Również mam blogu o tym problemie w szczegóły i wyjaśnić, dlaczego ważne jest tutaj .

Michael Logutov
źródło
0

Zmodyfikowałem kod @ user378380 dla większej elastyczności. Zamiast zwracać TResult teraz zwraca obiekt do akceptowania różnych typów w kolejności. Dodaje również kilka parametrów dla elastyczności. Cały pomysł należy do @ 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;
}
Tarık Özgün Güner
źródło