Jak buforować dane w aplikacji MVC


252

Przeczytałem wiele informacji o buforowaniu stron i częściowym buforowaniu stron w aplikacji MVC. Chciałbym jednak wiedzieć, w jaki sposób można buforować dane.

W moim scenariuszu będę używać LINQ do encji (struktura encji). Przy pierwszym wywołaniu GetNames (lub jakiejkolwiek innej metody) chcę pobrać dane z bazy danych. Chcę zapisać wyniki w pamięci podręcznej i przy drugim wywołaniu, aby użyć wersji buforowanej, jeśli istnieje.

Czy ktoś może pokazać przykład, jak to by działało, gdzie to powinno zostać zaimplementowane (model?) I czy to by działało.

Widziałem to w tradycyjnych aplikacjach ASP.NET, zwykle dla bardzo statycznych danych.


1
Przeglądając poniższe odpowiedzi, upewnij się, że chcesz, aby Twój administrator miał wiedzę / odpowiedzialność za dostęp do danych i problemy z buforowaniem. Zasadniczo chcesz to rozdzielić. Zobacz Wzorzec Repozytorium, aby uzyskać dobry sposób: deviq.com/repository-pattern
ssmith

Odpowiedzi:


75

Odwołaj się do biblioteki DLL System.Web w swoim modelu i użyj System.Web.Caching.Cache

    public string[] GetNames()
    {
      string[] names = Cache["names"] as string[];
      if(names == null) //not in cache
      {
        names = DB.GetNames();
        Cache["names"] = names;
      }
      return names;
    }

Trochę uproszczone, ale myślę, że to zadziała. Nie jest to specyficzne dla MVC i zawsze używałem tej metody do buforowania danych.


89
Nie polecam tego rozwiązania: w zamian możesz ponownie otrzymać obiekt zerowy, ponieważ jest on ponownie odczytywany w pamięci podręcznej i mógł już zostać usunięty z pamięci podręcznej. Wolę zrobić: public string [] GetNames () {string [] noms = Cache ["names"]; if (noms == null) {noms = DB.GetNames (); Pamięć podręczna [„names”] = noms; } return (noms); }
Oli

Zgadzam się z Oli .. uzyskanie wyników z rzeczywistego połączenia z
bazą danych

1
Czy to działa z DB.GetNames().AsQueryablemetodą opóźniania zapytania?
Chase Florell,

Nie, chyba że zmienisz wartość zwracaną z string [] na IEnumerable <string>
terjetyl

12
Jeśli nie ustawisz daty wygaśnięcia ... kiedy pamięć podręczna wygasa domyślnie?
Chaka

403

Oto miła i prosta klasa / usługa pomocnika pamięci podręcznej, z której korzystam:

using System.Runtime.Caching;  

public class InMemoryCache: ICacheService
{
    public T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class
    {
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(10));
        }
        return item;
    }
}

interface ICacheService
{
    T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class;
}

Stosowanie:

cacheProvider.GetOrSet("cache key", (delegate method if cache is empty));

Dostawca pamięci podręcznej sprawdzi, czy w pamięci podręcznej jest coś o nazwie „identyfikator pamięci podręcznej”, a jeśli nie, wywoła metodę delegowania w celu pobrania danych i zapisania ich w pamięci podręcznej.

Przykład:

var products=cacheService.GetOrSet("catalog.products", ()=>productRepository.GetAll())

3
Zaadaptowałem to, aby mechanizm buforowania był używany na sesję użytkownika za pomocą HttpContext.Current.Session. Umieściłem również właściwość Cache w mojej klasie BaseController, dzięki czemu jej łatwy dostęp i zaktualizowany konstruktor pozwalają na DI dla testów jednostkowych. Mam nadzieję że to pomoże.
WestDiscGolf

1
Możesz także ustawić tę klasę i metodę na statyczną, aby można ją było ponownie wykorzystać wśród innych kontrolerów.
Alex

