Wzór blokowania zapewniający prawidłowe korzystanie z .NET MemoryCache

115

Zakładam, że ten kod ma problemy ze współbieżnością:

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

Przyczyną problemu ze współbieżnością jest to, że wiele wątków może uzyskać klucz o wartości null, a następnie próbować wstawić dane do pamięci podręcznej.

Jaki byłby najkrótszy i najczystszy sposób na udowodnienie współbieżności tego kodu? Lubię podążać za dobrym wzorcem w moim kodzie związanym z pamięcią podręczną. Bardzo pomocny byłby link do artykułu online.

AKTUALIZACJA:

Wymyśliłem ten kod na podstawie odpowiedzi @Scott Chamberlain. Czy ktoś może znaleźć w tym problem wydajności lub współbieżności? Jeśli to zadziała, zaoszczędziłoby to wiele linii kodu i błędów.

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;
                }
            }
        }
    }
}
Allan Xu
źródło
3
dlaczego nie używasz ReaderWriterLockSlim?
DarthVader
2
Zgadzam się z DarthVaderem ... Myślę, że opierasz się ... Ale użyłbym ReaderWriterLockSlimrównież tej techniki, aby uniknąć try-finallystwierdzeń.
poy
1
W twojej zaktualizowanej wersji nie blokowałbym już pojedynczego cacheLock, zamiast tego chciałbym blokować według klucza. Można to łatwo zrobić, Dictionary<string, object>jeśli klucz jest tym samym kluczem, którego używasz w swoim, MemoryCachea obiekt w słowniku to tylko podstawowy, Objectktóry blokujesz. Jednak biorąc to pod uwagę, polecam przeczytanie odpowiedzi Jona Hanny. Bez odpowiedniego profilowania możesz bardziej spowalniać program blokując niż pozwalając na dwie instancje SomeHeavyAndExpensiveCalculation()uruchomienia i mieć jeden wynik odrzucony.
Scott Chamberlain
1
Wydaje mi się, że tworzenie CacheItemPolicy po uzyskaniu drogiej wartości do pamięci podręcznej byłoby dokładniejsze. W najgorszym przypadku, takim jak utworzenie raportu podsumowującego, który zajmuje 21 minut, aby zwrócić „kosztowny ciąg znaków” (może zawierać nazwę pliku raportu PDF), byłby już „wygasły”, zanim zostałby zwrócony.
Wonderbird
1
@Wonderbird Dobra uwaga, zaktualizowałem swoją odpowiedź, aby to zrobić.
Scott Chamberlain

Odpowiedzi:

91

To moja druga iteracja kodu. Ponieważ MemoryCachejest bezpieczny wątkowo, nie musisz blokować początkowego odczytu, możesz po prostu przeczytać, a jeśli pamięć podręczna zwróci wartość null, wykonaj sprawdzenie blokady, aby zobaczyć, czy musisz utworzyć ciąg. To znacznie upraszcza kod.

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

EDYCJA : Poniższy kod jest niepotrzebny, ale chciałem go zostawić, aby pokazać oryginalną metodę. Może to być przydatne dla przyszłych odwiedzających, którzy używają innej kolekcji, która ma odczyty bezpieczne dla wątków, ale zapisy bezpieczne bez wątków (prawie wszystkie klasy w System.Collectionsprzestrzeni nazw są takie).

