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 #?
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;
źródło
globalObject
w zakresie, w którym tak naprawdę nie istnieje. Powinno się zdarzyć, żedataObject
powinno to zostać użyte wewnątrz ostatecznego sprawdzenia null, a globalObject nie musi w ogóle istnieć.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; } }
źródło
keyLocks.TryRemove(key, out locker)
<= jaki jest z tego pożytek?factory
metodę jednocześnie. Tylko wtedy, gdy T1 poprzednio wykonał polecenie,factory
któ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?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; }
źródło
Craig Shoemaker zrobił doskonały program na temat buforowania asp.net: http://polymorphicpodcast.com/shows/webperformance/
źródło
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.
źródło
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; }
źródło
myList
zmienną lokalnąInsert
do zapobiegania wyjątkom, tylko jeśli chcesz się upewnić, żeDAL.ListCustomers
jest wywoływana raz (chociaż jeśli wynik jest równy null, zostanie wywołany za każdym razem).W tym artykule z CodeGuru wyjaśniono różne scenariusze blokowania pamięci podręcznej, a także niektóre najlepsze rozwiązania dotyczące blokowania pamięci podręcznej ASP.NET:
Synchronizowanie dostępu do pamięci podręcznej w ASP.NET
źródło
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 .
źródło
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; }
źródło