5
Ta klasa nie powinna zależeć od HttpContext. Uprościłem to tylko dla przykładu tutaj. Obiekt pamięci podręcznej musi zostać wstawiony przez konstruktor - można go następnie zastąpić innymi mechanizmami buforowania. Wszystko to osiąga się dzięki IoC / DI, wraz ze statycznym (singletonem) cyklem życia.
Hrvoje Hudo

3
@Brendan - i jeszcze gorzej, ma magiczne ciągi znaków dla kluczy pamięci podręcznej, zamiast wnioskować je na podstawie nazwy metody i parametrów.
ssmith

5
To niesamowite rozwiązanie niskiego poziomu. Tak jak inni wspominali, chcielibyście zawinąć to w bezpieczną dla danego typu klasę. Dostęp do tego bezpośrednio w kontrolerach byłby koszmarem z powodu magicznych strun.
Josh Noe

43

Odnoszę się do postu TT i sugeruję następujące podejście:

Odwołaj się do biblioteki DLL System.Web w swoim modelu i użyj System.Web.Caching.Cache

public string[] GetNames()
{ 
    var noms = Cache["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        Cache["names"] = noms; 
    }

    return ((string[])noms);
}

Nie powinieneś zwracać wartości ponownie odczytanej z pamięci podręcznej, ponieważ nigdy nie dowiesz się, czy w danym momencie nadal znajduje się w pamięci podręcznej. Nawet jeśli wstawiłeś go wcześniej do instrukcji, być może już go nie ma lub nigdy nie został dodany do pamięci podręcznej - po prostu nie wiesz.

Więc dodajesz dane odczytane z bazy danych i zwracasz je bezpośrednio, a nie ponowne czytanie z pamięci podręcznej.


Ale czy linia nie jest Cache["names"] = noms;umieszczana w pamięci podręcznej?
Omar,

2
@Baddie Tak to robi. Ale ten przykład różni się od pierwszego, do którego odnosi się Oli, ponieważ nie ma już dostępu do pamięci podręcznej - problem polega na tym, że po prostu robi: return (string []) Cache ["names"]; .. MOŻE spowodować zwrócenie wartości zerowej, ponieważ MOGŁA wygasnąć. To mało prawdopodobne, ale może się zdarzyć. Ten przykład jest lepszy, ponieważ przechowujemy rzeczywistą wartość zwróconą z bazy danych w pamięci, buforujemy tę wartość, a następnie zwracamy tę wartość, a nie wartość ponownie odczytaną z pamięci podręcznej.
jamiebarrow

Lub ... wartość ponownie odczytana z pamięci podręcznej, jeśli nadal istnieje (! = Null). Stąd cały punkt buforowania. To po prostu powiedzieć, że sprawdza dwukrotnie wartości zerowe i odczytuje bazę danych w razie potrzeby. Bardzo sprytny, dzięki Oli!
Sean Kendle,

Czy możesz podać link, w którym mogę przeczytać informacje o buforowaniu aplikacji opartym na kluczowej wartości. Nie mogę znaleźć linków.
Niezniszczalny

@Oli, Jak korzystać z tego rekordu pamięci podręcznej ze strony CSHTML lub HTML
Deepan Raj 10.04.19

37

Dla platformy .NET 4.5+

dodaj referencję: System.Runtime.Caching

dodaj za pomocą instrukcji: using System.Runtime.Caching;

public string[] GetNames()
{ 
    var noms = System.Runtime.Caching.MemoryCache.Default["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        System.Runtime.Caching.MemoryCache.Default["names"] = noms; 
    }

    return ((string[])noms);
}

W .NET Framework 3.5 i wcześniejszych wersjach ASP.NET zapewniał implementację pamięci podręcznej w pamięci w przestrzeni nazw System.Web.Caching. W poprzednich wersjach systemu .NET Framework buforowanie było dostępne tylko w przestrzeni nazw System.Web i dlatego wymagało zależności od klas ASP.NET. W .NET Framework 4 przestrzeń nazw System.Runtime.Caching zawiera interfejsy API zaprojektowane zarówno dla aplikacji WWW, jak i innych.