Oto, jak bym to zrobił, używając ReaderWriterLockSlimdo ochrony dostępu. Musisz zrobić coś w rodzaju „ podwójnie zaznaczonego blokowania ”, aby sprawdzić, czy ktoś inny utworzył buforowany element, podczas gdy my czekaliśmy na przejęcie blokady.

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();
    }
}
Scott Chamberlain
źródło
1
@DarthVader w jaki sposób powyższy kod nie zadziała? również nie jest to ściśle „podwójnie sprawdzane blokowanie”. Po prostu trzymam się podobnego schematu i był to najlepszy sposób, w jaki mogłem to opisać. Dlatego powiedziałem, że to rodzaj podwójnie sprawdzanego zamka.
Scott Chamberlain
Nie skomentowałem twojego kodu. Komentowałem, że funkcja Double Check Locking nie działa. Twój kod jest w porządku.
DarthVader
1
Trudno mi zrozumieć, w jakich sytuacjach ten rodzaj blokowania i tego rodzaju przechowywania miałby sens: jeśli blokujesz wszystkie kreacje wartości, istnieje MemoryCacheprawdopodobieństwo, że przynajmniej jedna z tych dwóch rzeczy była nie tak.
Jon Hanna
@ScottChamberlain po prostu patrzy na ten kod i czy nie jest podatny na wyrzucenie wyjątku między nabyciem blokady a blokiem try. Autor C # In a Nutshell omawia to tutaj, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimplicity
9
Wadą tego kodu jest to, że CacheKey "A" blokuje żądanie do CacheKey "B", jeśli oba nie są jeszcze buforowane. Aby rozwiązać ten problem, możesz użyć concurrentDictionary <string, object>, w którym przechowujesz klucze podręczne do zablokowania
MichaelD
44

Istnieje biblioteka open source [zastrzeżenie: które napisałem]: LazyCache, że IMO pokrywa twoje wymagania dwoma wierszami kodu:

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

Ma wbudowane blokowanie domyślnie, więc metoda buforowalna będzie wykonywana tylko raz na brak pamięci podręcznej i używa lambdy, więc możesz wykonać "pobierz lub dodaj" za jednym razem. Domyślnie upływa 20 minut.

Jest nawet pakiet NuGet ;)

alastairtree
źródło
4
Elegancki buforowanie.
Charles Burns
3
To pozwala mi być leniwym programistą, co czyni to najlepszą odpowiedzią!
jdnew18
Warto wspomnieć, że artykuł, na który wskazuje strona github dla LazyCache, jest dość dobrą lekturą z powodów, które za nim stoją. alastaircrabtree.com/…
Rafael Merlin,
2
Czy blokuje się na klucz czy na pamięć podręczną?
jjxtra
1
@DirkBoer nie, to nie zostanie zablokowane z powodu sposobu, w jaki zamki i lenistwo są używane w lazycache
alastairtree
30

Rozwiązałem ten problem, wykorzystując metodę AddOrGetExisting na MemoryCache i używając inicjalizacji Lazy .

Zasadniczo mój kod wygląda mniej więcej tak:

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

Najgorszy scenariusz jest taki, że ten sam Lazyobiekt tworzy się dwukrotnie. Ale to dość trywialne. Użycie AddOrGetExistinggwarancji, że otrzymasz tylko jedno wystąpienie Lazyobiektu, więc masz również gwarancję, że tylko raz wywołasz kosztowną metodę inicjalizacji.

Keith
źródło
4
Problem z tego typu podejściem polega na tym, że można wstawiać nieprawidłowe dane. Jeśli SomeHeavyAndExpensiveCalculationThatResultsAString()zgłosiłeś wyjątek, utknął w pamięci podręcznej. Nawet przejściowe wyjątki zostaną zapisane w pamięci podręcznej Lazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner
2
Chociaż prawdą jest, że Lazy <T> może zwrócić błąd, jeśli wyjątek inicjalizacji nie powiedzie się, jest to dość łatwa rzecz do wykrycia. Następnie możesz wykluczyć dowolny Lazy <T>, który powoduje błąd z pamięci podręcznej, utworzyć nowy Lazy <T>, umieścić go w pamięci podręcznej i rozwiązać go. W naszym własnym kodzie robimy coś podobnego. Ponawiamy określoną liczbę razy, zanim wyrzucimy błąd.
Keith
12
AddOrGetExisting zwraca null, jeśli element nie był obecny, więc w takim przypadku należy sprawdzić i zwrócić lazyObject
Gian Marco
1
Użycie LazyThreadSafetyMode.PublicationOnly pozwoli uniknąć buforowania wyjątków.
Clement
2
Zgodnie z komentarzami w tym wpisie na blogu, jeśli zainicjowanie wpisu pamięci podręcznej jest niezwykle kosztowne, lepiej jest po prostu eksmitować na podstawie wyjątku (jak pokazano w przykładzie w poście na blogu) niż używać PublicationOnly, ponieważ istnieje możliwość, że wszystkie wątki mogą wywoływać inicjator w tym samym czasie.
bcr
15