Więcej informacji:


Skąd Db.GerNames()pochodzi?
Junior

DB.GetNames to tylko metoda z DAL, która pobiera niektóre nazwy z bazy danych. To jest to, co normalnie byś odzyskał.
juFo

To powinno być na szczycie, ponieważ ma aktualne odpowiednie rozwiązanie
BYISHIMO Audace

2
Dzięki, potrzebowałem również dodać pakiet nuget System.Runtime.Caching (v4.5).
Steve Greene,

26

Steve Smith napisał dwa świetne posty na blogu, które pokazują, jak używać wzorca CachedRepository w ASP.NET MVC. Efektywnie wykorzystuje wzorzec repozytorium i pozwala uzyskać buforowanie bez konieczności zmiany istniejącego kodu.

http://ardalis.com/Introducing-the-CachedRepository-Pattern

http://ardalis.com/building-a-cachedrepository-via-strategy-pattern

W tych dwóch postach pokazuje, jak skonfigurować ten wzór, a także wyjaśnia, dlaczego jest on użyteczny. Korzystając z tego wzorca, uzyskujesz buforowanie, a istniejący kod nie widzi logiki buforowania. Zasadniczo używasz buforowanego repozytorium tak, jakby to było inne repozytorium.


1
Świetne posty! Dzięki za udostępnienie !!
Mark Good

Linki nieaktualne według stanu na 31.08.2013.
CBono


Czy możesz podać link, w którym mogę przeczytać informacje o buforowaniu aplikacji opartym na kluczowej wartości. Nie mogę znaleźć linków.
Niezniszczalny

4

AppFabric Caching jest rozproszone i technika buforowania w pamięci, która przechowuje dane w parach klucz-wartość za pomocą pamięci fizycznej na wielu serwerach. AppFabric zapewnia ulepszenia wydajności i skalowalności aplikacji .NET Framework. Koncepcje i architektura


Dotyczy to konkretnie platformy Azure, a nie ASP.NET MVC.
Henry C

3

Rozszerzając odpowiedź @Hrvoje Hudo ...

Kod:

using System;
using System.Runtime.Caching;

public class InMemoryCache : ICacheService
{
    public TValue Get<TValue>(string cacheKey, int durationInMinutes, Func<TValue> getItemCallback) where TValue : class
    {
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }

    public TValue Get<TValue, TId>(string cacheKeyFormat, TId id, int durationInMinutes, Func<TId, TValue> getItemCallback) where TValue : class
    {
        string cacheKey = string.Format(cacheKeyFormat, id);
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback(id);
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }
}

interface ICacheService
{
    TValue Get<TValue>(string cacheKey, Func<TValue> getItemCallback) where TValue : class;
    TValue Get<TValue, TId>(string cacheKeyFormat, TId id, Func<TId, TValue> getItemCallback) where TValue : class;
}

Przykłady

Buforowanie pojedynczego elementu (gdy każdy element jest buforowany na podstawie jego identyfikatora, ponieważ buforowanie całego katalogu dla typu elementu byłoby zbyt intensywne).

Product product = cache.Get("product_{0}", productId, 10, productData.getProductById);

Buforowanie wszystkiego

IEnumerable<Categories> categories = cache.Get("categories", 20, categoryData.getCategories);

Dlaczego TId

Drugi pomocnik jest szczególnie miły, ponieważ większość kluczy danych nie jest złożona. Dodatkowe metody można dodać, jeśli często używasz kluczy kompozytowych. W ten sposób unikasz wykonywania wszelkiego rodzaju konkatenacji łańcuchów lub łańcuchów. Formaty, aby uzyskać klucz do przekazania do pomocnika pamięci podręcznej. Ułatwia także przekazywanie metody dostępu do danych, ponieważ nie trzeba przekazywać identyfikatora do metody otoki ... całość staje się bardzo zwięzła i spójna w większości przypadków użycia.


1
W definicjach interfejsu brakuje parametru „durationInMinutes”. ;-)
Tech0