Zakładam, że ten kod ma problemy ze współbieżnością:

Właściwie jest całkiem możliwe, ale z możliwą poprawą.

Ogólnie rzecz biorąc, wzorzec, w którym mamy wiele wątków ustawiających wspólną wartość przy pierwszym użyciu, aby nie blokować uzyskiwanej i ustawianej wartości, może wyglądać następująco:

  1. Katastrofalne - inny kod zakłada, że ​​istnieje tylko jedna instancja.
  2. Katastrofalne - kod, który uzyskuje instancję, nie może tolerować tylko jednej (lub pewnej niewielkiej liczby) operacji równoległych.
  3. Katastrofalne - sposób przechowywania danych nie jest bezpieczny dla wątków (np. Dodawanie do słownika dwóch wątków i można uzyskać różnego rodzaju okropne błędy).
  4. Nieoptymalne - ogólna wydajność jest gorsza, niż gdyby blokowanie zapewniało, że tylko jeden wątek wykonał pracę w celu uzyskania wartości.
  5. Optymalne - koszt posiadania wielu wątków wykonujących nadmiarową pracę jest mniejszy niż koszt zapobiegania temu, zwłaszcza że może się to zdarzyć tylko w stosunkowo krótkim czasie.

Jednak biorąc pod uwagę, że MemoryCachemoże to spowodować eksmisję wpisów, to:

  1. Jeśli posiadanie więcej niż jednej instancji jest katastrofalne, wtedy MemoryCache jest jest to niewłaściwe podejście.
  2. Jeśli musisz uniemożliwić jednoczesne tworzenie, powinieneś to zrobić w momencie tworzenia.
  3. MemoryCache jest bezpieczny dla wątków pod względem dostępu do tego obiektu, więc nie stanowi to problemu.

Oczywiście należy wziąć pod uwagę obie te możliwości, chociaż jedyny problem z dwoma instancjami tego samego ciągu może stanowić problem, jeśli wykonujesz bardzo szczegółowe optymalizacje, które nie mają tutaj zastosowania *.

Tak więc pozostały nam możliwości:

  1. Taniej jest uniknąć kosztów podwójnych połączeń SomeHeavyAndExpensiveCalculation() .
  2. Taniej jest nie uniknąć kosztów podwójnych połączeń SomeHeavyAndExpensiveCalculation().

Wypracowanie tego może być trudne (w istocie jest to rzecz, w której warto profilować, zamiast zakładać, że możesz to rozwiązać). Warto tutaj jednak wziąć pod uwagę, że najbardziej oczywiste sposoby blokowania wkładki zapobiegną wszystkim dodatkom do pamięci podręcznej, w tym niepowiązanym.

Oznacza to, że gdybyśmy mieli 50 wątków próbujących ustawić 50 różnych wartości, musielibyśmy sprawić, że wszystkie 50 wątków zaczeka na siebie, nawet jeśli nie zamierzały nawet wykonywać tych samych obliczeń.

W związku z tym prawdopodobnie lepiej będzie, jeśli masz kod, niż kod, który unika warunków wyścigu, a jeśli stan wyścigu jest problemem, prawdopodobnie musisz sobie z tym poradzić gdzie indziej lub potrzebujesz innego strategia buforowania niż taka, która usuwa stare wpisy †.

Jedyną rzeczą, którą bym zmienił, jest zamiana połączenia na Set()jeden do AddOrGetExisting(). Z powyższego powinno być jasne, że prawdopodobnie nie jest to konieczne, ale pozwoliłoby na zebranie nowo uzyskanego elementu, zmniejszając ogólne użycie pamięci i pozwalając na wyższy stosunek kolekcji niskiej generacji do kolekcji wysokiej generacji.

Więc tak, możesz użyć podwójnego blokowania, aby zapobiec współbieżności, ale albo współbieżność w rzeczywistości nie jest problemem, albo twoje przechowywanie wartości w niewłaściwy sposób, albo podwójne blokowanie w sklepie nie byłoby najlepszym sposobem rozwiązania tego problemu .