3

Oto poprawka do odpowiedzi Hrvoje Hudo. Ta implementacja ma kilka kluczowych ulepszeń:

  • Klucze pamięci podręcznej są tworzone automatycznie na podstawie funkcji aktualizacji danych, a przekazywany obiekt określa zależności
  • Mijaj przedział czasu dla dowolnego czasu trwania pamięci podręcznej
  • Używa blokady dla bezpieczeństwa wątku

Zauważ, że jest to zależne od Newtonsoft.Json do serializacji obiektu dependOn, ale można to łatwo wymienić na dowolną inną metodę serializacji.

ICache.cs

public interface ICache
{
    T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class;
}

InMemoryCache.cs

using System;
using System.Reflection;
using System.Runtime.Caching;
using Newtonsoft.Json;

public class InMemoryCache : ICache
{
    private static readonly object CacheLockObject = new object();

    public T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class
    {
        string cacheKey = GetCacheKey(getItemCallback, dependsOn);
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            lock (CacheLockObject)
            {
                item = getItemCallback();
                MemoryCache.Default.Add(cacheKey, item, DateTime.Now.Add(duration));
            }
        }
        return item;
    }

    private string GetCacheKey<T>(Func<T> itemCallback, object dependsOn) where T: class
    {
        var serializedDependants = JsonConvert.SerializeObject(dependsOn);
        var methodType = itemCallback.GetType();
        return methodType.FullName + serializedDependants;
    }
}

Stosowanie:

var order = _cache.GetOrSet(
    () => _session.Set<Order>().SingleOrDefault(o => o.Id == orderId)
    , new { id = orderId }
    , new TimeSpan(0, 10, 0)
);

2
if (item == null)Powinny być wewnątrz zamka. Teraz, gdy ifjest to przed śluzą, mogą wystąpić warunki wyścigu. Lub jeszcze lepiej, powinieneś trzymać ifprzed blokadą, ale sprawdź ponownie, czy pamięć podręczna jest nadal pusta jako pierwsza linia w zamku. Ponieważ jeśli dwa wątki pojawią się w tym samym czasie, oba zaktualizują pamięć podręczną. Twoja obecna blokada nie jest pomocna.
Al Kepp

3
public sealed class CacheManager
{
    private static volatile CacheManager instance;
    private static object syncRoot = new Object();
    private ObjectCache cache = null;
    private CacheItemPolicy defaultCacheItemPolicy = null;

    private CacheEntryRemovedCallback callback = null;
    private bool allowCache = true;

    private CacheManager()
    {
        cache = MemoryCache.Default;
        callback = new CacheEntryRemovedCallback(this.CachedItemRemovedCallback);

        defaultCacheItemPolicy = new CacheItemPolicy();
        defaultCacheItemPolicy.AbsoluteExpiration = DateTime.Now.AddHours(1.0);
        defaultCacheItemPolicy.RemovedCallback = callback;
        allowCache = StringUtils.Str2Bool(ConfigurationManager.AppSettings["AllowCache"]); ;
    }
    public static CacheManager Instance
    {
        get
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new CacheManager();
                    }
                }
            }

            return instance;
        }
    }

    public IEnumerable GetCache(String Key)
    {
        if (Key == null || !allowCache)
        {
            return null;
        }

        try
        {
            String Key_ = Key;
            if (cache.Contains(Key_))
            {
                return (IEnumerable)cache.Get(Key_);
            }
            else
            {
                return null;
            }
        }
        catch (Exception)
        {
            return null;
        }
    }

    public void ClearCache(string key)
    {
        AddCache(key, null);
    }

    public bool AddCache(String Key, IEnumerable data, CacheItemPolicy cacheItemPolicy = null)
    {
        if (!allowCache) return true;
        try
        {
            if (Key == null)
            {
                return false;
            }

            if (cacheItemPolicy == null)
            {
                cacheItemPolicy = defaultCacheItemPolicy;
            }

            String Key_ = Key;

            lock (Key_)
            {
                return cache.Add(Key_, data, cacheItemPolicy);
            }
        }
        catch (Exception)
        {
            return false;
        }
    }

    private void CachedItemRemovedCallback(CacheEntryRemovedArguments arguments)
    {
        String strLog = String.Concat("Reason: ", arguments.RemovedReason.ToString(), " | Key-Name: ", arguments.CacheItem.Key, " | Value-Object: ", arguments.CacheItem.Value.ToString());
        LogManager.Instance.Info(strLog);
    }
}