* Jeśli wiesz, że istnieje tylko jeden z każdego zestawu ciągów, możesz zoptymalizować porównania równości, co jest mniej więcej jedynym przypadkiem, gdy posiadanie dwóch kopii łańcucha może być niepoprawne, a nie tylko nieoptymalne, ale chciałbyś to zrobić bardzo różne typy buforowania, aby miało to sens. Np. RodzajXmlReader działa wewnętrznie.

† Całkiem prawdopodobne, że albo taki, który przechowuje na czas nieokreślony, albo taki, który korzysta ze słabych odniesień, więc usunie wpisy tylko wtedy, gdy nie ma istniejących zastosowań.

Jon Hanna
źródło
1

Aby uniknąć globalnej blokady, możesz użyć SingletonCache do zaimplementowania jednej blokady na klucz, bez eksplodującego użycia pamięci (obiekty blokady są usuwane, gdy nie są już przywoływane, a pobieranie / zwalnianie jest bezpieczne wątkowo, co gwarantuje, że tylko 1 wystąpienie jest kiedykolwiek używane poprzez porównanie i zamień).

Używanie go wygląda następująco:

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

Kod jest tutaj na GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

Istnieje również implementacja LRU, która jest lżejsza niż MemoryCache i ma kilka zalet - szybsze równoczesne odczyty i zapisy, ograniczony rozmiar, brak wątku w tle, wewnętrzne liczniki wydajności itp. (Zastrzeżenie, napisałem to).

Alex Peck
źródło
0

Przykładem konsola z MemoryCache „Jak zapisać / dostać prostych obiektów klasy”

Wyjście po uruchomieniu i naciśnięciu Any keyz wyjątkiem Esc:

Zapisuję do pamięci podręcznej!
Pobieranie z pamięci podręcznej!
some1
Some2

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

    }
fr0ga
źródło
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();
    }
}
art24war
źródło
Bardzo szybki LazyCache :) Napisałem ten kod dla repozytoriów REST API.
art24war
0

Jednak trochę za późno ... Pełna realizacja:

    [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>();

Oto getPageContentpodpis:

async Task<string> getPageContent(RequestQuery requestQuery);

A oto MemoryCacheWithPolicyrealizacja:

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

nloggerjest po prostu nLogobiektem do śledzenia MemoryCacheWithPolicyzachowania. Ponownie tworzę pamięć podręczną, jeśli obiekt żądania ( RequestQuery requestQuery) zostanie zmieniony przez delegate ( Func<TParameter, TResult> createCacheData) lub ponownie utworzę, gdy przesunięcie lub bezwzględny czas osiągnął swój limit. Zauważ, że wszystko też jest asynchroniczne;)

Sam Saarian
źródło
Być może Twoja odpowiedź jest bardziej związana z tym pytaniem: Async Threadafe Pobierz z
MemoryCache
Chyba tak, ale nadal przydatna wymiana doświadczeń;)
Sam Saarian
0

Trudno jest wybrać, który jest lepszy; lock lub ReaderWriterLockSlim. Potrzebujesz rzeczywistych statystyk dotyczących odczytu i zapisu liczb i współczynników itp.

Ale jeśli uważasz, że użycie „zamka” jest właściwym sposobem. Tutaj jest inne rozwiązanie dla różnych potrzeb. Do kodu dołączam również rozwiązanie Allana Xu. Ponieważ oba mogą być potrzebne do różnych potrzeb.

Oto wymagania, które prowadzą mnie do tego rozwiązania:

  1. Z jakiegoś powodu nie chcesz lub nie możesz udostępnić funkcji „GetData”. Być może funkcja „GetData” znajduje się w jakiejś innej klasie z ciężkim konstruktorem i nie chcesz nawet tworzyć instancji, dopóki nie upewnisz się, że nie da się jej uniknąć.
  2. Musisz mieć dostęp do tych samych danych w pamięci podręcznej z różnych lokalizacji / poziomów aplikacji. A te różne lokalizacje nie mają dostępu do tego samego obiektu szafki.
  3. Nie masz stałego klucza pamięci podręcznej. Na przykład; potrzeba buforowania niektórych danych za pomocą klucza pamięci podręcznej sessionId.

Kod:

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

}
yvzman
źródło