3
Zastanów się nad dodaniem jakiegoś wyjaśnienia
Mike Debela

2

Użyłem go w ten sposób i działa dla mnie. https://msdn.microsoft.com/en-us/library/system.web.caching.cache.add(v=vs.110).aspx informacje o parametrach dla system.web.caching.cache.add.

public string GetInfo()
{
     string name = string.Empty;
     if(System.Web.HttpContext.Current.Cache["KeyName"] == null)
     {
         name = GetNameMethod();
         System.Web.HttpContext.Current.Cache.Add("KeyName", name, null, DateTime.Noew.AddMinutes(5), Cache.NoSlidingExpiration, CacheitemPriority.AboveNormal, null);
     }
     else
     {
         name = System.Web.HttpContext.Current.Cache["KeyName"] as string;
     }

      return name;

}

dodatkowe głosy poparcia dla pełnych kwalifikacji z pełną przestrzenią nazw !!
Ninjanoel

1

Używam dwóch klas. Najpierw obiekt rdzenia pamięci podręcznej:

public class Cacher<TValue>
    where TValue : class
{
    #region Properties
    private Func<TValue> _init;
    public string Key { get; private set; }
    public TValue Value
    {
        get
        {
            var item = HttpRuntime.Cache.Get(Key) as TValue;
            if (item == null)
            {
                item = _init();
                HttpContext.Current.Cache.Insert(Key, item);
            }
            return item;
        }
    }
    #endregion

    #region Constructor
    public Cacher(string key, Func<TValue> init)
    {
        Key = key;
        _init = init;
    }
    #endregion

    #region Methods
    public void Refresh()
    {
        HttpRuntime.Cache.Remove(Key);
    }
    #endregion
}

Drugi to lista obiektów pamięci podręcznej:

public static class Caches
{
    static Caches()
    {
        Languages = new Cacher<IEnumerable<Language>>("Languages", () =>
                                                          {
                                                              using (var context = new WordsContext())
                                                              {
                                                                  return context.Languages.ToList();
                                                              }
                                                          });
    }
    public static Cacher<IEnumerable<Language>> Languages { get; private set; }
}

0

Powiem, że wdrożenie Singleton w tym utrzymującym się problemie z danymi może być rozwiązaniem tego problemu, na wypadek gdyby poprzednie rozwiązania były bardzo skomplikowane

 public class GPDataDictionary
{
    private Dictionary<string, object> configDictionary = new Dictionary<string, object>();

    /// <summary>
    /// Configuration values dictionary
    /// </summary>
    public Dictionary<string, object> ConfigDictionary
    {
        get { return configDictionary; }
    }

    private static GPDataDictionary instance;
    public static GPDataDictionary Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new GPDataDictionary();
            }
            return instance;
        }
    }

    // private constructor
    private GPDataDictionary() { }

}  // singleton

To działało dla mnie idealnie, dlatego polecam to każdemu, komu może pomóc
GeraGamo,


-8

Możesz także spróbować użyć buforowania wbudowanego w ASP MVC:

Dodaj następujący atrybut do metody kontrolera, którą chcesz buforować:

[OutputCache(Duration=10)]

W takim przypadku ActionResult będzie buforowany przez 10 sekund.

Więcej na ten temat tutaj


4
OutputCache służy do renderowania akcji, pytanie dotyczyło buforowania danych, a nie strony.
Coolcoder,

jest nie na temat, ale OutputCache również buforuje dane w bazie danych
Muflix
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.